Instructions to use BorisTM/loss-guided-static-multi with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use BorisTM/loss-guided-static-multi with sentence-transformers:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("BorisTM/loss-guided-static-multi") sentences = [ "The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium." ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [3, 3] - Notebooks
- Google Colab
- Kaggle
Remove internal research files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- evaluation/compact-multilingual-comparison.json +0 -84
- evaluation/full-mmteb-summary.json +0 -233
- provenance/model-manifest.json +0 -77
- run_config.json +0 -753
- starse/__init__.py +0 -3
- starse/binary_static_embedding.py +0 -160
- starse/cached_loss.py +0 -223
- starse/checkpoint_precision.py +0 -166
- starse/collapse.py +0 -1930
- starse/compiled_latent_encoder.py +0 -281
- starse/conditioned_module.py +0 -738
- starse/gradient_safety.py +0 -161
- starse/grove_counterfactual.py +0 -293
- starse/grove_selection.py +0 -202
- starse/init_model.py +0 -362
- starse/language_conditioned.py +0 -165
- starse/latent_compilation.py +0 -1095
- starse/latent_lexicon.py +0 -1097
- starse/latent_profile.py +0 -267
- starse/latent_profiling.py +0 -975
- starse/latent_streaming.py +0 -1065
- starse/latent_training.py +0 -762
- starse/loss.py +0 -182
- starse/loss_path_vocabulary.py +0 -161
- starse/ngram_table.py +0 -86
- starse/online_tokenizer.py +0 -378
- starse/online_tokenizer_compilation.py +0 -686
- starse/openlex_compilation.py +0 -428
- starse/openlex_dictionary.py +0 -322
- starse/openlex_lattice.py +0 -396
- starse/openlex_model.py +0 -1196
- starse/openlex_replay.py +0 -604
- starse/openlex_replay_checkpoint.py +0 -168
- starse/openlex_reuse.py +0 -338
- starse/openlex_spans.py +0 -215
- starse/openlex_tokenizer.py +0 -180
- starse/progressive_model.py +0 -395
- starse/project.py +0 -120
- starse/recursive_cascade.py +0 -1078
- starse/split_vocabulary.py +0 -105
- starse/streaming.py +0 -1023
- starse/structured_tokenizer.py +0 -151
- starse/train.py +0 -508
- starse/train_multilingual.py +0 -0
- starse/train_progressive.py +0 -340
- starse/translation_edit.py +0 -142
- starse/vocab_assignment.py +0 -73
- starse/vocabulary_growth.py +0 -598
- starse/vocabulary_probe.py +0 -720
- starse/vocabulary_telemetry.py +0 -627
evaluation/compact-multilingual-comparison.json
DELETED
|
@@ -1,84 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"protocol": "q42-posthoc-final-crosslingual-comparison-v1",
|
| 3 |
-
"evaluation_registration": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-posthoc-lr1e4-5k-v1/crosslingual-evaluation-registration-v2.json",
|
| 4 |
-
"evaluation_registration_sha256": "63ba618a243b7159fb6580489d621a64a9fb8a82bb395acb6b31900d0991c01b",
|
| 5 |
-
"suite": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/configs/eval/q42-crosslingual-v2.json",
|
| 6 |
-
"suite_sha256": "291cd152569c504e854fb43188f92df92a538771b6c58615916ba1aa9c4103c2",
|
| 7 |
-
"tasks": [
|
| 8 |
-
"Tatoeba",
|
| 9 |
-
"BUCC.v2",
|
| 10 |
-
"FloresBitextMining"
|
| 11 |
-
],
|
| 12 |
-
"task_subset_counts": {
|
| 13 |
-
"Tatoeba": 106,
|
| 14 |
-
"BUCC.v2": 4,
|
| 15 |
-
"FloresBitextMining": 193
|
| 16 |
-
},
|
| 17 |
-
"treatment": {
|
| 18 |
-
"name": "posthoc_lr1e4_checkpoint_5000",
|
| 19 |
-
"branch_step": 5000,
|
| 20 |
-
"cumulative_model_step": 25000,
|
| 21 |
-
"checkpoint": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/outputs/q42-posthoc-lr1e4-5k-v1/main/checkpoint-5000",
|
| 22 |
-
"validation_manifest": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-posthoc-lr1e4-5k-v1/crosslingual-evaluation-v2/checkpoint-5000/validation_manifest.json",
|
| 23 |
-
"validation_manifest_sha256": "2e76e5d08baaf4433bcb7b431c72910311958f69b2c9b27aa0d9d8cecf9db41e",
|
| 24 |
-
"model_sha256": {
|
| 25 |
-
"config_sentence_transformers.json": "60c7241cedada0af5b9865fcd4a92ca4006d166921399a8cf8b609300f760365",
|
| 26 |
-
"language_conditioning.json": "e9c6eab6df244674476b62abe637379972180d609094fd78f26eea13450208ab",
|
| 27 |
-
"model.safetensors": "c28eaa242185aeccbd5ff643bb517b5a93d10b9d5a71c85dd03373ca4b2ad51a",
|
| 28 |
-
"modules.json": "f6af1ace0b140cab4fd6126cb8312a07c4fc5551443ce8bc4cb15a752080fab5",
|
| 29 |
-
"tokenizer.json": "cc62b0c9d8e67411174ea331ff4aad01706b51b6d60ded1380dd4eba6f33c5cc"
|
| 30 |
-
},
|
| 31 |
-
"scores": {
|
| 32 |
-
"Tatoeba": 0.572618920769259,
|
| 33 |
-
"BUCC.v2": 0.981170343691566,
|
| 34 |
-
"FloresBitextMining": 0.6370263037274586
|
| 35 |
-
},
|
| 36 |
-
"unweighted_task_macro": 0.7302718560627612
|
| 37 |
-
},
|
| 38 |
-
"reference": {
|
| 39 |
-
"name": "utility_threshold_checkpoint_20000",
|
| 40 |
-
"cumulative_model_step": 20000,
|
| 41 |
-
"checkpoint": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/outputs/q42-prefill176-deadline-v1/main/checkpoint-20000",
|
| 42 |
-
"validation_manifest": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-prefill176-deadline-v1/crosslingual-trajectory-v1/checkpoint-20000/validation_manifest.json",
|
| 43 |
-
"validation_manifest_sha256": "4dbab9bdb25d17a4f4d0afbee401fb9d6909882f77b62c90e6c3e2814332abf2",
|
| 44 |
-
"scores": {
|
| 45 |
-
"Tatoeba": 0.5693134993779012,
|
| 46 |
-
"BUCC.v2": 0.9806018948964788,
|
| 47 |
-
"FloresBitextMining": 0.6307120567351537
|
| 48 |
-
},
|
| 49 |
-
"unweighted_task_macro": 0.7268758170031778
|
| 50 |
-
},
|
| 51 |
-
"prior_posthoc_checkpoint_2000": {
|
| 52 |
-
"name": "posthoc_lr1e4_checkpoint_2000",
|
| 53 |
-
"branch_step": 2000,
|
| 54 |
-
"cumulative_model_step": 22000,
|
| 55 |
-
"comparison": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-posthoc-lr1e4-5k-v1/crosslingual-evaluation-v1/comparison.json",
|
| 56 |
-
"comparison_sha256": "80befb8728458241b2fe8492ca1a27ff230db9815f1acfa10520b3e76d317f0b",
|
| 57 |
-
"scores": {
|
| 58 |
-
"Tatoeba": 0.5715131861557535,
|
| 59 |
-
"BUCC.v2": 0.9807117618693686,
|
| 60 |
-
"FloresBitextMining": 0.6352098331029489
|
| 61 |
-
},
|
| 62 |
-
"unweighted_task_macro": 0.7291449270426904
|
| 63 |
-
},
|
| 64 |
-
"treatment_minus_reference": {
|
| 65 |
-
"scores": {
|
| 66 |
-
"Tatoeba": 0.00330542139135781,
|
| 67 |
-
"BUCC.v2": 0.0005684487950872175,
|
| 68 |
-
"FloresBitextMining": 0.006314246992304873
|
| 69 |
-
},
|
| 70 |
-
"unweighted_task_macro": 0.003396039059583411
|
| 71 |
-
},
|
| 72 |
-
"treatment_minus_prior_posthoc_checkpoint_2000": {
|
| 73 |
-
"scores": {
|
| 74 |
-
"Tatoeba": 0.0011057346135054935,
|
| 75 |
-
"BUCC.v2": 0.0004585818221973792,
|
| 76 |
-
"FloresBitextMining": 0.0018164706245097362
|
| 77 |
-
},
|
| 78 |
-
"unweighted_task_macro": 0.0011269290200708326
|
| 79 |
-
},
|
| 80 |
-
"seed_count": 1,
|
| 81 |
-
"noise_floor_measured": false,
|
| 82 |
-
"effect_claim": false,
|
| 83 |
-
"interpretation": "descriptive post-hoc continuation diagnostic; deltas are not established effects"
|
| 84 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
evaluation/full-mmteb-summary.json
DELETED
|
@@ -1,233 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"protocol": "q42-posthoc-checkpoint5000-full-mmteb-summary-v1",
|
| 3 |
-
"model_label": "StaRSE checkpoint-25000",
|
| 4 |
-
"model": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/outputs/q42-posthoc-lr1e4-5k-v1/main/checkpoint-5000",
|
| 5 |
-
"model_sha256": {
|
| 6 |
-
"model.safetensors": "c28eaa242185aeccbd5ff643bb517b5a93d10b9d5a71c85dd03373ca4b2ad51a",
|
| 7 |
-
"tokenizer.json": "cc62b0c9d8e67411174ea331ff4aad01706b51b6d60ded1380dd4eba6f33c5cc",
|
| 8 |
-
"language_conditioning.json": "e9c6eab6df244674476b62abe637379972180d609094fd78f26eea13450208ab",
|
| 9 |
-
"run_config.json": "9746d6ad27420a4e132ed8b2ebe082b1cdef0daa03844b7b899014c74cdae062",
|
| 10 |
-
"trainer_state.json": "dda0190ab66e524802356b05d0126c8e757913f889db9fefd01d5fe6b36f90b7"
|
| 11 |
-
},
|
| 12 |
-
"training": {
|
| 13 |
-
"branch_steps": 5000,
|
| 14 |
-
"cumulative_model_steps": 25000,
|
| 15 |
-
"temperature": 0.05
|
| 16 |
-
},
|
| 17 |
-
"benchmark": "MTEB(Multilingual, v2)",
|
| 18 |
-
"mteb_version": "2.12.30",
|
| 19 |
-
"coverage": {
|
| 20 |
-
"completed": 131,
|
| 21 |
-
"total": 131,
|
| 22 |
-
"failed": 0
|
| 23 |
-
},
|
| 24 |
-
"learned_pair_rows": 56071,
|
| 25 |
-
"language_markers": 1914,
|
| 26 |
-
"training_population_languages": 176,
|
| 27 |
-
"mean_task": 0.5013418283946551,
|
| 28 |
-
"mean_task_type": 0.4344923589077889,
|
| 29 |
-
"task_types": {
|
| 30 |
-
"BitextMining": {
|
| 31 |
-
"tasks": 13,
|
| 32 |
-
"mean": 0.5390453021109137
|
| 33 |
-
},
|
| 34 |
-
"Classification": {
|
| 35 |
-
"tasks": 43,
|
| 36 |
-
"mean": 0.5287139212464116
|
| 37 |
-
},
|
| 38 |
-
"Clustering": {
|
| 39 |
-
"tasks": 16,
|
| 40 |
-
"mean": 0.3891149940448969
|
| 41 |
-
},
|
| 42 |
-
"InstructionReranking": {
|
| 43 |
-
"tasks": 3,
|
| 44 |
-
"mean": -0.010429037176081506
|
| 45 |
-
},
|
| 46 |
-
"MultilabelClassification": {
|
| 47 |
-
"tasks": 5,
|
| 48 |
-
"mean": 0.1764299351262767
|
| 49 |
-
},
|
| 50 |
-
"PairClassification": {
|
| 51 |
-
"tasks": 11,
|
| 52 |
-
"mean": 0.726940355614489
|
| 53 |
-
},
|
| 54 |
-
"Reranking": {
|
| 55 |
-
"tasks": 6,
|
| 56 |
-
"mean": 0.4942303326640782
|
| 57 |
-
},
|
| 58 |
-
"Retrieval": {
|
| 59 |
-
"tasks": 18,
|
| 60 |
-
"mean": 0.4164483887355264
|
| 61 |
-
},
|
| 62 |
-
"STS": {
|
| 63 |
-
"tasks": 16,
|
| 64 |
-
"mean": 0.6499370378035891
|
| 65 |
-
}
|
| 66 |
-
},
|
| 67 |
-
"source_artifacts": {
|
| 68 |
-
"registration": {
|
| 69 |
-
"path": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-posthoc-lr1e4-5k-v1/full-mmteb-registration-v1.json",
|
| 70 |
-
"sha256": "53c53da0cc66c30130585c5f86ae6d3848c2a12001b43197e7a86c26ca1d14cc"
|
| 71 |
-
},
|
| 72 |
-
"dataset_manifest": {
|
| 73 |
-
"path": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-prefill176-deadline-v1/appendix-evidence/mmteb_v2_datasets.json",
|
| 74 |
-
"sha256": "d47146f3f4c9e5ea632b94fb1ca1209d41e2a82dde4baabb5671eedb1005f8de"
|
| 75 |
-
},
|
| 76 |
-
"runner_summary": {
|
| 77 |
-
"path": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-posthoc-lr1e4-5k-v1/full-mmteb-v2-cpu/mteb_eval_summary.json",
|
| 78 |
-
"sha256": "a66f35005a615459c081a6c461ec4bc2e23152001859da511f5577aa4a268f83"
|
| 79 |
-
},
|
| 80 |
-
"runner_results": {
|
| 81 |
-
"path": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-posthoc-lr1e4-5k-v1/full-mmteb-v2-cpu/mteb_eval_results.json",
|
| 82 |
-
"sha256": "75004685970e100579bbe45ad9869e64c2be57aa3218609eb2148cfb788e889b"
|
| 83 |
-
},
|
| 84 |
-
"scores_csv": {
|
| 85 |
-
"path": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/artifacts/q42-posthoc-lr1e4-5k-v1/full-mmteb-v2-cpu/mteb_scores.csv",
|
| 86 |
-
"sha256": "161434cd20284bf7af364a336c1efe1c319031933aedc998c2177928a1c92010"
|
| 87 |
-
},
|
| 88 |
-
"raw_task_result_file_set_sha256": "a7405e4778b26688f969843c7d9b75c594d4dbf1e62f4677221c98777dabeca8",
|
| 89 |
-
"raw_task_result_files": {
|
| 90 |
-
"AILAStatutes.json": "7dafa30d58f3ed0368ffa518aef7755c3e311c0b8b4a3da91545a50d738cbca1",
|
| 91 |
-
"AfriSentiClassification.json": "bc5031b616ca2b3a8e20d82316444358bccdeeffb0415ae090b480b93ca9aa0d",
|
| 92 |
-
"AlloProfClusteringS2S.v2.json": "a2b9bcee455aabbef3093193ab8dc8459efad75b88bbb4f2b212228589ce51e1",
|
| 93 |
-
"AlloprofReranking.json": "5588c71e9c1bec3a1fe01e2418935783eeab1b90f7fe134c2f7311cc5924413e",
|
| 94 |
-
"AmazonCounterfactualClassification.json": "74405921b91f5fd10d89dab01e12f5bdefdb17ec8d9526a479c30386adbc55b7",
|
| 95 |
-
"ArXivHierarchicalClusteringP2P.json": "249aa877da57ceeed62617afbc8341b11bbae757ad7aa7162222b9ccbae60ebf",
|
| 96 |
-
"ArXivHierarchicalClusteringS2S.json": "96064bb696f139dd35712929810ab15378491611e554875f91df4dbe2d14360c",
|
| 97 |
-
"ArguAna.json": "b557c9166f4b96809f4fd6f3a62e4313a4b19b8e98027cce44b120ef2db4b0ea",
|
| 98 |
-
"ArmenianParaphrasePC.json": "0614417e34d3f52c5e9c3fef5034b064d1cd551b948b9aed5e7f262e2f94b127",
|
| 99 |
-
"BUCC.v2.json": "102532376ccfa19507983df560ed986ed73e33946d88eeace016c47ce0b352bd",
|
| 100 |
-
"BelebeleRetrieval.json": "4f9b6a6cf83fad40dd03ff4fb3a84e81eba34bc849b2e88893ffcb8910ffaa7b",
|
| 101 |
-
"BibleNLPBitextMining.json": "d238b30f57d89cb58b324a23bbb714fc9f5a25c672a37e8dcf72213d4b722f7b",
|
| 102 |
-
"BigPatentClustering.v2.json": "d3770d16282b95cf1bc588cca6a0a7f00b70b846f27c473fb682f1ba8005f645",
|
| 103 |
-
"BiorxivClusteringP2P.v2.json": "09c551ca222ddd9717b25bb10cfcbe8abfc26df56cd06668d55c30a6a1756a37",
|
| 104 |
-
"BornholmBitextMining.json": "19da9f1dd6d0de398e1cf11b98c7f3065f30e375c5e7526d737a157bdd186239",
|
| 105 |
-
"BrazilianToxicTweetsClassification.json": "37ea20ffb5ff39313d0bf679a8b8314ab84e6d0f7b2e4ce13a439703e22c99f1",
|
| 106 |
-
"BulgarianStoreReviewSentimentClassfication.json": "86703f8e9fe1b94247950a18e9e79b9f2d63c67880f2a1b29fc9883c3f2c6de3",
|
| 107 |
-
"CEDRClassification.json": "02abe01234c595b6ca97994faabde2c46dacf33e7345f97eba88886457adc268",
|
| 108 |
-
"CLSClusteringP2P.v2.json": "33ccf4fb1814af2fb2c90700806b8046b7a2993f9bcce8569a5ab5501998e115",
|
| 109 |
-
"CSFDSKMovieReviewSentimentClassification.json": "7b083dda187549e98035c7a0c039fa5fae907362fac987f501871af0351012a2",
|
| 110 |
-
"CTKFactsNLI.json": "78720e033c9ec70170a27f3cc2a2f4f0cc668a46ac45f989d3aa70cdd76a12e7",
|
| 111 |
-
"CataloniaTweetClassification.json": "d857818a7dec88ae462d611d8638cf8915f0560575e752880a61c1d9f7b04986",
|
| 112 |
-
"Core17InstructionRetrieval.json": "8e19e0e37a82337b5cd580df431da7e53b1f07bd1313830560032abc8beca3a2",
|
| 113 |
-
"CovidRetrieval.json": "4b00deeef34c97738eb577117e4edc21e8c4230e9129f5cbb4fe933ab3721b0a",
|
| 114 |
-
"CyrillicTurkicLangClassification.json": "5f8ec9eee2c3ea8480f9cc7ac20b209dad4902f502e9c66d343ccbb81ebd3b0a",
|
| 115 |
-
"CzechProductReviewSentimentClassification.json": "922e7aecda07543cc7d3fae2226808adce065fce33af993a712b7ae86f198d9d",
|
| 116 |
-
"DBpediaClassification.json": "040a9ca765c62347645f3d42ea56f59574fbf54c29388a11c87f844612432e78",
|
| 117 |
-
"DalajClassification.json": "3267d5b37af579473dc3530cae96a1445871dbf12021342240628d670f323a40",
|
| 118 |
-
"DiaBlaBitextMining.json": "4cbf5e44649a710a0a897b6fc76c8e2cbed696651250129ff7b79c038e420e11",
|
| 119 |
-
"EstonianValenceClassification.json": "d53c472289f2acd2fc95aed2344b58d8f5cc003866b252d780ce65415cd8df36",
|
| 120 |
-
"FaroeseSTS.json": "2824fefdc1c607b51cc32f396f1255ebc6543c89f3a70c829a9ebda5514fb404",
|
| 121 |
-
"FilipinoShopeeReviewsClassification.json": "499efb6af43e886feb0b9a66cf1a9bf21be29090dd2f9198d278a4c00712783b",
|
| 122 |
-
"FinParaSTS.json": "68f9db53d0929882bcb276047b7d5fbccbc1f201b448847b2033b9834356f700",
|
| 123 |
-
"FinancialPhrasebankClassification.json": "b2b91a3c8a5085b8c635d786e4d4bfcc7a1ce9e2148f4666f830b8326036ec25",
|
| 124 |
-
"FloresBitextMining.json": "8536ceefe37ea9ee10bd950a082251f25b0d5acf59a553ef2f10da8a4382fdb3",
|
| 125 |
-
"GermanSTSBenchmark.json": "e87a208d17d98261dc444bf7f0c25520badc7496a7825bea0803f6acbdb55274",
|
| 126 |
-
"GreekLegalCodeClassification.json": "c9fe45095907fada06183bc7dc2e725094ead4846773a4ba5e5414ad153bbd09",
|
| 127 |
-
"GujaratiNewsClassification.json": "3ad97d61a848f9f7d5415d8cc842963fe198a71b068387e8bdbf906b3dc52337",
|
| 128 |
-
"HALClusteringS2S.v2.json": "e654d70a53759f28b29fb1f2c678406f1d6c21a0a3f03113e2c5eacec368c8d0",
|
| 129 |
-
"HagridRetrieval.json": "5f799e03a94ae6ba8873e04075e75835706be2db14f1c3ab7a36e6000be5757e",
|
| 130 |
-
"IN22GenBitextMining.json": "53bdd2bbf0e7c9b5eeabd9d1af4aaaae7636cb36282ec0847df6bd0df5a449af",
|
| 131 |
-
"IndicCrosslingualSTS.json": "ef1bfe6f1355985d157e0b86598cf6b76af41fb58ebe1690ed108e9d1e2ce8c4",
|
| 132 |
-
"IndicGenBenchFloresBitextMining.json": "daeb6b64d2346df22b7a29552b652e76b061b85205d71a824f4796bf4da075dc",
|
| 133 |
-
"IndicLangClassification.json": "3b15a38241093f41df953993568c06e914a96b1df4a0daa55e4a970cfef08f43",
|
| 134 |
-
"IndonesianIdClickbaitClassification.json": "8b11a8d3dd9be724f466c8015bd9c120f15a0d825bb8ce81dff091f35de17dfa",
|
| 135 |
-
"IsiZuluNewsClassification.json": "e4dd09fe3a94f71e7bcec3d9f241ae99fd122701ea30d1ca257ba44b04ce03ed",
|
| 136 |
-
"ItaCaseholdClassification.json": "69b7886722d1453883f6a33df38eabce3caa49bef4fc44501b47ba01862001cb",
|
| 137 |
-
"JSICK.json": "850ef7f4b0f88390711b7f8239a70dcb28fa525e0fc5dd5cc601f1730b58b888",
|
| 138 |
-
"KorHateSpeechMLClassification.json": "470b10b9106457534e3356c17d60a64277dd1472da1bb525d260eace73536210",
|
| 139 |
-
"KorSarcasmClassification.json": "e4b1c062046511afac95dcfeb4c3ddebc3f971988ff81b10feac5ee680c98e67",
|
| 140 |
-
"KurdishSentimentClassification.json": "4bffb12469a039d4174f44e3160860b9fda248fe037791104ac3be766f39b8bc",
|
| 141 |
-
"LEMBPasskeyRetrieval.json": "42a42226a4aa9231f28f37f68d18a9796340f4b2ab38f102d204f564fe9edd7f",
|
| 142 |
-
"LegalBenchCorporateLobbying.json": "96a386e8d592344f5916332449eb82a31661086e2fda812f2dd37a1a89a284a8",
|
| 143 |
-
"MIRACLRetrievalHardNegatives.json": "94361b64d332f0de1733aeffc82f6bd80c8d8f99041871054e80441e8ad0bd53",
|
| 144 |
-
"MLQARetrieval.json": "e25f3703faac2767fbc2c0a06eb0e9bf024ec2e4ab56e57229b2e8a922c1d2d4",
|
| 145 |
-
"MacedonianTweetSentimentClassification.json": "d87064f6a4cf706bb9db4750d048c44fe3e6cc274ef67fc8a1390fd62b57f082",
|
| 146 |
-
"MalteseNewsClassification.json": "83e45ed57f3505096293958e5014049ad86d686fac7cfcb6d6d625b9f69beb05",
|
| 147 |
-
"MasakhaNEWSClassification.json": "db09c257323d01da3c8f16f0d96a441eca079f744560aaf6351135f5c9fe75fa",
|
| 148 |
-
"MasakhaNEWSClusteringS2S.json": "2790ef1f97d421bb6af0fdf5eb145c691a10d592b09a30bf2051a5a0b1b25edf",
|
| 149 |
-
"MassiveIntentClassification.json": "5beefb8d54e5375c8fbb6945ed97ae0981c4acc67178b7837b011b37c31063de",
|
| 150 |
-
"MedrxivClusteringP2P.v2.json": "4a16b4ead9bb9a1a97f75d39cb7e8a1131d1e0f2a3945ff8e580334c0f5723ac",
|
| 151 |
-
"MultiEURLEXMultilabelClassification.json": "275c9aa8a609992c0d12563fa7cac95e341b84e87119d46e2d3ec47042e46435",
|
| 152 |
-
"MultiHateClassification.json": "64490b685e7f089a0f3e647b3e0e2981de8ab0f13bfe1dd11723006d9dbe2564",
|
| 153 |
-
"NTREXBitextMining.json": "afd94174cf5831f19c16876f2ed1268cb72f4ad201f1bebcd356505d00500aef",
|
| 154 |
-
"NepaliNewsClassification.json": "0263eb7cc2a2fb7af60e217d3b9c3198e0cb0754b1a3358d99094d4ae9fa2f88",
|
| 155 |
-
"News21InstructionRetrieval.json": "aad1e025bcf7f47d0fd9ef8b3744033449ab15a296f0f412b1772d8102b9e14b",
|
| 156 |
-
"NollySentiBitextMining.json": "ff9ffda7c72609fb4e21a589dc5c7d1dab48df369f3667a1e67aa14b612aa8d2",
|
| 157 |
-
"NordicLangClassification.json": "d15f27a8e801465dfec2d8e2b6e6c49be5fb07fd288bdaf44840d679a8d749ca",
|
| 158 |
-
"NorwegianCourtsBitextMining.json": "dc1ce23abe74b9e15078e53715098df4de58de8c54cf65c5b6b159f06813a249",
|
| 159 |
-
"NusaParagraphEmotionClassification.json": "96115bf45b589b0a0d048959d785b7baec04ed67348ede4d23013f4443263b37",
|
| 160 |
-
"NusaTranslationBitextMining.json": "0c26ac2d42f1f1189b80df1cf82c80e4b4956183d09fba0be67b51037b4faab1",
|
| 161 |
-
"NusaX-senti.json": "408c1ba2279382474a24b5dfbfa2acec290246103c2c20f9c62f384daa1862ec",
|
| 162 |
-
"NusaXBitextMining.json": "e7f5725cdbde58e7c5af80f1be2be5dec60e4d40ce738fbe67dfa16c372afa05",
|
| 163 |
-
"OdiaNewsClassification.json": "64dc2a916fee7fe3b88e804324615575a42e635d83e71a18663b6a5629c56a76",
|
| 164 |
-
"OpusparcusPC.json": "7fb8ed55748455e782f230e13497557a108a3b49b9c6fc8824b7fdd6d79effb4",
|
| 165 |
-
"PAC.json": "0ccc6b232990e58c679cf03edc9790160b396e5a29a9e0f1983684c62b099f2d",
|
| 166 |
-
"PawsXPairClassification.json": "0b0e26015ebaa580593ffa909353ed74b775eeea5794323b73df7177a4d877df",
|
| 167 |
-
"PlscClusteringP2P.v2.json": "cf0a774980dfb146502186abc4ec484990e52657137d9868be212df2580c5395",
|
| 168 |
-
"PoemSentimentClassification.json": "e3235e67e34c83be1caf1a299884dfea911db53a8deadfe64f4abc6737f2c6e7",
|
| 169 |
-
"PolEmo2.0-OUT.json": "68a1ebb851f6afa3b7ef5706ef93e52a425dea7a256f403c3c7cc32f027c71c7",
|
| 170 |
-
"PpcPC.json": "fb85cf0dfb8c19c4efbcbf77187b34b6e79e4008203f64b27e682eaaf36df732",
|
| 171 |
-
"PunjabiNewsClassification.json": "64ef53c113650d3842158116148c6c79f57f2ef31280ca8690677a1b4cbbfe83",
|
| 172 |
-
"RTE3.json": "a8d53dc45a06ce1b6e833a6ec06a248233696f7a161531da10bb052aa7b890b9",
|
| 173 |
-
"Robust04InstructionRetrieval.json": "0911cab67bdbc1a17959895fc93a9bfb5a2267c8ee69435e1021625d2254ab04",
|
| 174 |
-
"RomaniBibleClustering.json": "ebde0b56270c9f8ff771573f62c343603841855ac9377a600b50f02d2c11f91f",
|
| 175 |
-
"RuBQReranking.json": "995e6bfccd625c11a3cf2c9cb946632116454fa851edc431258a037a296cefcd",
|
| 176 |
-
"SCIDOCS.json": "7efd61ed5d28a3386762eb5fe5a60e75115e07dab1dd740c87473f4220937534",
|
| 177 |
-
"SIB200ClusteringS2S.json": "460046c01522655a23cd1aa7e7f60cd433c9cd5a22a942899dcd6abdb1539f50",
|
| 178 |
-
"SICK-R.json": "128d93cc07ffa7d01519178d53a74f851524dad6b31644a0fdc369bb3ceff58e",
|
| 179 |
-
"STS12.json": "f36b6a0f2d74996c5433bccd33b87bf45d31699a37bdecf32a5746b3e9af0ea0",
|
| 180 |
-
"STS13.json": "959465dd80a39e7e956fd59282cfc43d56949d23f2e69fbd279d029c2933e7f7",
|
| 181 |
-
"STS14.json": "7d958733feac5646c656a4a559c276e0b39359dc147d51f6efda339df18505b8",
|
| 182 |
-
"STS15.json": "5dba0eef267d594a88e74d3812051394ce4b0cabcbf3e05b2a71944f3f751b0d",
|
| 183 |
-
"STS17.json": "56b75442df510c3dd95c2c0d80f48185d538142751c11b511cd0eb7764c5cdde",
|
| 184 |
-
"STS22.v2.json": "390fe6203f122648482f00f4433d44e5d13ba5e12cffb077bac3d6777004d551",
|
| 185 |
-
"STSB.json": "c76eb2dbcb18ee6b72e5f7bd2ee139df1344968b24c8bff1ffcd60de6de07d8b",
|
| 186 |
-
"STSBenchmark.json": "ac962843150a33651c9cdc8fa0d89f7ceaa68f1a29ba47069e6bece5cd634c90",
|
| 187 |
-
"STSES.json": "73b9a257fa4f3d25b98eb77a53aacaa9f0b647bd1074e060473ca6edd3231f92",
|
| 188 |
-
"ScalaClassification.json": "a1ad2a32f8368a4cf1a7a12a35b3f17e0afed6b41c8596f86cddbf8632cf35d8",
|
| 189 |
-
"SemRel24STS.json": "ca7042f55a2c0eef37e11a5713e637a1dd262b70ec9f9d5e9b19b31e73fd09e1",
|
| 190 |
-
"SentimentAnalysisHindi.json": "bdebcfd9f1c94b29f864601626372c74e7a5eafe509afcade6bfe0c42fd525fc",
|
| 191 |
-
"SinhalaNewsClassification.json": "204584e1b96e11209e598d26f4c46f3a770029b6b080c41ad37dd0e07cc769ba",
|
| 192 |
-
"SiswatiNewsClassification.json": "4e013bee9315094b07eb7a6b52ae0ed93d1a4fe40de5c2cea58b7803f0bb8d35",
|
| 193 |
-
"SlovakMovieReviewSentimentClassification.json": "a2fb8c449d0a88f0de3725c6672a4dcc643e0fc03d6c830db9c76806679ed81e",
|
| 194 |
-
"SpartQA.json": "52d3df6ae10029c8657c8c870b9ea54a58b44bf6eb5d65a52b013763771fa11b",
|
| 195 |
-
"SprintDuplicateQuestions.json": "c3b041abe86b3bd1a39d2c36b8c0d169249f7c318d4276834e55cb44eaf98c3b",
|
| 196 |
-
"StackExchangeClustering.v2.json": "cfeaa5697114e0b4ac134ef0a84f95e8eefcabd40f89863fba44550ee3382751",
|
| 197 |
-
"StackOverflowQA.json": "eecfcb7bd64206f9e14c728cb511af721ecb46bb74cc9d37e357e12d450925f9",
|
| 198 |
-
"StatcanDialogueDatasetRetrieval.json": "6ffe407eb5901b1faeb932bae55c841175204b0a67b08bc48e34c4ef66fd45fb",
|
| 199 |
-
"SwahiliNewsClassification.json": "38ba63b5c6dc11a8c3adf0493d987be78ae3ad4ccce95f47f6b137b4e762d35f",
|
| 200 |
-
"SwednClusteringP2P.json": "39ab45fb54cce3ce6300e9d9fca14df37f498c2af0ab33c82b110eb4bdc04e1c",
|
| 201 |
-
"SwissJudgementClassification.json": "66ce97d8f2c29d46aa28f16e9eb6cc6f952e523a81402aa6256c2ae04c284628",
|
| 202 |
-
"T2Reranking.json": "9203f6b8e164a985aca0ac19476c95bfeae5135a018b900a3bfc376c2acb6298",
|
| 203 |
-
"TERRa.json": "db14b4226b4a9b0f17df6c21ba86c64de327c18be8850e791a88b7b9b95bbb8d",
|
| 204 |
-
"TRECCOVID.json": "4250b5f03d4fe84323a27341c25552996f4154956c215d7b0c38ce2476a5a755",
|
| 205 |
-
"Tatoeba.json": "d5bce6cf854d726031d3bbc22ee78fbffed4e0440c125edf43c76eea19218cca",
|
| 206 |
-
"TempReasonL1.json": "9ca54c81a01f96ad6f545b87b0cb714846e076d3ea7cda0186838f2acbd699bf",
|
| 207 |
-
"ToxicConversationsClassification.json": "cc6bc216a73badd096218f99a02b88d3e1a4737c7970ff0dde480c4bd2efa153",
|
| 208 |
-
"TswanaNewsClassification.json": "eb21e4f9336478ef22f55de89d8fbccc3edabc3b7c52753fd1f4c61e9df24f83",
|
| 209 |
-
"TweetTopicSingleClassification.json": "a81445ac5ec3362221bd3e76feba9602281c65bbb368312f3b859ca56ac6bd04",
|
| 210 |
-
"TwitterHjerneRetrieval.json": "c318e7ab2d9ee84f09e9eb19826bb383853d01db393f8369e916a468a4e0fd16",
|
| 211 |
-
"TwitterURLCorpus.json": "d45e6db10586e294c7a5e8749952a65e771e91db186d84feac2d2adca66e4fcb",
|
| 212 |
-
"VoyageMMarcoReranking.json": "3c15d8f5632453dcedc1e3fc4cf352856d7bc6604539f5780bfd5ad8d3a86871",
|
| 213 |
-
"WebLINXCandidatesReranking.json": "e8654e79ae10709a1aa1a6a8b973f952b5293f0c250e34cda713a35cf9fea541",
|
| 214 |
-
"WikiCitiesClustering.json": "14eab07e3d64b0bbc0b964795417140c9b360861a56c71fa48974b3ff7861168",
|
| 215 |
-
"WikiClusteringP2P.v2.json": "333f12e64231d8dc0812592c55d1646151f9270bb54f48bfcf52d4d93aaa2a73",
|
| 216 |
-
"WikipediaRerankingMultilingual.json": "5abdc774ea3f62346ac2c8e401bd2ecc93639e6a7e2ad9f7178f71745c1d0a63",
|
| 217 |
-
"WikipediaRetrievalMultilingual.json": "46d49a2c40a3e69bc605c1920cb8f81767b93b4cbba5687a78d454e929bbf5db",
|
| 218 |
-
"WinoGrande.json": "ab2602995810601fc6c2f8b775a2e18702aaec3e64e88e26877dde2912fd98f8",
|
| 219 |
-
"XNLI.json": "205aa3d3251a018509925b9830b6cabac99329dfb803d4784a2138a7b743cbe3",
|
| 220 |
-
"indonli.json": "6611c7a76a8925ab8dbd86124367e3a4700b98f40c1ecfa9e59a7999440b29d6"
|
| 221 |
-
}
|
| 222 |
-
},
|
| 223 |
-
"claim_boundary": {
|
| 224 |
-
"seed_count": 1,
|
| 225 |
-
"noise_floor_measured": false,
|
| 226 |
-
"effect_claim_authorized": false,
|
| 227 |
-
"interpretation": "absolute author-confirmed final-checkpoint MMTEB result"
|
| 228 |
-
},
|
| 229 |
-
"summarizer": {
|
| 230 |
-
"path": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/scripts/multilingual_suite/summarize_q42_posthoc_full_mmteb.py",
|
| 231 |
-
"sha256": "8091ef841a15327a0c2cba4fa7c5718cab938a7f2cf210f51f4c2ed73a072d6f"
|
| 232 |
-
}
|
| 233 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
provenance/model-manifest.json
DELETED
|
@@ -1,77 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"source": "/home/boris/Projects/MY_SCIENCE_CARREER/TEXT_RELATED/STATIC_EMBEDDINGS/StaRSE/outputs/q42-posthoc-lr1e4-5k-v1/main/checkpoint-5000",
|
| 3 |
-
"technical_name": "starse-multilingual-q42-25k",
|
| 4 |
-
"model_sha256": "c28eaa242185aeccbd5ff643bb517b5a93d10b9d5a71c85dd03373ca4b2ad51a",
|
| 5 |
-
"optimizer_and_scheduler_included": false,
|
| 6 |
-
"reason": "minimal inference/research backup",
|
| 7 |
-
"backup_info_sha256": "ce6bc98da54e38ddcc3e8e453f2ef7a6cd58b5aed05aaf61ba863e0ac0f2a72c",
|
| 8 |
-
"files": {
|
| 9 |
-
"LICENSE": "47a7c816765f78a5069228d70dc8057ee25cd784cf078b718ac5377372554031",
|
| 10 |
-
"README.md": "c8d28a9f67279650dc6196acc3322a10a8a3d09109a35e22db3a9c223d76bdf1",
|
| 11 |
-
"collapse.py": "ae0d7eb7c01f701dd0b654c8de28fb0f03241118ea8fdb1b5c89829d96356b4c",
|
| 12 |
-
"conditioned_module.py": "28ea77093746fbbda595426302aa5cc8c6dd43a88703c677e3c9115cd9cbea40",
|
| 13 |
-
"config_sentence_transformers.json": "60c7241cedada0af5b9865fcd4a92ca4006d166921399a8cf8b609300f760365",
|
| 14 |
-
"evaluation/compact-multilingual-comparison.json": "f5e8758592417246d4a023efbf863e7f3479888c312f54f18272901bfcede45b",
|
| 15 |
-
"evaluation/full-mmteb-summary.json": "5b1fd9694c55960fc72bd6fd5c151b29a77eaccaefea0b62f045108fe4f90d79",
|
| 16 |
-
"language_conditioned.py": "970882f6ec8e16e378b038f949a58ca4f00b057d3023dccf10602332007f32af",
|
| 17 |
-
"language_conditioning.json": "e9c6eab6df244674476b62abe637379972180d609094fd78f26eea13450208ab",
|
| 18 |
-
"model.safetensors": "c28eaa242185aeccbd5ff643bb517b5a93d10b9d5a71c85dd03373ca4b2ad51a",
|
| 19 |
-
"modules.json": "b75ae92fd01578d6cd1f77739e61a9161e41a7766c8ace3b061ee50d22a926c9",
|
| 20 |
-
"ngram_table.py": "ee2610a63ff89968c14897b5a1c4124135368c7df8e261335668405d0bfa3a2f",
|
| 21 |
-
"online_tokenizer.py": "ae82b2fc8a4f54d0f8d0bbcda8fae7d416fb65b22ce115c4ee1c323e919e4dc5",
|
| 22 |
-
"recursive_cascade.py": "7c5a19d0fa324dc1a9b27ddf1aa4ec05447d41f00929d94adbd5d3d027269257",
|
| 23 |
-
"run_config.json": "9746d6ad27420a4e132ed8b2ebe082b1cdef0daa03844b7b899014c74cdae062",
|
| 24 |
-
"split_vocabulary.py": "cf5009564b3e67d7e5e3ea9114723e15685f519d74f407ed842cecc2b007dd9c",
|
| 25 |
-
"starse/__init__.py": "8651088b1c65223c61ed3c71f81e3323385f5c005315298d800db4a4fa5baae2",
|
| 26 |
-
"starse/binary_static_embedding.py": "f15fb15cbdc993b9c14bffb45f7649237ea677bc3fa9b7d86e3ac3a847fa2ae9",
|
| 27 |
-
"starse/cached_loss.py": "7558310e93ecf645ca73fbbec9d95d473ee988eee3fc28148bd10dc9c4681442",
|
| 28 |
-
"starse/checkpoint_precision.py": "7d9d1bf1922958c741b080911b72f4929367e0085a96f0e13c1a8e2b549f65d1",
|
| 29 |
-
"starse/collapse.py": "ae0d7eb7c01f701dd0b654c8de28fb0f03241118ea8fdb1b5c89829d96356b4c",
|
| 30 |
-
"starse/compiled_latent_encoder.py": "91340d973480f55746e91071d9009f5e1c9ab2c3030460244e1a1ccb3093dced",
|
| 31 |
-
"starse/conditioned_module.py": "12e9fee8551251b926ebe4861e2571b76a160592b35bd33fba1aee6a7bf27095",
|
| 32 |
-
"starse/gradient_safety.py": "cc9048effe8f65107f8774e3c8fee102dc699853e4b0a6e3df208d026116abb2",
|
| 33 |
-
"starse/grove_counterfactual.py": "b10a0748f937398d08c5253fe10b85e85a8504d286b5fcc8eb91c7c983f53fb7",
|
| 34 |
-
"starse/grove_selection.py": "80f96242911d332936d5eeef46963508c20cab61b0949f645fb659e5eda8c353",
|
| 35 |
-
"starse/init_model.py": "02109fade40ddfc45fb79e2468164c26c8f20036545d115d88e62a2c1c591b63",
|
| 36 |
-
"starse/language_conditioned.py": "970882f6ec8e16e378b038f949a58ca4f00b057d3023dccf10602332007f32af",
|
| 37 |
-
"starse/latent_compilation.py": "805144dcbac4007c1828d1ddf29e2fac26c05780cbf0db5a6e2807bf91050470",
|
| 38 |
-
"starse/latent_lexicon.py": "b4490f74da1a8b533ee95540954a6a7a0a0a7ff40c044b43691944a41c594fcb",
|
| 39 |
-
"starse/latent_profile.py": "b6d0852f4de817a3b8fc97895ee401165266f37b1976f17d9667c728639b17a2",
|
| 40 |
-
"starse/latent_profiling.py": "c8212756d82c8c9205a7ff7a1b020f72634bd1d35bf5cb441198e5236fb99acf",
|
| 41 |
-
"starse/latent_streaming.py": "b18c395aa7d8ee5aa667fe2897e40ae76d6b64e83b08518dd52b997c50bd1f21",
|
| 42 |
-
"starse/latent_training.py": "98991dd242d96df00268fe541259df9d0cf82516f57ed2f605432a30372bd6d0",
|
| 43 |
-
"starse/loss.py": "8fc1047f8c413551779855be91023ad09fc0480e182ee7e63decfe9d3802a0bd",
|
| 44 |
-
"starse/loss_path_vocabulary.py": "0b97959e90b27d517585603e1672679de491686d1a9b30d15230b9c5593b8406",
|
| 45 |
-
"starse/ngram_table.py": "ee2610a63ff89968c14897b5a1c4124135368c7df8e261335668405d0bfa3a2f",
|
| 46 |
-
"starse/online_tokenizer.py": "3ca1bc75bbd76b94c7854f07e7acff6bb792bfcaceb666f5fd4dfcb6703cb054",
|
| 47 |
-
"starse/online_tokenizer_compilation.py": "14c1f04afbc27fdfb5b1bac2548895b0d5681866264781a29c53e2f93bd53125",
|
| 48 |
-
"starse/openlex_compilation.py": "d9d51faecbaabb0f331fe0e41683b0b2eacd251f55c4304a5cb4f974c903615c",
|
| 49 |
-
"starse/openlex_dictionary.py": "204f634902667bb443d1cd31be28d516988e8af3bbd064650b52d2506f7c02a4",
|
| 50 |
-
"starse/openlex_lattice.py": "91bc9005d9f1cf8bf0402178d41ec7a063de9c429a229cb2c00a6766ffe07bad",
|
| 51 |
-
"starse/openlex_model.py": "e2dd952ba0ec1aa0056fa06c80ab5a0dcf40846992a06f10d486cd4f12be9bbe",
|
| 52 |
-
"starse/openlex_replay.py": "b8a1d224014d11d079ec62ed6dce78e6a7807aece69b184b3b75b33d9983d919",
|
| 53 |
-
"starse/openlex_replay_checkpoint.py": "3dd1d54694a4b0192624a4352a7e86c166966425b965e819bbfdba7c01e851e7",
|
| 54 |
-
"starse/openlex_reuse.py": "42b9f2f75da839f7bf743fcadce85ad3c05082c9e56e95b8cd56e0240c930c4b",
|
| 55 |
-
"starse/openlex_spans.py": "773e44cc95b48fb58ce593daa6259199a6fbf3cf42dfa3038cacb92088e5b1b2",
|
| 56 |
-
"starse/openlex_tokenizer.py": "bf90eb2c8d9853f28417664dd4b187f17c78c339c708c81b891ecb5295c579a7",
|
| 57 |
-
"starse/progressive_model.py": "3e31f7e46c7b834ea6512159575846e9d8fc9c8475a18ee6921865b9bf19d070",
|
| 58 |
-
"starse/project.py": "4df9c5903f5856da30ea543ac6de81664288f1dc75a7322fbfe31117b9aff02a",
|
| 59 |
-
"starse/recursive_cascade.py": "ec7c79d48493a273787138a4d97d97544d18aa13a1226a29ea5eea8963f82e2a",
|
| 60 |
-
"starse/split_vocabulary.py": "cf5009564b3e67d7e5e3ea9114723e15685f519d74f407ed842cecc2b007dd9c",
|
| 61 |
-
"starse/streaming.py": "e529e331ac9c5689971b20c19bb450a728851445bd485887fb680c0a2e0bb126",
|
| 62 |
-
"starse/structured_tokenizer.py": "d234431d01e77fa5ba5017836ce451b7be30dcba7661152b4eaeee9c6e2a9e19",
|
| 63 |
-
"starse/train.py": "0e027b066d726242ee4ff561370625a5dc4147d3ee74a62863b7dcfb2609428b",
|
| 64 |
-
"starse/train_multilingual.py": "e2c1585ec2686b749999a101a01d315de9bac4c82a48005ca1d730cc08bd9c3d",
|
| 65 |
-
"starse/train_progressive.py": "edd6bbf1b9c46905e4d58fbb90c0946117a20ace3abc7f8a4f9d34b4b2c90cf5",
|
| 66 |
-
"starse/translation_edit.py": "b6ca08fabcb1d7b0ab9e6671cde2c8917f0a389a7fd9f2e071212ed1b4a6c3d7",
|
| 67 |
-
"starse/vocab_assignment.py": "9edab22d17eed85b7d4975996e676f4c4bb56e6309301830e527389036ba3481",
|
| 68 |
-
"starse/vocabulary_growth.py": "76f3ab197e4cf0a5d830978957726f1cfafafec5b1bc85d6fbdf81a566467330",
|
| 69 |
-
"starse/vocabulary_probe.py": "9d0abc37750a92a8ae60b3c261580ec05a58b5b8d511abf9da4de6e51f69746d",
|
| 70 |
-
"starse/vocabulary_telemetry.py": "dc5fbcaa33e375f6242a34d6b4bbe79b1ff7bb16d9377d03f2c217e9c10e5b58",
|
| 71 |
-
"starse/weighted_schedule.py": "1e8bff3ba267dfeb73c6fe3faf7bca34d9a0d60b0229dda07161e5d9c70c875b",
|
| 72 |
-
"structured_tokenizer.py": "d234431d01e77fa5ba5017836ce451b7be30dcba7661152b4eaeee9c6e2a9e19",
|
| 73 |
-
"tokenizer.json": "cc62b0c9d8e67411174ea331ff4aad01706b51b6d60ded1380dd4eba6f33c5cc",
|
| 74 |
-
"trainer_state.json": "dda0190ab66e524802356b05d0126c8e757913f889db9fefd01d5fe6b36f90b7",
|
| 75 |
-
"vocab_assignment.py": "9edab22d17eed85b7d4975996e676f4c4bb56e6309301830e527389036ba3481"
|
| 76 |
-
}
|
| 77 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
run_config.json
DELETED
|
@@ -1,753 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"run_name": "q42-posthoc-lr1e4-5k-v1",
|
| 3 |
-
"seed": 20260805,
|
| 4 |
-
"streams": [
|
| 5 |
-
{
|
| 6 |
-
"name": "ccmatrix",
|
| 7 |
-
"weight": 1.0,
|
| 8 |
-
"anchor_column": "english",
|
| 9 |
-
"positive_column": "non_english",
|
| 10 |
-
"parquet_files": 1278,
|
| 11 |
-
"language_sampling": {
|
| 12 |
-
"path_component": 0,
|
| 13 |
-
"temperature": 1.0,
|
| 14 |
-
"include_languages": [
|
| 15 |
-
"en-af",
|
| 16 |
-
"en-ar",
|
| 17 |
-
"en-ast",
|
| 18 |
-
"en-az",
|
| 19 |
-
"en-bg",
|
| 20 |
-
"en-bn",
|
| 21 |
-
"en-br",
|
| 22 |
-
"en-ca",
|
| 23 |
-
"en-cs",
|
| 24 |
-
"en-da",
|
| 25 |
-
"en-de",
|
| 26 |
-
"en-el",
|
| 27 |
-
"en-eo",
|
| 28 |
-
"en-es",
|
| 29 |
-
"en-eu",
|
| 30 |
-
"en-fa",
|
| 31 |
-
"en-fi",
|
| 32 |
-
"en-fr",
|
| 33 |
-
"en-fy",
|
| 34 |
-
"en-gl",
|
| 35 |
-
"en-he",
|
| 36 |
-
"en-hi",
|
| 37 |
-
"en-hr",
|
| 38 |
-
"en-hu",
|
| 39 |
-
"en-id",
|
| 40 |
-
"en-ilo",
|
| 41 |
-
"en-is",
|
| 42 |
-
"en-it",
|
| 43 |
-
"en-ja",
|
| 44 |
-
"en-jv",
|
| 45 |
-
"en-ko",
|
| 46 |
-
"en-la",
|
| 47 |
-
"en-lt",
|
| 48 |
-
"en-lv",
|
| 49 |
-
"en-mg",
|
| 50 |
-
"en-mk",
|
| 51 |
-
"en-ml",
|
| 52 |
-
"en-mr",
|
| 53 |
-
"en-ms",
|
| 54 |
-
"en-ne",
|
| 55 |
-
"en-nl",
|
| 56 |
-
"en-no",
|
| 57 |
-
"en-oc",
|
| 58 |
-
"en-or",
|
| 59 |
-
"en-pl",
|
| 60 |
-
"en-pt",
|
| 61 |
-
"en-ro",
|
| 62 |
-
"en-ru",
|
| 63 |
-
"en-sd",
|
| 64 |
-
"en-si",
|
| 65 |
-
"en-sk",
|
| 66 |
-
"en-sl",
|
| 67 |
-
"en-so",
|
| 68 |
-
"en-sq",
|
| 69 |
-
"en-sr",
|
| 70 |
-
"en-su",
|
| 71 |
-
"en-sv",
|
| 72 |
-
"en-sw",
|
| 73 |
-
"en-ta",
|
| 74 |
-
"en-tl",
|
| 75 |
-
"en-tr",
|
| 76 |
-
"en-uk",
|
| 77 |
-
"en-ur",
|
| 78 |
-
"en-vi",
|
| 79 |
-
"en-yi",
|
| 80 |
-
"en-zh"
|
| 81 |
-
]
|
| 82 |
-
}
|
| 83 |
-
},
|
| 84 |
-
{
|
| 85 |
-
"name": "fineweb-en",
|
| 86 |
-
"weight": 1.0,
|
| 87 |
-
"anchor_column": "anchor",
|
| 88 |
-
"positive_column": "positive",
|
| 89 |
-
"parquet_files": 40,
|
| 90 |
-
"language_sampling": {
|
| 91 |
-
"path_component": 0,
|
| 92 |
-
"temperature": 1.0,
|
| 93 |
-
"include_languages": [
|
| 94 |
-
"eng_Latn"
|
| 95 |
-
]
|
| 96 |
-
}
|
| 97 |
-
},
|
| 98 |
-
{
|
| 99 |
-
"name": "fineweb2",
|
| 100 |
-
"weight": 1.0,
|
| 101 |
-
"anchor_column": "sentence_1",
|
| 102 |
-
"positive_column": "sentence_2",
|
| 103 |
-
"parquet_files": 328,
|
| 104 |
-
"language_sampling": {
|
| 105 |
-
"path_component": 0,
|
| 106 |
-
"temperature": 1.0,
|
| 107 |
-
"include_languages": [
|
| 108 |
-
"aeb_Arab",
|
| 109 |
-
"afr_Latn",
|
| 110 |
-
"als_Latn",
|
| 111 |
-
"amh_Ethi",
|
| 112 |
-
"apc_Arab",
|
| 113 |
-
"arb_Arab",
|
| 114 |
-
"ars_Arab",
|
| 115 |
-
"ary_Arab",
|
| 116 |
-
"arz_Arab",
|
| 117 |
-
"asm_Beng",
|
| 118 |
-
"ast_Latn",
|
| 119 |
-
"azb_Arab",
|
| 120 |
-
"azj_Latn",
|
| 121 |
-
"bak_Cyrl",
|
| 122 |
-
"ban_Latn",
|
| 123 |
-
"bbc_Latn",
|
| 124 |
-
"bel_Cyrl",
|
| 125 |
-
"ben_Beng",
|
| 126 |
-
"ben_Latn",
|
| 127 |
-
"bew_Latn",
|
| 128 |
-
"bho_Deva",
|
| 129 |
-
"bod_Tibt",
|
| 130 |
-
"bos_Latn",
|
| 131 |
-
"bre_Latn",
|
| 132 |
-
"bul_Cyrl",
|
| 133 |
-
"cat_Latn",
|
| 134 |
-
"ceb_Latn",
|
| 135 |
-
"ces_Latn",
|
| 136 |
-
"chv_Cyrl",
|
| 137 |
-
"ckb_Arab",
|
| 138 |
-
"cmn_Hani",
|
| 139 |
-
"crh_Latn",
|
| 140 |
-
"csb_Latn",
|
| 141 |
-
"cym_Latn",
|
| 142 |
-
"dan_Latn",
|
| 143 |
-
"deu_Latn",
|
| 144 |
-
"div_Thaa",
|
| 145 |
-
"ell_Grek",
|
| 146 |
-
"eng_Latn",
|
| 147 |
-
"epo_Latn",
|
| 148 |
-
"eus_Latn",
|
| 149 |
-
"ewe_Latn",
|
| 150 |
-
"fao_Latn",
|
| 151 |
-
"fas_Arab",
|
| 152 |
-
"fij_Latn",
|
| 153 |
-
"fil_Latn",
|
| 154 |
-
"fin_Latn",
|
| 155 |
-
"fra_Latn",
|
| 156 |
-
"fry_Latn",
|
| 157 |
-
"fur_Latn",
|
| 158 |
-
"gaz_Latn",
|
| 159 |
-
"gla_Latn",
|
| 160 |
-
"gle_Latn",
|
| 161 |
-
"glg_Latn",
|
| 162 |
-
"glk_Arab",
|
| 163 |
-
"grc_Grek",
|
| 164 |
-
"gsw_Latn",
|
| 165 |
-
"guj_Gujr",
|
| 166 |
-
"hat_Latn",
|
| 167 |
-
"hau_Latn",
|
| 168 |
-
"haw_Latn",
|
| 169 |
-
"hbo_Hebr",
|
| 170 |
-
"heb_Hebr",
|
| 171 |
-
"hin_Deva",
|
| 172 |
-
"hin_Latn",
|
| 173 |
-
"hrv_Latn",
|
| 174 |
-
"hsb_Latn",
|
| 175 |
-
"hun_Latn",
|
| 176 |
-
"hye_Armn",
|
| 177 |
-
"ibo_Latn",
|
| 178 |
-
"ilo_Latn",
|
| 179 |
-
"ind_Latn",
|
| 180 |
-
"isl_Latn",
|
| 181 |
-
"ita_Latn",
|
| 182 |
-
"jav_Latn",
|
| 183 |
-
"jpn_Jpan",
|
| 184 |
-
"kab_Latn",
|
| 185 |
-
"kan_Knda",
|
| 186 |
-
"kat_Geor",
|
| 187 |
-
"kaz_Cyrl",
|
| 188 |
-
"khk_Cyrl",
|
| 189 |
-
"khm_Khmr",
|
| 190 |
-
"kin_Latn",
|
| 191 |
-
"kir_Cyrl",
|
| 192 |
-
"kmr_Latn",
|
| 193 |
-
"kor_Hang",
|
| 194 |
-
"krc_Cyrl",
|
| 195 |
-
"lao_Laoo",
|
| 196 |
-
"lat_Latn",
|
| 197 |
-
"lij_Latn",
|
| 198 |
-
"lim_Latn",
|
| 199 |
-
"lin_Latn",
|
| 200 |
-
"lit_Latn",
|
| 201 |
-
"lmo_Latn",
|
| 202 |
-
"ltg_Latn",
|
| 203 |
-
"ltz_Latn",
|
| 204 |
-
"lug_Latn",
|
| 205 |
-
"lus_Latn",
|
| 206 |
-
"lvs_Latn",
|
| 207 |
-
"mai_Deva",
|
| 208 |
-
"mal_Mlym",
|
| 209 |
-
"mar_Deva",
|
| 210 |
-
"mhr_Cyrl",
|
| 211 |
-
"mkd_Cyrl",
|
| 212 |
-
"mlt_Latn",
|
| 213 |
-
"mri_Latn",
|
| 214 |
-
"mya_Mymr",
|
| 215 |
-
"nde_Latn",
|
| 216 |
-
"nds_Latn",
|
| 217 |
-
"nld_Latn",
|
| 218 |
-
"nno_Latn",
|
| 219 |
-
"nob_Latn",
|
| 220 |
-
"npi_Deva",
|
| 221 |
-
"nya_Latn",
|
| 222 |
-
"oci_Latn",
|
| 223 |
-
"ory_Orya",
|
| 224 |
-
"pan_Guru",
|
| 225 |
-
"pap_Latn",
|
| 226 |
-
"pbt_Arab",
|
| 227 |
-
"plt_Latn",
|
| 228 |
-
"pms_Latn",
|
| 229 |
-
"pol_Latn",
|
| 230 |
-
"por_Latn",
|
| 231 |
-
"rmy_Latn",
|
| 232 |
-
"ron_Latn",
|
| 233 |
-
"run_Latn",
|
| 234 |
-
"rus_Cyrl",
|
| 235 |
-
"sah_Cyrl",
|
| 236 |
-
"san_Deva",
|
| 237 |
-
"san_Latn",
|
| 238 |
-
"scn_Latn",
|
| 239 |
-
"sco_Latn",
|
| 240 |
-
"shn_Mymr",
|
| 241 |
-
"sin_Sinh",
|
| 242 |
-
"slk_Latn",
|
| 243 |
-
"slv_Latn",
|
| 244 |
-
"smo_Latn",
|
| 245 |
-
"sna_Latn",
|
| 246 |
-
"snd_Arab",
|
| 247 |
-
"som_Latn",
|
| 248 |
-
"sot_Latn",
|
| 249 |
-
"spa_Latn",
|
| 250 |
-
"srd_Latn",
|
| 251 |
-
"srp_Cyrl",
|
| 252 |
-
"srp_Latn",
|
| 253 |
-
"sun_Latn",
|
| 254 |
-
"swe_Latn",
|
| 255 |
-
"swh_Latn",
|
| 256 |
-
"tam_Latn",
|
| 257 |
-
"tam_Taml",
|
| 258 |
-
"tat_Cyrl",
|
| 259 |
-
"tat_Latn",
|
| 260 |
-
"tel_Latn",
|
| 261 |
-
"tel_Telu",
|
| 262 |
-
"tgk_Cyrl",
|
| 263 |
-
"tha_Thai",
|
| 264 |
-
"tir_Ethi",
|
| 265 |
-
"tuk_Latn",
|
| 266 |
-
"tur_Latn",
|
| 267 |
-
"tyv_Cyrl",
|
| 268 |
-
"uig_Arab",
|
| 269 |
-
"uig_Cyrl",
|
| 270 |
-
"uig_Latn",
|
| 271 |
-
"ukr_Cyrl",
|
| 272 |
-
"urd_Arab",
|
| 273 |
-
"urd_Latn",
|
| 274 |
-
"uzn_Cyrl",
|
| 275 |
-
"uzn_Latn",
|
| 276 |
-
"vie_Latn",
|
| 277 |
-
"wln_Latn",
|
| 278 |
-
"xho_Latn",
|
| 279 |
-
"ydd_Hebr",
|
| 280 |
-
"yor_Latn",
|
| 281 |
-
"yue_Hani",
|
| 282 |
-
"zsm_Latn",
|
| 283 |
-
"zul_Latn"
|
| 284 |
-
]
|
| 285 |
-
}
|
| 286 |
-
},
|
| 287 |
-
{
|
| 288 |
-
"name": "mrl-all-nli",
|
| 289 |
-
"weight": 1.0,
|
| 290 |
-
"anchor_column": "anchor",
|
| 291 |
-
"positive_column": "positive",
|
| 292 |
-
"parquet_files": 6,
|
| 293 |
-
"language_sampling": {
|
| 294 |
-
"path_component": 0,
|
| 295 |
-
"temperature": 1.0,
|
| 296 |
-
"include_languages": [
|
| 297 |
-
"eng_Latn"
|
| 298 |
-
]
|
| 299 |
-
}
|
| 300 |
-
},
|
| 301 |
-
{
|
| 302 |
-
"name": "mrl-altlex",
|
| 303 |
-
"weight": 1.0,
|
| 304 |
-
"anchor_column": "anchor",
|
| 305 |
-
"positive_column": "positive",
|
| 306 |
-
"parquet_files": 2,
|
| 307 |
-
"language_sampling": {
|
| 308 |
-
"path_component": 0,
|
| 309 |
-
"temperature": 1.0,
|
| 310 |
-
"include_languages": [
|
| 311 |
-
"eng_Latn"
|
| 312 |
-
]
|
| 313 |
-
}
|
| 314 |
-
},
|
| 315 |
-
{
|
| 316 |
-
"name": "mrl-coco",
|
| 317 |
-
"weight": 1.0,
|
| 318 |
-
"anchor_column": "anchor",
|
| 319 |
-
"positive_column": "positive",
|
| 320 |
-
"parquet_files": 5,
|
| 321 |
-
"language_sampling": {
|
| 322 |
-
"path_component": 0,
|
| 323 |
-
"temperature": 1.0,
|
| 324 |
-
"include_languages": [
|
| 325 |
-
"eng_Latn"
|
| 326 |
-
]
|
| 327 |
-
}
|
| 328 |
-
},
|
| 329 |
-
{
|
| 330 |
-
"name": "mrl-europarl",
|
| 331 |
-
"weight": 1.0,
|
| 332 |
-
"anchor_column": "anchor",
|
| 333 |
-
"positive_column": "positive",
|
| 334 |
-
"parquet_files": 50,
|
| 335 |
-
"language_sampling": {
|
| 336 |
-
"path_component": 0,
|
| 337 |
-
"temperature": 1.0,
|
| 338 |
-
"include_languages": [
|
| 339 |
-
"bul_Cyrl",
|
| 340 |
-
"ces_Latn",
|
| 341 |
-
"dan_Latn",
|
| 342 |
-
"deu_Latn",
|
| 343 |
-
"nob_Latn"
|
| 344 |
-
]
|
| 345 |
-
}
|
| 346 |
-
},
|
| 347 |
-
{
|
| 348 |
-
"name": "mrl-flickr30k",
|
| 349 |
-
"weight": 1.0,
|
| 350 |
-
"anchor_column": "anchor",
|
| 351 |
-
"positive_column": "positive",
|
| 352 |
-
"parquet_files": 2,
|
| 353 |
-
"language_sampling": {
|
| 354 |
-
"path_component": 0,
|
| 355 |
-
"temperature": 1.0,
|
| 356 |
-
"include_languages": [
|
| 357 |
-
"eng_Latn"
|
| 358 |
-
]
|
| 359 |
-
}
|
| 360 |
-
},
|
| 361 |
-
{
|
| 362 |
-
"name": "mrl-global-voices",
|
| 363 |
-
"weight": 1.0,
|
| 364 |
-
"anchor_column": "anchor",
|
| 365 |
-
"positive_column": "positive",
|
| 366 |
-
"parquet_files": 14,
|
| 367 |
-
"language_sampling": {
|
| 368 |
-
"path_component": 0,
|
| 369 |
-
"temperature": 1.0,
|
| 370 |
-
"include_languages": [
|
| 371 |
-
"arb_Arab",
|
| 372 |
-
"deu_Latn",
|
| 373 |
-
"ell_Grek",
|
| 374 |
-
"fra_Latn",
|
| 375 |
-
"ita_Latn",
|
| 376 |
-
"mkd_Cyrl",
|
| 377 |
-
"nld_Latn",
|
| 378 |
-
"pol_Latn",
|
| 379 |
-
"por_Latn",
|
| 380 |
-
"rus_Cyrl",
|
| 381 |
-
"spa_Latn"
|
| 382 |
-
]
|
| 383 |
-
}
|
| 384 |
-
},
|
| 385 |
-
{
|
| 386 |
-
"name": "mrl-muse",
|
| 387 |
-
"weight": 1.0,
|
| 388 |
-
"anchor_column": "anchor",
|
| 389 |
-
"positive_column": "positive",
|
| 390 |
-
"parquet_files": 8,
|
| 391 |
-
"language_sampling": {
|
| 392 |
-
"path_component": 0,
|
| 393 |
-
"temperature": 1.0,
|
| 394 |
-
"include_languages": [
|
| 395 |
-
"arb_Arab",
|
| 396 |
-
"deu_Latn",
|
| 397 |
-
"ell_Grek",
|
| 398 |
-
"fas_Arab",
|
| 399 |
-
"heb_Hebr",
|
| 400 |
-
"jpn_Jpan",
|
| 401 |
-
"rus_Cyrl",
|
| 402 |
-
"ukr_Cyrl"
|
| 403 |
-
]
|
| 404 |
-
}
|
| 405 |
-
},
|
| 406 |
-
{
|
| 407 |
-
"name": "mrl-nli-simcse",
|
| 408 |
-
"weight": 1.0,
|
| 409 |
-
"anchor_column": "anchor",
|
| 410 |
-
"positive_column": "positive",
|
| 411 |
-
"parquet_files": 3,
|
| 412 |
-
"language_sampling": {
|
| 413 |
-
"path_component": 0,
|
| 414 |
-
"temperature": 1.0,
|
| 415 |
-
"include_languages": [
|
| 416 |
-
"eng_Latn"
|
| 417 |
-
]
|
| 418 |
-
}
|
| 419 |
-
},
|
| 420 |
-
{
|
| 421 |
-
"name": "mrl-opensubtitles",
|
| 422 |
-
"weight": 1.0,
|
| 423 |
-
"anchor_column": "anchor",
|
| 424 |
-
"positive_column": "positive",
|
| 425 |
-
"parquet_files": 47,
|
| 426 |
-
"language_sampling": {
|
| 427 |
-
"path_component": 0,
|
| 428 |
-
"temperature": 1.0,
|
| 429 |
-
"include_languages": [
|
| 430 |
-
"arb_Arab",
|
| 431 |
-
"fas_Arab"
|
| 432 |
-
]
|
| 433 |
-
}
|
| 434 |
-
},
|
| 435 |
-
{
|
| 436 |
-
"name": "mrl-quora",
|
| 437 |
-
"weight": 1.0,
|
| 438 |
-
"anchor_column": "anchor",
|
| 439 |
-
"positive_column": "positive",
|
| 440 |
-
"parquet_files": 2,
|
| 441 |
-
"language_sampling": {
|
| 442 |
-
"path_component": 0,
|
| 443 |
-
"temperature": 1.0,
|
| 444 |
-
"include_languages": [
|
| 445 |
-
"eng_Latn"
|
| 446 |
-
]
|
| 447 |
-
}
|
| 448 |
-
},
|
| 449 |
-
{
|
| 450 |
-
"name": "mrl-simple-wiki",
|
| 451 |
-
"weight": 1.0,
|
| 452 |
-
"anchor_column": "anchor",
|
| 453 |
-
"positive_column": "positive",
|
| 454 |
-
"parquet_files": 2,
|
| 455 |
-
"language_sampling": {
|
| 456 |
-
"path_component": 0,
|
| 457 |
-
"temperature": 1.0,
|
| 458 |
-
"include_languages": [
|
| 459 |
-
"eng_Latn"
|
| 460 |
-
]
|
| 461 |
-
}
|
| 462 |
-
},
|
| 463 |
-
{
|
| 464 |
-
"name": "mrl-stackexchange",
|
| 465 |
-
"weight": 1.0,
|
| 466 |
-
"anchor_column": "anchor",
|
| 467 |
-
"positive_column": "positive",
|
| 468 |
-
"parquet_files": 1,
|
| 469 |
-
"language_sampling": {
|
| 470 |
-
"path_component": 0,
|
| 471 |
-
"temperature": 1.0,
|
| 472 |
-
"include_languages": [
|
| 473 |
-
"eng_Latn"
|
| 474 |
-
]
|
| 475 |
-
}
|
| 476 |
-
},
|
| 477 |
-
{
|
| 478 |
-
"name": "mrl-talks",
|
| 479 |
-
"weight": 1.0,
|
| 480 |
-
"anchor_column": "anchor",
|
| 481 |
-
"positive_column": "positive",
|
| 482 |
-
"parquet_files": 114,
|
| 483 |
-
"language_sampling": {
|
| 484 |
-
"path_component": 0,
|
| 485 |
-
"temperature": 1.0,
|
| 486 |
-
"include_languages": [
|
| 487 |
-
"als_Latn",
|
| 488 |
-
"arb_Arab",
|
| 489 |
-
"bul_Cyrl",
|
| 490 |
-
"cat_Latn",
|
| 491 |
-
"ces_Latn",
|
| 492 |
-
"cmn_Hani",
|
| 493 |
-
"dan_Latn",
|
| 494 |
-
"deu_Latn",
|
| 495 |
-
"ell_Grek",
|
| 496 |
-
"fas_Arab",
|
| 497 |
-
"fin_Latn",
|
| 498 |
-
"fra_Latn",
|
| 499 |
-
"glg_Latn",
|
| 500 |
-
"heb_Hebr",
|
| 501 |
-
"hin_Deva",
|
| 502 |
-
"hrv_Latn",
|
| 503 |
-
"hun_Latn",
|
| 504 |
-
"hye_Armn",
|
| 505 |
-
"ind_Latn",
|
| 506 |
-
"ita_Latn",
|
| 507 |
-
"jpn_Jpan",
|
| 508 |
-
"kat_Geor",
|
| 509 |
-
"khk_Cyrl",
|
| 510 |
-
"kor_Hang",
|
| 511 |
-
"lit_Latn",
|
| 512 |
-
"lvs_Latn",
|
| 513 |
-
"mar_Deva",
|
| 514 |
-
"mkd_Cyrl",
|
| 515 |
-
"mya_Mymr",
|
| 516 |
-
"nld_Latn",
|
| 517 |
-
"nob_Latn",
|
| 518 |
-
"pol_Latn",
|
| 519 |
-
"por_Latn",
|
| 520 |
-
"ron_Latn",
|
| 521 |
-
"rus_Cyrl",
|
| 522 |
-
"slk_Latn",
|
| 523 |
-
"slv_Latn",
|
| 524 |
-
"spa_Latn",
|
| 525 |
-
"srp_Cyrl",
|
| 526 |
-
"swe_Latn",
|
| 527 |
-
"tha_Thai",
|
| 528 |
-
"tur_Latn",
|
| 529 |
-
"ukr_Cyrl",
|
| 530 |
-
"vie_Latn"
|
| 531 |
-
]
|
| 532 |
-
}
|
| 533 |
-
},
|
| 534 |
-
{
|
| 535 |
-
"name": "mrl-wikianswers",
|
| 536 |
-
"weight": 1.0,
|
| 537 |
-
"anchor_column": "anchor",
|
| 538 |
-
"positive_column": "positive",
|
| 539 |
-
"parquet_files": 100,
|
| 540 |
-
"language_sampling": {
|
| 541 |
-
"path_component": 0,
|
| 542 |
-
"temperature": 1.0,
|
| 543 |
-
"include_languages": [
|
| 544 |
-
"eng_Latn"
|
| 545 |
-
]
|
| 546 |
-
}
|
| 547 |
-
},
|
| 548 |
-
{
|
| 549 |
-
"name": "mrl-wikimatrix",
|
| 550 |
-
"weight": 1.0,
|
| 551 |
-
"anchor_column": "anchor",
|
| 552 |
-
"positive_column": "positive",
|
| 553 |
-
"parquet_files": 112,
|
| 554 |
-
"language_sampling": {
|
| 555 |
-
"path_component": 0,
|
| 556 |
-
"temperature": 1.0,
|
| 557 |
-
"include_languages": [
|
| 558 |
-
"als_Latn",
|
| 559 |
-
"arb_Arab",
|
| 560 |
-
"bul_Cyrl",
|
| 561 |
-
"cat_Latn",
|
| 562 |
-
"ces_Latn",
|
| 563 |
-
"cmn_Hani",
|
| 564 |
-
"dan_Latn",
|
| 565 |
-
"deu_Latn",
|
| 566 |
-
"ell_Grek",
|
| 567 |
-
"fas_Arab",
|
| 568 |
-
"fin_Latn",
|
| 569 |
-
"fra_Latn",
|
| 570 |
-
"glg_Latn",
|
| 571 |
-
"heb_Hebr",
|
| 572 |
-
"hin_Deva",
|
| 573 |
-
"hrv_Latn",
|
| 574 |
-
"hun_Latn",
|
| 575 |
-
"ind_Latn",
|
| 576 |
-
"ita_Latn",
|
| 577 |
-
"jpn_Jpan",
|
| 578 |
-
"kor_Hang",
|
| 579 |
-
"lit_Latn",
|
| 580 |
-
"mkd_Cyrl",
|
| 581 |
-
"nld_Latn",
|
| 582 |
-
"pol_Latn",
|
| 583 |
-
"por_Latn",
|
| 584 |
-
"ron_Latn",
|
| 585 |
-
"rus_Cyrl",
|
| 586 |
-
"slk_Latn",
|
| 587 |
-
"slv_Latn",
|
| 588 |
-
"spa_Latn",
|
| 589 |
-
"srp_Cyrl",
|
| 590 |
-
"swe_Latn",
|
| 591 |
-
"tur_Latn",
|
| 592 |
-
"ukr_Cyrl",
|
| 593 |
-
"vie_Latn"
|
| 594 |
-
]
|
| 595 |
-
}
|
| 596 |
-
},
|
| 597 |
-
{
|
| 598 |
-
"name": "mrl-wikititles",
|
| 599 |
-
"weight": 1.0,
|
| 600 |
-
"anchor_column": "anchor",
|
| 601 |
-
"positive_column": "positive",
|
| 602 |
-
"parquet_files": 122,
|
| 603 |
-
"language_sampling": {
|
| 604 |
-
"path_component": 0,
|
| 605 |
-
"temperature": 1.0,
|
| 606 |
-
"include_languages": [
|
| 607 |
-
"arb_Arab",
|
| 608 |
-
"azj_Latn",
|
| 609 |
-
"bel_Cyrl",
|
| 610 |
-
"ben_Beng",
|
| 611 |
-
"bul_Cyrl",
|
| 612 |
-
"cat_Latn",
|
| 613 |
-
"ces_Latn",
|
| 614 |
-
"cmn_Hani",
|
| 615 |
-
"dan_Latn",
|
| 616 |
-
"deu_Latn",
|
| 617 |
-
"ell_Grek",
|
| 618 |
-
"eng_Latn",
|
| 619 |
-
"fas_Arab",
|
| 620 |
-
"fin_Latn",
|
| 621 |
-
"fra_Latn",
|
| 622 |
-
"heb_Hebr",
|
| 623 |
-
"hin_Deva",
|
| 624 |
-
"hrv_Latn",
|
| 625 |
-
"hun_Latn",
|
| 626 |
-
"hye_Armn",
|
| 627 |
-
"ind_Latn",
|
| 628 |
-
"ita_Latn",
|
| 629 |
-
"jpn_Jpan",
|
| 630 |
-
"kat_Geor",
|
| 631 |
-
"kor_Hang",
|
| 632 |
-
"lit_Latn",
|
| 633 |
-
"lvs_Latn",
|
| 634 |
-
"mal_Mlym",
|
| 635 |
-
"mar_Deva",
|
| 636 |
-
"mkd_Cyrl",
|
| 637 |
-
"mya_Mymr",
|
| 638 |
-
"nld_Latn",
|
| 639 |
-
"nob_Latn",
|
| 640 |
-
"pol_Latn",
|
| 641 |
-
"por_Latn",
|
| 642 |
-
"ron_Latn",
|
| 643 |
-
"rus_Cyrl",
|
| 644 |
-
"slk_Latn",
|
| 645 |
-
"slv_Latn",
|
| 646 |
-
"spa_Latn",
|
| 647 |
-
"srp_Cyrl",
|
| 648 |
-
"swe_Latn",
|
| 649 |
-
"tam_Taml",
|
| 650 |
-
"tha_Thai",
|
| 651 |
-
"tur_Latn",
|
| 652 |
-
"ukr_Cyrl",
|
| 653 |
-
"urd_Arab",
|
| 654 |
-
"vie_Latn"
|
| 655 |
-
]
|
| 656 |
-
}
|
| 657 |
-
},
|
| 658 |
-
{
|
| 659 |
-
"name": "semantic-ltg-wiki-paraphrases",
|
| 660 |
-
"weight": 1.0,
|
| 661 |
-
"anchor_column": "anchor",
|
| 662 |
-
"positive_column": "positive",
|
| 663 |
-
"parquet_files": 52,
|
| 664 |
-
"language_sampling": {
|
| 665 |
-
"path_component": 0,
|
| 666 |
-
"temperature": 1.0,
|
| 667 |
-
"include_languages": [
|
| 668 |
-
"eng_Latn"
|
| 669 |
-
]
|
| 670 |
-
}
|
| 671 |
-
},
|
| 672 |
-
{
|
| 673 |
-
"name": "semantic-opengloss",
|
| 674 |
-
"weight": 1.0,
|
| 675 |
-
"anchor_column": "anchor",
|
| 676 |
-
"positive_column": "positive",
|
| 677 |
-
"parquet_files": 3,
|
| 678 |
-
"language_sampling": {
|
| 679 |
-
"path_component": 0,
|
| 680 |
-
"temperature": 1.0,
|
| 681 |
-
"include_languages": [
|
| 682 |
-
"eng_Latn"
|
| 683 |
-
]
|
| 684 |
-
}
|
| 685 |
-
},
|
| 686 |
-
{
|
| 687 |
-
"name": "semantic-redis-paraphrases",
|
| 688 |
-
"weight": 1.0,
|
| 689 |
-
"anchor_column": "anchor",
|
| 690 |
-
"positive_column": "positive",
|
| 691 |
-
"parquet_files": 13,
|
| 692 |
-
"language_sampling": {
|
| 693 |
-
"path_component": 0,
|
| 694 |
-
"temperature": 1.0,
|
| 695 |
-
"include_languages": [
|
| 696 |
-
"eng_Latn"
|
| 697 |
-
]
|
| 698 |
-
}
|
| 699 |
-
}
|
| 700 |
-
],
|
| 701 |
-
"streaming": {
|
| 702 |
-
"fragment_rows": 16384,
|
| 703 |
-
"max_chars": 512,
|
| 704 |
-
"shuffle_buffer_size": 1,
|
| 705 |
-
"queue_size": 65536,
|
| 706 |
-
"worker_replicas": 1,
|
| 707 |
-
"batch_size": 16384,
|
| 708 |
-
"batch_no_duplicates": true,
|
| 709 |
-
"monolingual_batches": true,
|
| 710 |
-
"language_markers": true,
|
| 711 |
-
"holdout_row_groups": 0,
|
| 712 |
-
"smooth_weighted_round_robin": true,
|
| 713 |
-
"source_temperature": 0.4,
|
| 714 |
-
"language_temperature": 0.4,
|
| 715 |
-
"strict_unique_batches": true,
|
| 716 |
-
"scheduler_start_batch": 21000,
|
| 717 |
-
"seed": 20260805
|
| 718 |
-
},
|
| 719 |
-
"loss": {
|
| 720 |
-
"type": "cached_multiple_negatives",
|
| 721 |
-
"scale": 20.0,
|
| 722 |
-
"mini_batch_size": 2048,
|
| 723 |
-
"directions": [
|
| 724 |
-
"query_to_doc",
|
| 725 |
-
"doc_to_query"
|
| 726 |
-
],
|
| 727 |
-
"partition_mode": "per_direction"
|
| 728 |
-
},
|
| 729 |
-
"dynamic_vocabulary": {},
|
| 730 |
-
"vocabulary_growth": {},
|
| 731 |
-
"recursive_vocabulary": {},
|
| 732 |
-
"online_tokenizer": {},
|
| 733 |
-
"openlex_replay": {},
|
| 734 |
-
"vocabulary_telemetry": {},
|
| 735 |
-
"training": {
|
| 736 |
-
"max_steps": 5000,
|
| 737 |
-
"per_device_train_batch_size": 16384,
|
| 738 |
-
"gradient_accumulation_steps": 1,
|
| 739 |
-
"learning_rate": 0.0001,
|
| 740 |
-
"weight_decay": 0.05,
|
| 741 |
-
"warmup_steps": 1000,
|
| 742 |
-
"lr_scheduler_type": "inverse_sqrt",
|
| 743 |
-
"save_steps": 1000,
|
| 744 |
-
"save_total_limit": 5,
|
| 745 |
-
"logging_steps": 100,
|
| 746 |
-
"dataloader_num_workers": 1,
|
| 747 |
-
"dataloader_prefetch_factor": 1,
|
| 748 |
-
"bf16": true,
|
| 749 |
-
"max_tokens": 128,
|
| 750 |
-
"split_penalty": 0.0,
|
| 751 |
-
"save_strategy": "steps"
|
| 752 |
-
}
|
| 753 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/__init__.py
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
"""Training code for StaRSE static sentence embeddings."""
|
| 2 |
-
|
| 3 |
-
__version__ = "0.1.0"
|
|
|
|
|
|
|
|
|
|
|
|
starse/binary_static_embedding.py
DELETED
|
@@ -1,160 +0,0 @@
|
|
| 1 |
-
"""Sentence-Transformers module backed by packed sign bits and row norms."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import json
|
| 6 |
-
import math
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Any
|
| 9 |
-
|
| 10 |
-
try:
|
| 11 |
-
from typing import Self
|
| 12 |
-
except ImportError:
|
| 13 |
-
from typing_extensions import Self
|
| 14 |
-
|
| 15 |
-
import numpy as np
|
| 16 |
-
import torch
|
| 17 |
-
from safetensors.torch import load_file, save_file
|
| 18 |
-
from sentence_transformers.base.modules.input_module import InputModule
|
| 19 |
-
from tokenizers import Tokenizer
|
| 20 |
-
from torch import nn
|
| 21 |
-
from transformers import PreTrainedTokenizerFast
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
class BinaryStaticEmbedding(InputModule):
|
| 25 |
-
"""StaticEmbedding stored as one sign bit per coordinate plus one row norm."""
|
| 26 |
-
|
| 27 |
-
modalities: list[str] = ["text"]
|
| 28 |
-
config_keys: list[str] = ["embedding_dim", "vocab_size"]
|
| 29 |
-
config_file_name = "binary_static_embedding_config.json"
|
| 30 |
-
weights_file_name = "model.safetensors"
|
| 31 |
-
tokenizer_file_name = "tokenizer.json"
|
| 32 |
-
|
| 33 |
-
def __init__(
|
| 34 |
-
self,
|
| 35 |
-
tokenizer: Tokenizer | PreTrainedTokenizerFast,
|
| 36 |
-
embedding_dim: int,
|
| 37 |
-
vocab_size: int,
|
| 38 |
-
packed_signs: torch.Tensor | np.ndarray | None = None,
|
| 39 |
-
norms: torch.Tensor | np.ndarray | None = None,
|
| 40 |
-
embedding_weights: torch.Tensor | np.ndarray | None = None,
|
| 41 |
-
**kwargs: Any,
|
| 42 |
-
) -> None:
|
| 43 |
-
super().__init__()
|
| 44 |
-
if isinstance(tokenizer, PreTrainedTokenizerFast):
|
| 45 |
-
tokenizer = tokenizer.backend_tokenizer
|
| 46 |
-
if not isinstance(tokenizer, Tokenizer):
|
| 47 |
-
raise TypeError("BinaryStaticEmbedding requires a Rust-backed tokenizer")
|
| 48 |
-
self.tokenizer = tokenizer
|
| 49 |
-
self.tokenizer.no_padding()
|
| 50 |
-
self.embedding_dim = int(embedding_dim)
|
| 51 |
-
self.vocab_size = int(vocab_size)
|
| 52 |
-
if embedding_weights is not None:
|
| 53 |
-
weight = torch.as_tensor(embedding_weights, dtype=torch.float32).detach().cpu()
|
| 54 |
-
elif packed_signs is not None and norms is not None:
|
| 55 |
-
weight = self.unpack(packed_signs, norms, self.embedding_dim)
|
| 56 |
-
else:
|
| 57 |
-
weight = torch.zeros((self.vocab_size, self.embedding_dim), dtype=torch.float32)
|
| 58 |
-
if tuple(weight.shape) != (self.vocab_size, self.embedding_dim):
|
| 59 |
-
raise ValueError(
|
| 60 |
-
f"Embedding shape {tuple(weight.shape)} does not match ({self.vocab_size}, {self.embedding_dim})"
|
| 61 |
-
)
|
| 62 |
-
self.embedding = nn.EmbeddingBag.from_pretrained(weight.contiguous(), freeze=True)
|
| 63 |
-
self.num_embeddings = self.embedding.num_embeddings
|
| 64 |
-
self.base_model = kwargs.get("base_model")
|
| 65 |
-
|
| 66 |
-
@staticmethod
|
| 67 |
-
def pack(weight: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 68 |
-
weight = weight.detach().to(dtype=torch.float32, device="cpu")
|
| 69 |
-
norms = torch.linalg.vector_norm(weight, dim=1).clamp_min(1e-12)
|
| 70 |
-
packed = np.packbits((weight >= 0).numpy(), axis=1, bitorder="big")
|
| 71 |
-
return torch.from_numpy(packed), norms
|
| 72 |
-
|
| 73 |
-
@staticmethod
|
| 74 |
-
def unpack(
|
| 75 |
-
packed_signs: torch.Tensor | np.ndarray,
|
| 76 |
-
norms: torch.Tensor | np.ndarray,
|
| 77 |
-
embedding_dim: int,
|
| 78 |
-
) -> torch.Tensor:
|
| 79 |
-
packed = torch.as_tensor(packed_signs, dtype=torch.uint8).cpu()
|
| 80 |
-
row_norms = torch.as_tensor(norms, dtype=torch.float32).cpu().reshape(-1)
|
| 81 |
-
expected_columns = (int(embedding_dim) + 7) // 8
|
| 82 |
-
if packed.ndim != 2 or packed.shape[1] != expected_columns:
|
| 83 |
-
raise ValueError(f"Packed shape {tuple(packed.shape)} does not match (vocab, ceil({embedding_dim}/8))")
|
| 84 |
-
if packed.shape[0] != row_norms.shape[0]:
|
| 85 |
-
raise ValueError("Packed sign rows and norm rows differ")
|
| 86 |
-
bits = np.unpackbits(packed.numpy(), axis=1, bitorder="big")[:, :embedding_dim]
|
| 87 |
-
signs = torch.from_numpy(bits.astype(np.float32) * 2.0 - 1.0)
|
| 88 |
-
return (signs * (row_norms / math.sqrt(embedding_dim)).unsqueeze(1)).contiguous()
|
| 89 |
-
|
| 90 |
-
def preprocess(self, inputs: list[str], prompt: str | None = None, **kwargs: Any) -> dict[str, torch.Tensor]:
|
| 91 |
-
if prompt:
|
| 92 |
-
inputs = self._prepend_prompt(inputs, prompt)
|
| 93 |
-
rows = [encoding.ids for encoding in self.tokenizer.encode_batch(inputs, add_special_tokens=False)]
|
| 94 |
-
offsets = torch.from_numpy(np.cumsum([0] + [len(row) for row in rows[:-1]]))
|
| 95 |
-
input_ids = torch.tensor([token_id for row in rows for token_id in row], dtype=torch.long)
|
| 96 |
-
return {"input_ids": input_ids, "offsets": offsets}
|
| 97 |
-
|
| 98 |
-
def forward(self, features: dict[str, torch.Tensor], **kwargs: Any) -> dict[str, torch.Tensor]:
|
| 99 |
-
features["sentence_embedding"] = self.embedding(features["input_ids"], features["offsets"])
|
| 100 |
-
return features
|
| 101 |
-
|
| 102 |
-
@property
|
| 103 |
-
def max_seq_length(self) -> int | float:
|
| 104 |
-
return math.inf
|
| 105 |
-
|
| 106 |
-
def get_embedding_dimension(self) -> int:
|
| 107 |
-
return self.embedding_dim
|
| 108 |
-
|
| 109 |
-
def save(self, output_path: str, *args: Any, **kwargs: Any) -> None:
|
| 110 |
-
path = Path(output_path)
|
| 111 |
-
path.mkdir(parents=True, exist_ok=True)
|
| 112 |
-
packed_signs, norms = self.pack(self.embedding.weight)
|
| 113 |
-
save_file({"packed_signs": packed_signs, "norms": norms}, str(path / self.weights_file_name))
|
| 114 |
-
(path / self.config_file_name).write_text(
|
| 115 |
-
json.dumps(
|
| 116 |
-
{
|
| 117 |
-
"embedding_dim": self.embedding_dim,
|
| 118 |
-
"vocab_size": self.vocab_size,
|
| 119 |
-
"packed_bit_order": "big",
|
| 120 |
-
"scale": "norm / sqrt(embedding_dim)",
|
| 121 |
-
},
|
| 122 |
-
indent=2,
|
| 123 |
-
)
|
| 124 |
-
+ "\n",
|
| 125 |
-
encoding="utf-8",
|
| 126 |
-
)
|
| 127 |
-
self.tokenizer.save(str(path / self.tokenizer_file_name))
|
| 128 |
-
|
| 129 |
-
@classmethod
|
| 130 |
-
def load(
|
| 131 |
-
cls,
|
| 132 |
-
model_name_or_path: str,
|
| 133 |
-
subfolder: str = "",
|
| 134 |
-
token: bool | str | None = None,
|
| 135 |
-
cache_folder: str | None = None,
|
| 136 |
-
revision: str | None = None,
|
| 137 |
-
local_files_only: bool = False,
|
| 138 |
-
**kwargs: Any,
|
| 139 |
-
) -> Self:
|
| 140 |
-
hub = {
|
| 141 |
-
"subfolder": subfolder,
|
| 142 |
-
"token": token,
|
| 143 |
-
"cache_folder": cache_folder,
|
| 144 |
-
"revision": revision,
|
| 145 |
-
"local_files_only": local_files_only,
|
| 146 |
-
}
|
| 147 |
-
config_path = cls.load_file_path(model_name_or_path, filename=cls.config_file_name, **hub)
|
| 148 |
-
tokenizer_path = cls.load_file_path(model_name_or_path, filename=cls.tokenizer_file_name, **hub)
|
| 149 |
-
weights_path = cls.load_file_path(model_name_or_path, filename=cls.weights_file_name, **hub)
|
| 150 |
-
if not config_path or not tokenizer_path or not weights_path:
|
| 151 |
-
raise FileNotFoundError(f"Incomplete BinaryStaticEmbedding at {model_name_or_path}")
|
| 152 |
-
config = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
| 153 |
-
state = load_file(weights_path)
|
| 154 |
-
return cls(
|
| 155 |
-
tokenizer=Tokenizer.from_file(tokenizer_path),
|
| 156 |
-
embedding_dim=int(config["embedding_dim"]),
|
| 157 |
-
vocab_size=int(config["vocab_size"]),
|
| 158 |
-
packed_signs=state["packed_signs"],
|
| 159 |
-
norms=state["norms"],
|
| 160 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/cached_loss.py
DELETED
|
@@ -1,223 +0,0 @@
|
|
| 1 |
-
"""Gradient-cache losses for StaRSE's differentiable static modules."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections.abc import Iterable, Iterator
|
| 6 |
-
from contextlib import nullcontext
|
| 7 |
-
from typing import Any
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
-
import tqdm
|
| 11 |
-
from sentence_transformers.sentence_transformer.losses import (
|
| 12 |
-
CachedMultipleNegativesRankingLoss,
|
| 13 |
-
)
|
| 14 |
-
from sentence_transformers.sentence_transformer.losses.cached_multiple_negatives_ranking import (
|
| 15 |
-
RandContext,
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def _is_static_flattened(features: dict[str, Any]) -> bool:
|
| 20 |
-
input_ids = features.get("input_ids")
|
| 21 |
-
offsets = features.get("offsets")
|
| 22 |
-
return (
|
| 23 |
-
isinstance(input_ids, torch.Tensor)
|
| 24 |
-
and input_ids.ndim == 1
|
| 25 |
-
and isinstance(offsets, torch.Tensor)
|
| 26 |
-
and offsets.ndim == 1
|
| 27 |
-
)
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def _slice_static_features(
|
| 31 |
-
features: dict[str, Any], *, begin: int, end: int
|
| 32 |
-
) -> dict[str, Any]:
|
| 33 |
-
"""Slice StaRSE's flattened token stream by sentence offsets."""
|
| 34 |
-
|
| 35 |
-
if not _is_static_flattened(features):
|
| 36 |
-
raise ValueError("static feature slicing requires 1D input_ids and offsets")
|
| 37 |
-
input_ids = features["input_ids"]
|
| 38 |
-
offsets = features["offsets"]
|
| 39 |
-
batch_size = int(offsets.numel())
|
| 40 |
-
begin = max(0, min(int(begin), batch_size))
|
| 41 |
-
end = max(begin, min(int(end), batch_size))
|
| 42 |
-
token_begin = int(offsets[begin].item()) if begin < batch_size else input_ids.numel()
|
| 43 |
-
token_end = (
|
| 44 |
-
int(offsets[end].item()) if end < batch_size else input_ids.numel()
|
| 45 |
-
)
|
| 46 |
-
result: dict[str, Any] = {}
|
| 47 |
-
for key, value in features.items():
|
| 48 |
-
if key == "input_ids":
|
| 49 |
-
result[key] = value[token_begin:token_end]
|
| 50 |
-
elif key == "offsets":
|
| 51 |
-
result[key] = value[begin:end] - token_begin
|
| 52 |
-
elif isinstance(value, torch.Tensor) and value.ndim:
|
| 53 |
-
if value.shape[0] == batch_size:
|
| 54 |
-
result[key] = value[begin:end]
|
| 55 |
-
elif value.shape[0] == input_ids.numel():
|
| 56 |
-
result[key] = value[token_begin:token_end]
|
| 57 |
-
else:
|
| 58 |
-
result[key] = value
|
| 59 |
-
else:
|
| 60 |
-
result[key] = value
|
| 61 |
-
return result
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
class _StaticTypeGuardProxy:
|
| 65 |
-
"""Bypass only the upstream constructor's StaticEmbedding type guard."""
|
| 66 |
-
|
| 67 |
-
def __init__(self, model: Any) -> None:
|
| 68 |
-
self._model = model
|
| 69 |
-
|
| 70 |
-
def __getitem__(self, index: int) -> object:
|
| 71 |
-
if index != 0:
|
| 72 |
-
raise IndexError(index)
|
| 73 |
-
return object()
|
| 74 |
-
|
| 75 |
-
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
| 76 |
-
return self._model(*args, **kwargs)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
class StaticCompatibleCachedMultipleNegativesRankingLoss(
|
| 80 |
-
CachedMultipleNegativesRankingLoss
|
| 81 |
-
):
|
| 82 |
-
"""Use upstream GradCache with a differentiable StaRSE static encoder.
|
| 83 |
-
|
| 84 |
-
SentenceTransformers rejects all ``StaticEmbedding`` subclasses because its
|
| 85 |
-
ordinary lookup module has little reason to use GradCache. StaRSE adds a
|
| 86 |
-
differentiable collapse graph whose activation memory does benefit from the
|
| 87 |
-
cache. Construction therefore uses a temporary type-guard proxy, then
|
| 88 |
-
restores the real model before any forward call. All cache, objective and
|
| 89 |
-
replay code remains upstream.
|
| 90 |
-
"""
|
| 91 |
-
|
| 92 |
-
def __init__(self, model: Any, *args: Any, **kwargs: Any) -> None:
|
| 93 |
-
super().__init__(_StaticTypeGuardProxy(model), *args, **kwargs)
|
| 94 |
-
self.model = model
|
| 95 |
-
|
| 96 |
-
def _online_table(self) -> Any | None:
|
| 97 |
-
try:
|
| 98 |
-
module = self.model[0]
|
| 99 |
-
except (AttributeError, IndexError, KeyError, TypeError):
|
| 100 |
-
return None
|
| 101 |
-
return getattr(module, "online_collapse", None)
|
| 102 |
-
|
| 103 |
-
def _telemetry_window(self) -> Any | None:
|
| 104 |
-
try:
|
| 105 |
-
module = self.model[0]
|
| 106 |
-
except (AttributeError, IndexError, KeyError, TypeError):
|
| 107 |
-
return None
|
| 108 |
-
for name in ("online_collapse", "collapse"):
|
| 109 |
-
table = getattr(module, name, None)
|
| 110 |
-
window = getattr(table, "_vocabulary_telemetry", None)
|
| 111 |
-
if window is not None:
|
| 112 |
-
return window
|
| 113 |
-
return None
|
| 114 |
-
|
| 115 |
-
def forward(
|
| 116 |
-
self,
|
| 117 |
-
sentence_features: Iterable[dict[str, torch.Tensor]],
|
| 118 |
-
labels: torch.Tensor | None,
|
| 119 |
-
) -> torch.Tensor:
|
| 120 |
-
table = self._online_table()
|
| 121 |
-
audit = table is not None and torch.is_grad_enabled()
|
| 122 |
-
if audit:
|
| 123 |
-
table.start_cache_audit()
|
| 124 |
-
try:
|
| 125 |
-
return super().forward(sentence_features, labels)
|
| 126 |
-
except BaseException:
|
| 127 |
-
if audit:
|
| 128 |
-
table.abort_cache_audit()
|
| 129 |
-
raise
|
| 130 |
-
|
| 131 |
-
def embed_minibatch(
|
| 132 |
-
self,
|
| 133 |
-
sentence_feature: dict[str, torch.Tensor],
|
| 134 |
-
begin: int,
|
| 135 |
-
end: int,
|
| 136 |
-
with_grad: bool,
|
| 137 |
-
copy_random_state: bool,
|
| 138 |
-
random_state: RandContext | None = None,
|
| 139 |
-
) -> tuple[torch.Tensor, RandContext | None]:
|
| 140 |
-
if not _is_static_flattened(sentence_feature):
|
| 141 |
-
return super().embed_minibatch(
|
| 142 |
-
sentence_feature,
|
| 143 |
-
begin,
|
| 144 |
-
end,
|
| 145 |
-
with_grad,
|
| 146 |
-
copy_random_state,
|
| 147 |
-
random_state,
|
| 148 |
-
)
|
| 149 |
-
minibatch = _slice_static_features(
|
| 150 |
-
sentence_feature, begin=begin, end=end
|
| 151 |
-
)
|
| 152 |
-
table = self._online_table()
|
| 153 |
-
telemetry = self._telemetry_window()
|
| 154 |
-
if table is not None:
|
| 155 |
-
if with_grad:
|
| 156 |
-
table.begin_cache_replay()
|
| 157 |
-
else:
|
| 158 |
-
table.begin_cache_record()
|
| 159 |
-
telemetry_started = False
|
| 160 |
-
if telemetry is not None:
|
| 161 |
-
if with_grad:
|
| 162 |
-
telemetry.begin_replay()
|
| 163 |
-
else:
|
| 164 |
-
telemetry.begin_record()
|
| 165 |
-
telemetry_started = True
|
| 166 |
-
grad_context = nullcontext if with_grad else torch.no_grad
|
| 167 |
-
random_state_context = nullcontext() if random_state is None else random_state
|
| 168 |
-
try:
|
| 169 |
-
with random_state_context:
|
| 170 |
-
with grad_context():
|
| 171 |
-
copied = (
|
| 172 |
-
RandContext(*minibatch.values()) if copy_random_state else None
|
| 173 |
-
)
|
| 174 |
-
reps = self.model(minibatch)["sentence_embedding"]
|
| 175 |
-
except BaseException:
|
| 176 |
-
if table is not None:
|
| 177 |
-
table.abort_cache_audit()
|
| 178 |
-
if telemetry is not None:
|
| 179 |
-
telemetry.abort_phase()
|
| 180 |
-
telemetry_started = False
|
| 181 |
-
raise
|
| 182 |
-
finally:
|
| 183 |
-
if table is not None:
|
| 184 |
-
table.end_cache_phase()
|
| 185 |
-
if telemetry is not None and telemetry_started:
|
| 186 |
-
telemetry.end_phase()
|
| 187 |
-
return reps, copied
|
| 188 |
-
|
| 189 |
-
def embed_minibatch_iter(
|
| 190 |
-
self,
|
| 191 |
-
sentence_feature: dict[str, torch.Tensor],
|
| 192 |
-
with_grad: bool,
|
| 193 |
-
copy_random_state: bool,
|
| 194 |
-
random_states: list[RandContext] | None = None,
|
| 195 |
-
) -> Iterator[tuple[torch.Tensor, RandContext | None]]:
|
| 196 |
-
if not _is_static_flattened(sentence_feature):
|
| 197 |
-
yield from super().embed_minibatch_iter(
|
| 198 |
-
sentence_feature,
|
| 199 |
-
with_grad,
|
| 200 |
-
copy_random_state,
|
| 201 |
-
random_states,
|
| 202 |
-
)
|
| 203 |
-
return
|
| 204 |
-
batch_size = int(sentence_feature["offsets"].numel())
|
| 205 |
-
for index, begin in enumerate(
|
| 206 |
-
tqdm.trange(
|
| 207 |
-
0,
|
| 208 |
-
batch_size,
|
| 209 |
-
self.mini_batch_size,
|
| 210 |
-
desc="Embed static mini-batches",
|
| 211 |
-
disable=not self.show_progress_bar,
|
| 212 |
-
)
|
| 213 |
-
):
|
| 214 |
-
yield self.embed_minibatch(
|
| 215 |
-
sentence_feature=sentence_feature,
|
| 216 |
-
begin=begin,
|
| 217 |
-
end=begin + self.mini_batch_size,
|
| 218 |
-
with_grad=with_grad,
|
| 219 |
-
copy_random_state=copy_random_state,
|
| 220 |
-
random_state=(
|
| 221 |
-
None if random_states is None else random_states[index]
|
| 222 |
-
),
|
| 223 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/checkpoint_precision.py
DELETED
|
@@ -1,166 +0,0 @@
|
|
| 1 |
-
"""Atomic BF16 model persistence and model/resume-state retention policy."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import hashlib
|
| 6 |
-
import json
|
| 7 |
-
import os
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
import re
|
| 10 |
-
from typing import Any
|
| 11 |
-
|
| 12 |
-
import torch
|
| 13 |
-
from safetensors.torch import load_file, save_file
|
| 14 |
-
from transformers import TrainerCallback
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
_CHECKPOINT = re.compile(r"checkpoint-(\d+)$")
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def _sha256(path: Path) -> str:
|
| 21 |
-
digest = hashlib.sha256()
|
| 22 |
-
with path.open("rb") as handle:
|
| 23 |
-
for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
| 24 |
-
digest.update(chunk)
|
| 25 |
-
return digest.hexdigest()
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _fsync_directory(path: Path) -> None:
|
| 29 |
-
descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
|
| 30 |
-
try:
|
| 31 |
-
os.fsync(descriptor)
|
| 32 |
-
finally:
|
| 33 |
-
os.close(descriptor)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def rewrite_safetensors_bf16(path: Path) -> dict[str, object]:
|
| 37 |
-
"""Replace one safetensors file only after a complete BF16 verification."""
|
| 38 |
-
|
| 39 |
-
path = Path(path).expanduser().resolve()
|
| 40 |
-
if not path.is_file():
|
| 41 |
-
raise FileNotFoundError(path)
|
| 42 |
-
before_sha256 = _sha256(path)
|
| 43 |
-
tensors = load_file(str(path), device="cpu")
|
| 44 |
-
inventory: dict[str, dict[str, object]] = {}
|
| 45 |
-
converted: dict[str, torch.Tensor] = {}
|
| 46 |
-
for name, tensor in sorted(tensors.items()):
|
| 47 |
-
if tensor.is_floating_point() and not bool(torch.isfinite(tensor).all()):
|
| 48 |
-
raise FloatingPointError(f"nonfinite checkpoint tensor: {name}")
|
| 49 |
-
target = tensor.to(torch.bfloat16) if tensor.is_floating_point() else tensor
|
| 50 |
-
converted[name] = target.contiguous()
|
| 51 |
-
inventory[name] = {
|
| 52 |
-
"shape": list(tensor.shape),
|
| 53 |
-
"source_dtype": str(tensor.dtype).removeprefix("torch."),
|
| 54 |
-
"stored_dtype": str(target.dtype).removeprefix("torch."),
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
temporary = path.with_suffix(path.suffix + ".bf16.tmp")
|
| 58 |
-
if temporary.exists():
|
| 59 |
-
raise FileExistsError(temporary)
|
| 60 |
-
try:
|
| 61 |
-
save_file(converted, str(temporary))
|
| 62 |
-
with temporary.open("rb") as handle:
|
| 63 |
-
os.fsync(handle.fileno())
|
| 64 |
-
verified = load_file(str(temporary), device="cpu")
|
| 65 |
-
if set(verified) != set(converted):
|
| 66 |
-
raise RuntimeError("BF16 checkpoint tensor inventory changed")
|
| 67 |
-
for name, tensor in verified.items():
|
| 68 |
-
source = tensors[name]
|
| 69 |
-
if tensor.shape != source.shape:
|
| 70 |
-
raise RuntimeError(f"BF16 checkpoint shape changed: {name}")
|
| 71 |
-
expected_dtype = torch.bfloat16 if source.is_floating_point() else source.dtype
|
| 72 |
-
if tensor.dtype != expected_dtype:
|
| 73 |
-
raise RuntimeError(f"BF16 checkpoint dtype mismatch: {name}")
|
| 74 |
-
if tensor.is_floating_point() and not bool(torch.isfinite(tensor).all()):
|
| 75 |
-
raise FloatingPointError(f"nonfinite converted checkpoint tensor: {name}")
|
| 76 |
-
os.replace(temporary, path)
|
| 77 |
-
_fsync_directory(path.parent)
|
| 78 |
-
except BaseException:
|
| 79 |
-
if temporary.exists():
|
| 80 |
-
temporary.unlink()
|
| 81 |
-
raise
|
| 82 |
-
|
| 83 |
-
return {
|
| 84 |
-
"protocol": "bf16-model-safetensors-v1",
|
| 85 |
-
"path": str(path),
|
| 86 |
-
"before_sha256": before_sha256,
|
| 87 |
-
"after_sha256": _sha256(path),
|
| 88 |
-
"stored_floating_dtype": "bfloat16",
|
| 89 |
-
"tensor_count": len(inventory),
|
| 90 |
-
"tensors": inventory,
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
class BFloat16CheckpointCallback(TrainerCallback):
|
| 95 |
-
"""Quantize every model snapshot and retain resume files only on the newest."""
|
| 96 |
-
|
| 97 |
-
def __init__(self, output_dir: Path, *, keep_resume_checkpoints: int = 1) -> None:
|
| 98 |
-
self.output_dir = Path(output_dir).expanduser().resolve()
|
| 99 |
-
if keep_resume_checkpoints <= 0:
|
| 100 |
-
raise ValueError("keep_resume_checkpoints must be positive")
|
| 101 |
-
self.keep_resume_checkpoints = int(keep_resume_checkpoints)
|
| 102 |
-
self.completed_saves = 0
|
| 103 |
-
|
| 104 |
-
@staticmethod
|
| 105 |
-
def _step(path: Path) -> int:
|
| 106 |
-
match = _CHECKPOINT.fullmatch(path.name)
|
| 107 |
-
if match is None:
|
| 108 |
-
raise ValueError(f"invalid Trainer checkpoint name: {path.name}")
|
| 109 |
-
return int(match.group(1))
|
| 110 |
-
|
| 111 |
-
def _write_manifest(self, checkpoint: Path, report: dict[str, object]) -> None:
|
| 112 |
-
destination = checkpoint / "checkpoint_precision.json"
|
| 113 |
-
temporary = checkpoint / "checkpoint_precision.json.tmp"
|
| 114 |
-
if destination.exists() or temporary.exists():
|
| 115 |
-
raise FileExistsError(destination)
|
| 116 |
-
payload: dict[str, Any] = {
|
| 117 |
-
"protocol": "bf16-checkpoint-precision-v1",
|
| 118 |
-
"checkpoint_step": self._step(checkpoint),
|
| 119 |
-
"stored_floating_dtype": "bfloat16",
|
| 120 |
-
"model_sha256": report["after_sha256"],
|
| 121 |
-
"rewrite": report,
|
| 122 |
-
}
|
| 123 |
-
data = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
| 124 |
-
with temporary.open("x", encoding="utf-8") as handle:
|
| 125 |
-
handle.write(data)
|
| 126 |
-
handle.flush()
|
| 127 |
-
os.fsync(handle.fileno())
|
| 128 |
-
os.replace(temporary, destination)
|
| 129 |
-
_fsync_directory(checkpoint)
|
| 130 |
-
|
| 131 |
-
@staticmethod
|
| 132 |
-
def _remove_resume_files(checkpoint: Path) -> None:
|
| 133 |
-
targets = {
|
| 134 |
-
checkpoint / "optimizer.pt",
|
| 135 |
-
checkpoint / "scheduler.pt",
|
| 136 |
-
checkpoint / "scaler.pt",
|
| 137 |
-
checkpoint / "rng_state.pth",
|
| 138 |
-
*checkpoint.glob("rng_state_*.pth"),
|
| 139 |
-
}
|
| 140 |
-
for target in sorted(targets):
|
| 141 |
-
if target.is_file():
|
| 142 |
-
target.unlink()
|
| 143 |
-
_fsync_directory(checkpoint)
|
| 144 |
-
|
| 145 |
-
def on_save(self, args, state, control, **kwargs):
|
| 146 |
-
del kwargs
|
| 147 |
-
if Path(args.output_dir).expanduser().resolve() != self.output_dir:
|
| 148 |
-
raise ValueError("BF16 callback output directory differs from Trainer")
|
| 149 |
-
checkpoint = self.output_dir / f"checkpoint-{int(state.global_step)}"
|
| 150 |
-
model_path = checkpoint / "model.safetensors"
|
| 151 |
-
report = rewrite_safetensors_bf16(model_path)
|
| 152 |
-
self._write_manifest(checkpoint, report)
|
| 153 |
-
|
| 154 |
-
verified = sorted(
|
| 155 |
-
(
|
| 156 |
-
path for path in self.output_dir.glob("checkpoint-*")
|
| 157 |
-
if path.is_dir() and (path / "checkpoint_precision.json").is_file()
|
| 158 |
-
),
|
| 159 |
-
key=self._step,
|
| 160 |
-
)
|
| 161 |
-
retained = set(verified[-self.keep_resume_checkpoints :])
|
| 162 |
-
for older in verified:
|
| 163 |
-
if older not in retained:
|
| 164 |
-
self._remove_resume_files(older)
|
| 165 |
-
self.completed_saves += 1
|
| 166 |
-
return control
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/collapse.py
DELETED
|
@@ -1,1930 +0,0 @@
|
|
| 1 |
-
"""Learned collapsing of adjacent tokens into single units.
|
| 2 |
-
|
| 3 |
-
The earlier n-gram experiment *added* bigram rows next to the unigrams, so a
|
| 4 |
-
Georgian word cut into six pieces became six unigrams plus five bigrams — eleven
|
| 5 |
-
items instead of six. That raised fragmentation instead of lowering it, which is
|
| 6 |
-
why well-tokenised languages lost ground: an English sentence of four tokens
|
| 7 |
-
gained three bigram rows and half its pooled mass went to them.
|
| 8 |
-
|
| 9 |
-
Collapsing is a soft *replacement*: a learned merged row enters the numerator
|
| 10 |
-
while the neighbouring unigram rows lose mass. That changes the direction toward
|
| 11 |
-
units the tokenizer should have produced and targets tokens-per-word directly —
|
| 12 |
-
the quantity whose rank correlation with held-out Recall@1 is -0.607.
|
| 13 |
-
|
| 14 |
-
For adjacent pair i, a merge probability p_i gates a learned merged row M_i:
|
| 15 |
-
|
| 16 |
-
numerator = sum_i (1 - (p_{i-1} + p_i)/2) * E[t_i] + sum_i p_i * M_i
|
| 17 |
-
denominator = n - sum_i p_i
|
| 18 |
-
|
| 19 |
-
At p = 0 this is exactly the plain mean, so the control is recovered by
|
| 20 |
-
construction. Increasing p rotates the pooled direction from neighbouring
|
| 21 |
-
unigrams toward the learned merged row; overlapping candidates share the
|
| 22 |
-
reduction instead of needing a discrete matching. Under cosine scoring the
|
| 23 |
-
positive scalar denominator cancels, so the mechanism is the relative numerator
|
| 24 |
-
composition, not the smaller denominator.
|
| 25 |
-
|
| 26 |
-
The merge score carries a per-language bias, so a language whose words are
|
| 27 |
-
already single tokens can shut merging off globally rather than paying for a
|
| 28 |
-
mechanism it does not need — the failure mode of the ungated n-gram arm.
|
| 29 |
-
"""
|
| 30 |
-
|
| 31 |
-
from __future__ import annotations
|
| 32 |
-
|
| 33 |
-
from collections import defaultdict
|
| 34 |
-
from dataclasses import dataclass
|
| 35 |
-
import heapq
|
| 36 |
-
import math
|
| 37 |
-
import statistics
|
| 38 |
-
from typing import Any
|
| 39 |
-
|
| 40 |
-
import torch
|
| 41 |
-
from torch import nn
|
| 42 |
-
|
| 43 |
-
_MIX_A = 2654435761
|
| 44 |
-
_MIX_B = 40503
|
| 45 |
-
_SKETCH_A = (6364136223846793005, 3202034522624059733, 3935559000370003845)
|
| 46 |
-
_SKETCH_B = (1442695040888963407, 2691343689449507681, 4768777513237032717)
|
| 47 |
-
_SKETCH_SIGN_A = (2862933555777941757, 7046029254386353131, 3935559000370003845)
|
| 48 |
-
_MAX_WEIGHTED_SOURCE_BYTES = 256 * 1024 * 1024
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def _weighted_index_add_(
|
| 52 |
-
target: torch.Tensor,
|
| 53 |
-
index: torch.Tensor,
|
| 54 |
-
source: torch.Tensor,
|
| 55 |
-
weight: torch.Tensor,
|
| 56 |
-
*,
|
| 57 |
-
max_chunk_bytes: int = _MAX_WEIGHTED_SOURCE_BYTES,
|
| 58 |
-
) -> torch.Tensor:
|
| 59 |
-
"""Index-add weighted rows without materialising an unbounded N x dim product."""
|
| 60 |
-
if source.ndim != 2 or weight.ndim != 1 or source.shape[0] != weight.shape[0]:
|
| 61 |
-
raise ValueError("weighted index-add needs aligned matrix rows and scalar weights")
|
| 62 |
-
if index.ndim != 1 or index.shape[0] != source.shape[0]:
|
| 63 |
-
raise ValueError("weighted index-add needs one destination per source row")
|
| 64 |
-
if max_chunk_bytes <= 0:
|
| 65 |
-
raise ValueError("weighted index-add chunk budget must be positive")
|
| 66 |
-
bytes_per_row = max(1, source.shape[1] * source.element_size())
|
| 67 |
-
rows_per_chunk = max(1, max_chunk_bytes // bytes_per_row)
|
| 68 |
-
for start in range(0, source.shape[0], rows_per_chunk):
|
| 69 |
-
stop = min(source.shape[0], start + rows_per_chunk)
|
| 70 |
-
target.index_add_(
|
| 71 |
-
0,
|
| 72 |
-
index[start:stop],
|
| 73 |
-
source[start:stop] * weight[start:stop].unsqueeze(1),
|
| 74 |
-
)
|
| 75 |
-
return target
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
@dataclass(frozen=True)
|
| 79 |
-
class PromotedVocabulary:
|
| 80 |
-
"""Deterministic result of bounded exact-pair promotion."""
|
| 81 |
-
|
| 82 |
-
pair_keys: tuple[int, ...]
|
| 83 |
-
records: tuple[dict[str, Any], ...]
|
| 84 |
-
candidate_count: int
|
| 85 |
-
reservation_memberships: tuple[tuple[int, int], ...]
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
class ContrastiveVocabularyDiscovery:
|
| 89 |
-
"""Bounded candidate table with signed CountSketch admission.
|
| 90 |
-
|
| 91 |
-
The sketch receives ``-p * dL/dp`` after each backward pass. It transfers
|
| 92 |
-
only the largest absolute pair/language aggregates from a forward. A fixed
|
| 93 |
-
signed CountSketch retains cumulative admission evidence even for keys not
|
| 94 |
-
currently in the exact table; the bounded exact table stores utility,
|
| 95 |
-
probability and support only from admission onward.
|
| 96 |
-
"""
|
| 97 |
-
|
| 98 |
-
admission_mode = "utility"
|
| 99 |
-
|
| 100 |
-
def __init__(
|
| 101 |
-
self,
|
| 102 |
-
*,
|
| 103 |
-
n_languages: int,
|
| 104 |
-
candidate_capacity: int,
|
| 105 |
-
top_per_forward: int,
|
| 106 |
-
) -> None:
|
| 107 |
-
if n_languages <= 0:
|
| 108 |
-
raise ValueError("n_languages must be positive")
|
| 109 |
-
if candidate_capacity <= 0:
|
| 110 |
-
raise ValueError("candidate_capacity must be positive")
|
| 111 |
-
if top_per_forward <= 0:
|
| 112 |
-
raise ValueError("top_per_forward must be positive")
|
| 113 |
-
self.n_languages = int(n_languages)
|
| 114 |
-
self.candidate_capacity = int(candidate_capacity)
|
| 115 |
-
self.top_per_forward = int(top_per_forward)
|
| 116 |
-
# composite pair/language key -> [signed utility, p sum, captured count]
|
| 117 |
-
self.records: dict[int, list[float | int]] = {}
|
| 118 |
-
self._sketch_width = max(64, self.candidate_capacity)
|
| 119 |
-
self._utility_sketch = torch.zeros(
|
| 120 |
-
(len(_SKETCH_A), self._sketch_width), dtype=torch.float64
|
| 121 |
-
)
|
| 122 |
-
# Lazy min-heap over retained rows. Updates append a new rank and leave
|
| 123 |
-
# the previous entry stale; periodic rebuilding gives the metadata a
|
| 124 |
-
# fixed bound without sorting the entire sketch on every forward.
|
| 125 |
-
self._heap: list[tuple[float, int, int, int]] = []
|
| 126 |
-
self.observed_occurrences = 0
|
| 127 |
-
self.transferred_records = 0
|
| 128 |
-
self.unique_records_before_transfer = 0
|
| 129 |
-
self.dropped_by_top_per_forward = 0
|
| 130 |
-
self.retained_updates = 0
|
| 131 |
-
self.admitted_into_free_slots = 0
|
| 132 |
-
self.capacity_replacements = 0
|
| 133 |
-
self.capacity_rejections = 0
|
| 134 |
-
|
| 135 |
-
@staticmethod
|
| 136 |
-
def _rank(key: int, row: list[float | int]) -> tuple[float, int, int]:
|
| 137 |
-
return float(row[0]), int(row[2]), -int(key)
|
| 138 |
-
|
| 139 |
-
def _push_heap(self, key: int) -> None:
|
| 140 |
-
heapq.heappush(self._heap, (*self._rank(key, self.records[key]), int(key)))
|
| 141 |
-
|
| 142 |
-
def _discard_stale_heap_entries(self) -> None:
|
| 143 |
-
while self._heap:
|
| 144 |
-
utility, count, negative_key, key = self._heap[0]
|
| 145 |
-
row = self.records.get(key)
|
| 146 |
-
if row is not None and (utility, count, negative_key) == self._rank(key, row):
|
| 147 |
-
return
|
| 148 |
-
heapq.heappop(self._heap)
|
| 149 |
-
|
| 150 |
-
def _rebuild_heap(self) -> None:
|
| 151 |
-
self._heap = [(*self._rank(key, row), key) for key, row in self.records.items()]
|
| 152 |
-
heapq.heapify(self._heap)
|
| 153 |
-
|
| 154 |
-
def _update_utility_sketch(
|
| 155 |
-
self, keys: torch.Tensor, values: torch.Tensor
|
| 156 |
-
) -> torch.Tensor:
|
| 157 |
-
"""Update fixed memory and return the median signed estimate per key."""
|
| 158 |
-
|
| 159 |
-
keys = keys.to(device="cpu", dtype=torch.long)
|
| 160 |
-
values = values.to(device="cpu", dtype=torch.float64)
|
| 161 |
-
estimates = []
|
| 162 |
-
for depth, (multiplier, offset, sign_multiplier) in enumerate(zip(
|
| 163 |
-
_SKETCH_A, _SKETCH_B, _SKETCH_SIGN_A, strict=True
|
| 164 |
-
)):
|
| 165 |
-
bucket = torch.remainder(keys * multiplier + offset, self._sketch_width)
|
| 166 |
-
sign = torch.where(
|
| 167 |
-
torch.bitwise_and(keys * sign_multiplier + offset, 1) == 0,
|
| 168 |
-
1.0,
|
| 169 |
-
-1.0,
|
| 170 |
-
).to(torch.float64)
|
| 171 |
-
self._utility_sketch[depth].index_add_(0, bucket, values * sign)
|
| 172 |
-
estimates.append(self._utility_sketch[depth, bucket] * sign)
|
| 173 |
-
return torch.stack(estimates).median(dim=0).values
|
| 174 |
-
|
| 175 |
-
@torch.no_grad()
|
| 176 |
-
def observe(
|
| 177 |
-
self,
|
| 178 |
-
pair_key: torch.Tensor,
|
| 179 |
-
language: torch.Tensor,
|
| 180 |
-
probability: torch.Tensor,
|
| 181 |
-
gradient: torch.Tensor,
|
| 182 |
-
) -> None:
|
| 183 |
-
if not pair_key.numel():
|
| 184 |
-
return
|
| 185 |
-
utility = -probability.detach().float() * gradient.detach().float()
|
| 186 |
-
finite = torch.isfinite(utility)
|
| 187 |
-
if not bool(finite.any()):
|
| 188 |
-
return
|
| 189 |
-
pair_key = pair_key.detach()[finite].to(torch.long)
|
| 190 |
-
language = language.detach()[finite].to(torch.long)
|
| 191 |
-
probability = probability.detach()[finite].float()
|
| 192 |
-
utility = utility[finite]
|
| 193 |
-
self.observed_occurrences += int(pair_key.numel())
|
| 194 |
-
|
| 195 |
-
composite = pair_key * self.n_languages + language
|
| 196 |
-
unique, inverse = torch.unique(composite, return_inverse=True)
|
| 197 |
-
utility_sum = torch.zeros(unique.numel(), device=utility.device)
|
| 198 |
-
probability_sum = torch.zeros(unique.numel(), device=utility.device)
|
| 199 |
-
count = torch.zeros(unique.numel(), dtype=torch.long, device=utility.device)
|
| 200 |
-
utility_sum.index_add_(0, inverse, utility)
|
| 201 |
-
probability_sum.index_add_(0, inverse, probability)
|
| 202 |
-
count.index_add_(0, inverse, torch.ones_like(inverse))
|
| 203 |
-
|
| 204 |
-
self.unique_records_before_transfer += int(unique.numel())
|
| 205 |
-
keep = min(self.top_per_forward, unique.numel())
|
| 206 |
-
if keep < unique.numel():
|
| 207 |
-
self.dropped_by_top_per_forward += int(unique.numel() - keep)
|
| 208 |
-
chosen = torch.topk(utility_sum.abs(), keep, sorted=False).indices
|
| 209 |
-
unique = unique[chosen]
|
| 210 |
-
utility_sum = utility_sum[chosen]
|
| 211 |
-
probability_sum = probability_sum[chosen]
|
| 212 |
-
count = count[chosen]
|
| 213 |
-
|
| 214 |
-
unique_cpu = unique.cpu()
|
| 215 |
-
utility_cpu = utility_sum.cpu()
|
| 216 |
-
admission_estimates = self._update_utility_sketch(unique_cpu, utility_cpu)
|
| 217 |
-
keys = unique_cpu.tolist()
|
| 218 |
-
utilities = utility_cpu.tolist()
|
| 219 |
-
probabilities = probability_sum.cpu().tolist()
|
| 220 |
-
counts = count.cpu().tolist()
|
| 221 |
-
self.transferred_records += len(keys)
|
| 222 |
-
incoming = {
|
| 223 |
-
int(key): [float(value), float(p_sum), int(n)]
|
| 224 |
-
for key, value, p_sum, n in zip(
|
| 225 |
-
keys, utilities, probabilities, counts, strict=True
|
| 226 |
-
)
|
| 227 |
-
}
|
| 228 |
-
admission = {
|
| 229 |
-
int(key): float(estimate)
|
| 230 |
-
for key, estimate in zip(keys, admission_estimates.tolist(), strict=True)
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
# First update retained rows, then consider unseen rows in descending
|
| 234 |
-
# batch rank. This makes replacement deterministic and independent of
|
| 235 |
-
# the device order returned by top-k.
|
| 236 |
-
retained_keys = sorted(key for key in incoming if key in self.records)
|
| 237 |
-
self.retained_updates += len(retained_keys)
|
| 238 |
-
for key in retained_keys:
|
| 239 |
-
value, p_sum, n = incoming.pop(key)
|
| 240 |
-
row = self.records[key]
|
| 241 |
-
row[0] = float(row[0]) + float(value)
|
| 242 |
-
row[1] = float(row[1]) + float(p_sum)
|
| 243 |
-
row[2] = int(row[2]) + int(n)
|
| 244 |
-
self._push_heap(key)
|
| 245 |
-
|
| 246 |
-
unseen = sorted(
|
| 247 |
-
incoming.items(),
|
| 248 |
-
key=lambda item: (admission[item[0]], int(item[1][2]), -item[0]),
|
| 249 |
-
reverse=True,
|
| 250 |
-
)
|
| 251 |
-
for key, row in unseen:
|
| 252 |
-
if len(self.records) < self.candidate_capacity:
|
| 253 |
-
self.records[key] = row
|
| 254 |
-
self._push_heap(key)
|
| 255 |
-
self.admitted_into_free_slots += 1
|
| 256 |
-
continue
|
| 257 |
-
self._discard_stale_heap_entries()
|
| 258 |
-
if not self._heap:
|
| 259 |
-
self._rebuild_heap()
|
| 260 |
-
admission_rank = (admission[key], int(row[2]), -key)
|
| 261 |
-
if admission_rank <= self._heap[0][:3]:
|
| 262 |
-
self.capacity_rejections += 1
|
| 263 |
-
continue
|
| 264 |
-
victim = heapq.heappop(self._heap)[3]
|
| 265 |
-
del self.records[victim]
|
| 266 |
-
self.records[key] = row
|
| 267 |
-
self._push_heap(key)
|
| 268 |
-
self.capacity_replacements += 1
|
| 269 |
-
|
| 270 |
-
# The retained dictionary never exceeds candidate_capacity. Lazy heap
|
| 271 |
-
# metadata is also bounded: after a call it has at most 2x as many
|
| 272 |
-
# entries as the registered candidate table.
|
| 273 |
-
if len(self._heap) > 2 * self.candidate_capacity:
|
| 274 |
-
self._rebuild_heap()
|
| 275 |
-
|
| 276 |
-
def _prune(self) -> None:
|
| 277 |
-
ranked = sorted(
|
| 278 |
-
self.records.items(),
|
| 279 |
-
key=lambda item: self._rank(item[0], item[1]),
|
| 280 |
-
reverse=True,
|
| 281 |
-
)[: self.candidate_capacity]
|
| 282 |
-
self.records = dict(ranked)
|
| 283 |
-
self._rebuild_heap()
|
| 284 |
-
|
| 285 |
-
def snapshot_records(self) -> tuple[dict[str, float | int], ...]:
|
| 286 |
-
"""Return the complete bounded pair/language table deterministically."""
|
| 287 |
-
|
| 288 |
-
self._prune()
|
| 289 |
-
rows = []
|
| 290 |
-
for composite, row in sorted(self.records.items()):
|
| 291 |
-
pair_key, language = divmod(int(composite), self.n_languages)
|
| 292 |
-
utility, probability_sum, count = float(row[0]), float(row[1]), int(row[2])
|
| 293 |
-
rows.append({
|
| 294 |
-
"composite_key": int(composite),
|
| 295 |
-
"pair_key": pair_key,
|
| 296 |
-
"language_index": language,
|
| 297 |
-
"utility": utility,
|
| 298 |
-
"probability_sum": probability_sum,
|
| 299 |
-
"captured_support": count,
|
| 300 |
-
"mean_probability": probability_sum / count if count else 0.0,
|
| 301 |
-
})
|
| 302 |
-
return tuple(rows)
|
| 303 |
-
|
| 304 |
-
def state_dict(self) -> dict[str, Any]:
|
| 305 |
-
"""Return the complete bounded discovery state for an exact resume."""
|
| 306 |
-
|
| 307 |
-
self._prune()
|
| 308 |
-
return {
|
| 309 |
-
"version": 2,
|
| 310 |
-
"n_languages": self.n_languages,
|
| 311 |
-
"candidate_capacity": self.candidate_capacity,
|
| 312 |
-
"top_per_forward": self.top_per_forward,
|
| 313 |
-
"sketch_width": self._sketch_width,
|
| 314 |
-
"records": {
|
| 315 |
-
int(key): [float(row[0]), float(row[1]), int(row[2])]
|
| 316 |
-
for key, row in sorted(self.records.items())
|
| 317 |
-
},
|
| 318 |
-
"utility_sketch": self._utility_sketch.clone(),
|
| 319 |
-
"observed_occurrences": self.observed_occurrences,
|
| 320 |
-
"transferred_records": self.transferred_records,
|
| 321 |
-
"admission_pressure": self.admission_pressure(),
|
| 322 |
-
}
|
| 323 |
-
|
| 324 |
-
def load_state_dict(self, raw: dict[str, Any]) -> None:
|
| 325 |
-
"""Restore a state produced by :meth:`state_dict` after validation."""
|
| 326 |
-
|
| 327 |
-
version = int(raw.get("version", -1))
|
| 328 |
-
if version not in (1, 2):
|
| 329 |
-
raise ValueError("unsupported discovery state version")
|
| 330 |
-
if int(raw.get("n_languages", -1)) != self.n_languages:
|
| 331 |
-
raise ValueError("discovery state language count does not match")
|
| 332 |
-
if int(raw.get("candidate_capacity", -1)) != self.candidate_capacity:
|
| 333 |
-
raise ValueError("discovery state candidate capacity does not match")
|
| 334 |
-
if int(raw.get("top_per_forward", -1)) != self.top_per_forward:
|
| 335 |
-
raise ValueError("discovery state top-per-forward does not match")
|
| 336 |
-
if int(raw.get("sketch_width", -1)) != self._sketch_width:
|
| 337 |
-
raise ValueError("discovery state sketch width does not match")
|
| 338 |
-
sketch = raw.get("utility_sketch")
|
| 339 |
-
if (
|
| 340 |
-
not torch.is_tensor(sketch)
|
| 341 |
-
or sketch.dtype != torch.float64
|
| 342 |
-
or tuple(sketch.shape) != tuple(self._utility_sketch.shape)
|
| 343 |
-
or not bool(torch.isfinite(sketch).all())
|
| 344 |
-
):
|
| 345 |
-
raise ValueError("discovery state utility sketch is invalid")
|
| 346 |
-
raw_records = raw.get("records")
|
| 347 |
-
if not isinstance(raw_records, dict) or len(raw_records) > self.candidate_capacity:
|
| 348 |
-
raise ValueError("discovery state records are invalid")
|
| 349 |
-
records: dict[int, list[float | int]] = {}
|
| 350 |
-
for raw_key, raw_row in raw_records.items():
|
| 351 |
-
key = int(raw_key)
|
| 352 |
-
if key < 0 or not isinstance(raw_row, (list, tuple)) or len(raw_row) != 3:
|
| 353 |
-
raise ValueError("discovery state record is invalid")
|
| 354 |
-
utility, probability, count = float(raw_row[0]), float(raw_row[1]), int(raw_row[2])
|
| 355 |
-
if not (math.isfinite(utility) and math.isfinite(probability)) or count < 0:
|
| 356 |
-
raise ValueError("discovery state record values are invalid")
|
| 357 |
-
records[key] = [utility, probability, count]
|
| 358 |
-
observed = int(raw.get("observed_occurrences", -1))
|
| 359 |
-
transferred = int(raw.get("transferred_records", -1))
|
| 360 |
-
if observed < 0 or transferred < 0:
|
| 361 |
-
raise ValueError("discovery state counters are invalid")
|
| 362 |
-
counter_names = tuple(self.admission_pressure())
|
| 363 |
-
raw_pressure = raw.get("admission_pressure", {})
|
| 364 |
-
if version == 2 and (
|
| 365 |
-
not isinstance(raw_pressure, dict)
|
| 366 |
-
or set(raw_pressure) != set(counter_names)
|
| 367 |
-
):
|
| 368 |
-
raise ValueError("discovery state admission counters are invalid")
|
| 369 |
-
pressure = {
|
| 370 |
-
name: int(raw_pressure.get(name, 0)) for name in counter_names
|
| 371 |
-
}
|
| 372 |
-
if any(value < 0 for value in pressure.values()):
|
| 373 |
-
raise ValueError("discovery state admission counters are invalid")
|
| 374 |
-
|
| 375 |
-
self.records = records
|
| 376 |
-
self._utility_sketch.copy_(sketch)
|
| 377 |
-
self.observed_occurrences = observed
|
| 378 |
-
self.transferred_records = transferred
|
| 379 |
-
for name, value in pressure.items():
|
| 380 |
-
setattr(self, name, value)
|
| 381 |
-
self._rebuild_heap()
|
| 382 |
-
|
| 383 |
-
def admission_pressure(self) -> dict[str, int]:
|
| 384 |
-
return {
|
| 385 |
-
"unique_records_before_transfer": self.unique_records_before_transfer,
|
| 386 |
-
"dropped_by_top_per_forward": self.dropped_by_top_per_forward,
|
| 387 |
-
"retained_updates": self.retained_updates,
|
| 388 |
-
"admitted_into_free_slots": self.admitted_into_free_slots,
|
| 389 |
-
"capacity_replacements": self.capacity_replacements,
|
| 390 |
-
"capacity_rejections": self.capacity_rejections,
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
def promote(
|
| 395 |
-
self,
|
| 396 |
-
*,
|
| 397 |
-
budget: int,
|
| 398 |
-
min_support: int,
|
| 399 |
-
per_language_quota: int,
|
| 400 |
-
per_language_min_support: int,
|
| 401 |
-
min_utility: float = 0.0,
|
| 402 |
-
) -> PromotedVocabulary:
|
| 403 |
-
"""Select utility-thresholded exact rows under a safety L0 ceiling."""
|
| 404 |
-
|
| 405 |
-
if budget <= 0:
|
| 406 |
-
raise ValueError("budget must be positive")
|
| 407 |
-
if min_support <= 0 or per_language_min_support <= 0:
|
| 408 |
-
raise ValueError("support thresholds must be positive")
|
| 409 |
-
if per_language_quota < 0:
|
| 410 |
-
raise ValueError("per_language_quota must be non-negative")
|
| 411 |
-
if not math.isfinite(min_utility) or min_utility < 0:
|
| 412 |
-
raise ValueError("min_utility must be finite and non-negative")
|
| 413 |
-
self._prune()
|
| 414 |
-
|
| 415 |
-
by_pair: dict[int, dict[str, Any]] = defaultdict(
|
| 416 |
-
lambda: {"utility": 0.0, "p_sum": 0.0, "count": 0, "languages": {}}
|
| 417 |
-
)
|
| 418 |
-
for composite, row in self.records.items():
|
| 419 |
-
pair_key, language = divmod(composite, self.n_languages)
|
| 420 |
-
utility, p_sum, count = float(row[0]), float(row[1]), int(row[2])
|
| 421 |
-
target = by_pair[pair_key]
|
| 422 |
-
target["utility"] += utility
|
| 423 |
-
target["p_sum"] += p_sum
|
| 424 |
-
target["count"] += count
|
| 425 |
-
target["languages"][language] = {
|
| 426 |
-
"utility": utility,
|
| 427 |
-
"p_sum": p_sum,
|
| 428 |
-
"count": count,
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
selected: set[int] = set()
|
| 432 |
-
reservation_languages: dict[int, set[int]] = defaultdict(set)
|
| 433 |
-
if per_language_quota:
|
| 434 |
-
candidates_by_language: list[list[tuple[float, int, int, int]]] = [
|
| 435 |
-
[] for _ in range(self.n_languages)
|
| 436 |
-
]
|
| 437 |
-
for pair_key, row in by_pair.items():
|
| 438 |
-
# A language reservation protects multilingual coverage, but
|
| 439 |
-
# it cannot override the registered global U(q) > 0
|
| 440 |
-
# eligibility constraint. Build the per-language lists in
|
| 441 |
-
# one pass over observed records; scanning every pair once per
|
| 442 |
-
# marker is prohibitive for the 1,914-language run.
|
| 443 |
-
if row["utility"] <= 0 or row["utility"] < min_utility:
|
| 444 |
-
continue
|
| 445 |
-
for language, language_row in row["languages"].items():
|
| 446 |
-
if (language_row["utility"] > 0
|
| 447 |
-
and language_row["count"] >= per_language_min_support):
|
| 448 |
-
candidates_by_language[language].append((
|
| 449 |
-
float(language_row["utility"]),
|
| 450 |
-
int(language_row["count"]),
|
| 451 |
-
-int(pair_key),
|
| 452 |
-
int(pair_key),
|
| 453 |
-
))
|
| 454 |
-
for language, candidates in enumerate(candidates_by_language):
|
| 455 |
-
candidates.sort(reverse=True)
|
| 456 |
-
for candidate in candidates[:per_language_quota]:
|
| 457 |
-
pair_key = candidate[3]
|
| 458 |
-
selected.add(pair_key)
|
| 459 |
-
reservation_languages[pair_key].add(language)
|
| 460 |
-
|
| 461 |
-
if len(selected) > budget:
|
| 462 |
-
selected = set(sorted(
|
| 463 |
-
selected,
|
| 464 |
-
key=lambda key: (
|
| 465 |
-
float(by_pair[key]["utility"]),
|
| 466 |
-
int(by_pair[key]["count"]),
|
| 467 |
-
-key,
|
| 468 |
-
),
|
| 469 |
-
reverse=True,
|
| 470 |
-
)[:budget])
|
| 471 |
-
reservation_languages = {
|
| 472 |
-
key: languages for key, languages in reservation_languages.items()
|
| 473 |
-
if key in selected
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
global_candidates = [
|
| 477 |
-
key for key, row in by_pair.items()
|
| 478 |
-
if (
|
| 479 |
-
row["utility"] > 0
|
| 480 |
-
and row["utility"] >= min_utility
|
| 481 |
-
and row["count"] >= min_support
|
| 482 |
-
and key not in selected
|
| 483 |
-
)
|
| 484 |
-
]
|
| 485 |
-
global_candidates.sort(
|
| 486 |
-
key=lambda key: (
|
| 487 |
-
float(by_pair[key]["utility"]),
|
| 488 |
-
int(by_pair[key]["count"]),
|
| 489 |
-
-key,
|
| 490 |
-
),
|
| 491 |
-
reverse=True,
|
| 492 |
-
)
|
| 493 |
-
selected.update(global_candidates[: max(0, budget - len(selected))])
|
| 494 |
-
|
| 495 |
-
pair_keys = tuple(sorted(selected))
|
| 496 |
-
rows = []
|
| 497 |
-
for pair_key in pair_keys:
|
| 498 |
-
row = by_pair[pair_key]
|
| 499 |
-
reserved = sorted(reservation_languages.get(pair_key, ()))
|
| 500 |
-
rows.append({
|
| 501 |
-
"pair_key": pair_key,
|
| 502 |
-
"utility": float(row["utility"]),
|
| 503 |
-
"captured_support": int(row["count"]),
|
| 504 |
-
"mean_probability": (
|
| 505 |
-
float(row["p_sum"]) / int(row["count"]) if row["count"] else 0.0
|
| 506 |
-
),
|
| 507 |
-
"language_count": len(row["languages"]),
|
| 508 |
-
"reservation_language_count": len(reserved),
|
| 509 |
-
"max_reservation_support": max(
|
| 510 |
-
(int(row["languages"][language]["count"]) for language in reserved),
|
| 511 |
-
default=0,
|
| 512 |
-
),
|
| 513 |
-
"global_support_eligible": int(row["count"]) >= min_support,
|
| 514 |
-
})
|
| 515 |
-
return PromotedVocabulary(
|
| 516 |
-
pair_keys=pair_keys,
|
| 517 |
-
records=tuple(rows),
|
| 518 |
-
candidate_count=len(by_pair),
|
| 519 |
-
reservation_memberships=tuple(sorted(
|
| 520 |
-
(pair_key, language)
|
| 521 |
-
for pair_key, languages in reservation_languages.items()
|
| 522 |
-
for language in languages
|
| 523 |
-
)),
|
| 524 |
-
)
|
| 525 |
-
|
| 526 |
-
def audit_promotion(
|
| 527 |
-
self,
|
| 528 |
-
promoted: PromotedVocabulary,
|
| 529 |
-
*,
|
| 530 |
-
budget: int,
|
| 531 |
-
min_support: int,
|
| 532 |
-
per_language_quota: int,
|
| 533 |
-
per_language_min_support: int,
|
| 534 |
-
min_utility: float = 0.0,
|
| 535 |
-
reservation_audit_quota: int | None = None,
|
| 536 |
-
trainable_language_indices: tuple[int, ...] | None = None,
|
| 537 |
-
) -> dict[str, Any]:
|
| 538 |
-
"""Compare a quota selection with the same discovery under quota zero."""
|
| 539 |
-
|
| 540 |
-
if reservation_audit_quota is None:
|
| 541 |
-
reservation_audit_quota = per_language_quota
|
| 542 |
-
if reservation_audit_quota < 0:
|
| 543 |
-
raise ValueError("reservation_audit_quota must be non-negative")
|
| 544 |
-
if trainable_language_indices is None:
|
| 545 |
-
trainable_language_indices = tuple(range(self.n_languages))
|
| 546 |
-
else:
|
| 547 |
-
trainable_language_indices = tuple(
|
| 548 |
-
int(index) for index in trainable_language_indices
|
| 549 |
-
)
|
| 550 |
-
if (
|
| 551 |
-
not trainable_language_indices
|
| 552 |
-
or len(set(trainable_language_indices)) != len(trainable_language_indices)
|
| 553 |
-
or any(
|
| 554 |
-
index < 0 or index >= self.n_languages
|
| 555 |
-
for index in trainable_language_indices
|
| 556 |
-
)
|
| 557 |
-
):
|
| 558 |
-
raise ValueError("trainable language indices are invalid")
|
| 559 |
-
trainable_set = set(trainable_language_indices)
|
| 560 |
-
global_only = (
|
| 561 |
-
promoted
|
| 562 |
-
if per_language_quota == 0
|
| 563 |
-
else self.promote(
|
| 564 |
-
budget=budget,
|
| 565 |
-
min_support=min_support,
|
| 566 |
-
per_language_quota=0,
|
| 567 |
-
per_language_min_support=per_language_min_support,
|
| 568 |
-
min_utility=min_utility,
|
| 569 |
-
)
|
| 570 |
-
)
|
| 571 |
-
reservation_selection = (
|
| 572 |
-
promoted
|
| 573 |
-
if per_language_quota == reservation_audit_quota
|
| 574 |
-
else self.promote(
|
| 575 |
-
budget=budget,
|
| 576 |
-
min_support=min_support,
|
| 577 |
-
per_language_quota=reservation_audit_quota,
|
| 578 |
-
per_language_min_support=per_language_min_support,
|
| 579 |
-
min_utility=min_utility,
|
| 580 |
-
)
|
| 581 |
-
)
|
| 582 |
-
all_candidate_counts = [0] * self.n_languages
|
| 583 |
-
aggregate_utility: dict[int, float] = defaultdict(float)
|
| 584 |
-
for composite, row in self.records.items():
|
| 585 |
-
pair_key, language = divmod(int(composite), self.n_languages)
|
| 586 |
-
if language not in trainable_set:
|
| 587 |
-
raise ValueError(
|
| 588 |
-
"discovery contains a nontrainable language candidate"
|
| 589 |
-
)
|
| 590 |
-
all_candidate_counts[language] += 1
|
| 591 |
-
aggregate_utility[pair_key] += float(row[0])
|
| 592 |
-
all_eligible_counts = [0] * self.n_languages
|
| 593 |
-
for composite, row in self.records.items():
|
| 594 |
-
pair_key, language = divmod(int(composite), self.n_languages)
|
| 595 |
-
if (
|
| 596 |
-
aggregate_utility[pair_key] > 0
|
| 597 |
-
and aggregate_utility[pair_key] >= min_utility
|
| 598 |
-
and float(row[0]) > 0
|
| 599 |
-
and int(row[2]) >= per_language_min_support
|
| 600 |
-
):
|
| 601 |
-
all_eligible_counts[language] += 1
|
| 602 |
-
candidate_counts = [
|
| 603 |
-
all_candidate_counts[index] for index in trainable_language_indices
|
| 604 |
-
]
|
| 605 |
-
eligible_counts = [
|
| 606 |
-
all_eligible_counts[index] for index in trainable_language_indices
|
| 607 |
-
]
|
| 608 |
-
all_reservation_counts = [0] * self.n_languages
|
| 609 |
-
for _, language in reservation_selection.reservation_memberships:
|
| 610 |
-
if language not in trainable_set:
|
| 611 |
-
raise ValueError("reservation selected a nontrainable language")
|
| 612 |
-
all_reservation_counts[language] += 1
|
| 613 |
-
reservation_counts = [
|
| 614 |
-
all_reservation_counts[index] for index in trainable_language_indices
|
| 615 |
-
]
|
| 616 |
-
|
| 617 |
-
reservation_utility = math.fsum(
|
| 618 |
-
float(record["utility"]) for record in reservation_selection.records
|
| 619 |
-
)
|
| 620 |
-
global_utility = math.fsum(
|
| 621 |
-
float(record["utility"]) for record in global_only.records
|
| 622 |
-
)
|
| 623 |
-
utility_displaced = global_utility - reservation_utility
|
| 624 |
-
membership_capacity = reservation_audit_quota * len(trainable_language_indices)
|
| 625 |
-
reservation_keys = set(reservation_selection.pair_keys)
|
| 626 |
-
global_keys = set(global_only.pair_keys)
|
| 627 |
-
return {
|
| 628 |
-
"deployed_selector": {
|
| 629 |
-
"per_language_quota": per_language_quota,
|
| 630 |
-
"matches_global_only": promoted.pair_keys == global_only.pair_keys,
|
| 631 |
-
"reservation_audit_quota": reservation_audit_quota,
|
| 632 |
-
},
|
| 633 |
-
"candidate_table": {
|
| 634 |
-
"capacity": self.candidate_capacity,
|
| 635 |
-
"pair_language_records": len(self.records),
|
| 636 |
-
"saturation": len(self.records) / self.candidate_capacity,
|
| 637 |
-
"candidate_pairs": promoted.candidate_count,
|
| 638 |
-
"initializer_languages_total": self.n_languages,
|
| 639 |
-
"languages_total": len(trainable_language_indices),
|
| 640 |
-
"trainable_language_indices": list(trainable_language_indices),
|
| 641 |
-
"languages_with_candidates": sum(
|
| 642 |
-
count > 0 for count in candidate_counts
|
| 643 |
-
),
|
| 644 |
-
"records_per_language": candidate_counts,
|
| 645 |
-
"reservation_eligible_records_per_language": eligible_counts,
|
| 646 |
-
"min_records_per_language": min(candidate_counts),
|
| 647 |
-
"median_records_per_language": statistics.median(candidate_counts),
|
| 648 |
-
"max_records_per_language": max(candidate_counts),
|
| 649 |
-
"admission_pressure": {
|
| 650 |
-
**{
|
| 651 |
-
"unique_records_before_transfer": (
|
| 652 |
-
self.unique_records_before_transfer
|
| 653 |
-
),
|
| 654 |
-
"dropped_by_top_per_forward": (
|
| 655 |
-
self.dropped_by_top_per_forward
|
| 656 |
-
),
|
| 657 |
-
"transferred_records": self.transferred_records,
|
| 658 |
-
},
|
| 659 |
-
"retained_updates": self.retained_updates,
|
| 660 |
-
"admitted_into_free_slots": self.admitted_into_free_slots,
|
| 661 |
-
"capacity_replacements": self.capacity_replacements,
|
| 662 |
-
"capacity_rejections": self.capacity_rejections,
|
| 663 |
-
},
|
| 664 |
-
},
|
| 665 |
-
"reservation": {
|
| 666 |
-
"quota_per_language": reservation_audit_quota,
|
| 667 |
-
"membership_capacity": membership_capacity,
|
| 668 |
-
"memberships": len(reservation_selection.reservation_memberships),
|
| 669 |
-
"membership_occupancy": (
|
| 670 |
-
len(reservation_selection.reservation_memberships)
|
| 671 |
-
/ membership_capacity
|
| 672 |
-
if membership_capacity
|
| 673 |
-
else 0.0
|
| 674 |
-
),
|
| 675 |
-
"unique_reserved_rows": len({
|
| 676 |
-
pair_key
|
| 677 |
-
for pair_key, _ in reservation_selection.reservation_memberships
|
| 678 |
-
}),
|
| 679 |
-
"languages_with_reservations": sum(
|
| 680 |
-
count > 0 for count in reservation_counts
|
| 681 |
-
),
|
| 682 |
-
"memberships_per_language": reservation_counts,
|
| 683 |
-
},
|
| 684 |
-
"global_only_counterfactual": {
|
| 685 |
-
"global_selected_rows": len(global_only.pair_keys),
|
| 686 |
-
"global_selected_utility": global_utility,
|
| 687 |
-
"reservation_selected_rows": len(reservation_selection.pair_keys),
|
| 688 |
-
"reservation_selected_utility": reservation_utility,
|
| 689 |
-
"utility_displaced_by_reservations": utility_displaced,
|
| 690 |
-
"relative_utility_displaced_by_reservations": (
|
| 691 |
-
utility_displaced / global_utility if global_utility else None
|
| 692 |
-
),
|
| 693 |
-
"reservation_only_rows": len(reservation_keys - global_keys),
|
| 694 |
-
"global_only_rows": len(global_keys - reservation_keys),
|
| 695 |
-
},
|
| 696 |
-
}
|
| 697 |
-
|
| 698 |
-
def select_external(
|
| 699 |
-
self,
|
| 700 |
-
pair_keys: tuple[int, ...] | list[int],
|
| 701 |
-
*,
|
| 702 |
-
minimum_support: int,
|
| 703 |
-
) -> PromotedVocabulary:
|
| 704 |
-
"""Validate and materialize a hash-pinned external promotion decision."""
|
| 705 |
-
|
| 706 |
-
if minimum_support <= 0:
|
| 707 |
-
raise ValueError("external-selection minimum support must be positive")
|
| 708 |
-
keys = tuple(int(key) for key in pair_keys)
|
| 709 |
-
if not keys or tuple(sorted(set(keys))) != keys:
|
| 710 |
-
raise ValueError("external pair keys must be non-empty, sorted and unique")
|
| 711 |
-
self._prune()
|
| 712 |
-
by_pair: dict[int, dict[str, Any]] = defaultdict(
|
| 713 |
-
lambda: {"utility": 0.0, "p_sum": 0.0, "count": 0, "languages": {}}
|
| 714 |
-
)
|
| 715 |
-
for composite, raw in self.records.items():
|
| 716 |
-
pair_key, language = divmod(composite, self.n_languages)
|
| 717 |
-
utility, p_sum, count = float(raw[0]), float(raw[1]), int(raw[2])
|
| 718 |
-
row = by_pair[pair_key]
|
| 719 |
-
row["utility"] += utility
|
| 720 |
-
row["p_sum"] += p_sum
|
| 721 |
-
row["count"] += count
|
| 722 |
-
row["languages"][language] = {
|
| 723 |
-
"utility": utility, "p_sum": p_sum, "count": count,
|
| 724 |
-
}
|
| 725 |
-
missing = [key for key in keys if key not in by_pair]
|
| 726 |
-
if missing:
|
| 727 |
-
raise ValueError(f"external pair keys absent from discovery: {missing[:8]}")
|
| 728 |
-
ineligible = [
|
| 729 |
-
key for key in keys
|
| 730 |
-
if by_pair[key]["utility"] <= 0 or by_pair[key]["count"] < minimum_support
|
| 731 |
-
]
|
| 732 |
-
if ineligible:
|
| 733 |
-
raise ValueError(
|
| 734 |
-
f"external pair keys fail utility/support eligibility: {ineligible[:8]}"
|
| 735 |
-
)
|
| 736 |
-
rows = []
|
| 737 |
-
for pair_key in keys:
|
| 738 |
-
row = by_pair[pair_key]
|
| 739 |
-
rows.append({
|
| 740 |
-
"pair_key": pair_key,
|
| 741 |
-
"utility": float(row["utility"]),
|
| 742 |
-
"captured_support": int(row["count"]),
|
| 743 |
-
"mean_probability": (
|
| 744 |
-
float(row["p_sum"]) / int(row["count"]) if row["count"] else 0.0
|
| 745 |
-
),
|
| 746 |
-
"language_count": len(row["languages"]),
|
| 747 |
-
"reservation_language_count": 0,
|
| 748 |
-
"max_reservation_support": 0,
|
| 749 |
-
"global_support_eligible": int(row["count"]) >= minimum_support,
|
| 750 |
-
})
|
| 751 |
-
return PromotedVocabulary(
|
| 752 |
-
pair_keys=keys,
|
| 753 |
-
records=tuple(rows),
|
| 754 |
-
candidate_count=len(by_pair),
|
| 755 |
-
reservation_memberships=(),
|
| 756 |
-
)
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
class FrequencyVocabularyDiscovery:
|
| 760 |
-
"""Independent fixed-memory heavy hitters ranked only by occurrence count.
|
| 761 |
-
|
| 762 |
-
Unlike :class:`ContrastiveVocabularyDiscovery`, this observer never reads
|
| 763 |
-
the value or sign of ``-p * dL/dp``. A Count-Min sketch sees every pair
|
| 764 |
-
occurrence, while a bounded exact key table retains the largest sketch
|
| 765 |
-
estimates. The table is pair-level rather than pair/language-level so a
|
| 766 |
-
globally frequent pair does not spend multiple candidate slots merely
|
| 767 |
-
because it occurs in several languages.
|
| 768 |
-
|
| 769 |
-
The sketch has the same depth and width rule as the utility collector. Its
|
| 770 |
-
estimates can over-count because of sketch collisions; they cannot
|
| 771 |
-
under-count. This is an independent fixed-memory frequency selector, not
|
| 772 |
-
an exact unbounded corpus histogram.
|
| 773 |
-
"""
|
| 774 |
-
|
| 775 |
-
admission_mode = "frequency"
|
| 776 |
-
|
| 777 |
-
def __init__(
|
| 778 |
-
self,
|
| 779 |
-
*,
|
| 780 |
-
n_languages: int,
|
| 781 |
-
candidate_capacity: int,
|
| 782 |
-
top_per_forward: int,
|
| 783 |
-
) -> None:
|
| 784 |
-
if n_languages <= 0:
|
| 785 |
-
raise ValueError("n_languages must be positive")
|
| 786 |
-
if candidate_capacity <= 0:
|
| 787 |
-
raise ValueError("candidate_capacity must be positive")
|
| 788 |
-
if top_per_forward <= 0:
|
| 789 |
-
raise ValueError("top_per_forward must be positive")
|
| 790 |
-
self.n_languages = int(n_languages)
|
| 791 |
-
self.candidate_capacity = int(candidate_capacity)
|
| 792 |
-
self.top_per_forward = int(top_per_forward)
|
| 793 |
-
# pair key -> [unused utility, unused probability sum, support estimate]
|
| 794 |
-
self.records: dict[int, list[float | int]] = {}
|
| 795 |
-
self._sketch_width = max(64, self.candidate_capacity)
|
| 796 |
-
self._frequency_sketch = torch.zeros(
|
| 797 |
-
(len(_SKETCH_A), self._sketch_width), dtype=torch.long
|
| 798 |
-
)
|
| 799 |
-
self._heap: list[tuple[int, int, int]] = []
|
| 800 |
-
self.observed_occurrences = 0
|
| 801 |
-
self.transferred_records = 0
|
| 802 |
-
self.unique_records_before_transfer = 0
|
| 803 |
-
self.dropped_by_top_per_forward = 0
|
| 804 |
-
self.retained_updates = 0
|
| 805 |
-
self.admitted_into_free_slots = 0
|
| 806 |
-
self.capacity_replacements = 0
|
| 807 |
-
self.capacity_rejections = 0
|
| 808 |
-
|
| 809 |
-
@staticmethod
|
| 810 |
-
def _rank(key: int, row: list[float | int]) -> tuple[int, int]:
|
| 811 |
-
return int(row[2]), -int(key)
|
| 812 |
-
|
| 813 |
-
def _push_heap(self, key: int) -> None:
|
| 814 |
-
heapq.heappush(self._heap, (*self._rank(key, self.records[key]), int(key)))
|
| 815 |
-
|
| 816 |
-
def _discard_stale_heap_entries(self) -> None:
|
| 817 |
-
while self._heap:
|
| 818 |
-
support, negative_key, key = self._heap[0]
|
| 819 |
-
row = self.records.get(key)
|
| 820 |
-
if row is not None and (support, negative_key) == self._rank(key, row):
|
| 821 |
-
return
|
| 822 |
-
heapq.heappop(self._heap)
|
| 823 |
-
|
| 824 |
-
def _rebuild_heap(self) -> None:
|
| 825 |
-
self._heap = [
|
| 826 |
-
(*self._rank(key, row), key) for key, row in self.records.items()
|
| 827 |
-
]
|
| 828 |
-
heapq.heapify(self._heap)
|
| 829 |
-
|
| 830 |
-
def _update_frequency_sketch(
|
| 831 |
-
self, keys: torch.Tensor, counts: torch.Tensor
|
| 832 |
-
) -> torch.Tensor:
|
| 833 |
-
"""Update Count-Min state for every key and return its current estimate."""
|
| 834 |
-
|
| 835 |
-
keys = keys.to(dtype=torch.long)
|
| 836 |
-
counts = counts.to(device=keys.device, dtype=torch.long)
|
| 837 |
-
if self._frequency_sketch.device != keys.device:
|
| 838 |
-
self._frequency_sketch = self._frequency_sketch.to(keys.device)
|
| 839 |
-
estimates = []
|
| 840 |
-
for depth, (multiplier, offset) in enumerate(
|
| 841 |
-
zip(_SKETCH_A, _SKETCH_B, strict=True)
|
| 842 |
-
):
|
| 843 |
-
bucket = torch.remainder(
|
| 844 |
-
keys * multiplier + offset, self._sketch_width
|
| 845 |
-
)
|
| 846 |
-
self._frequency_sketch[depth].index_add_(0, bucket, counts)
|
| 847 |
-
estimates.append(self._frequency_sketch[depth, bucket])
|
| 848 |
-
return torch.stack(estimates).amin(dim=0)
|
| 849 |
-
|
| 850 |
-
@staticmethod
|
| 851 |
-
def _largest_with_deterministic_boundary(
|
| 852 |
-
keys: torch.Tensor, estimates: torch.Tensor, keep: int
|
| 853 |
-
) -> torch.Tensor:
|
| 854 |
-
"""Keep the largest estimates, resolving the cutoff by smaller key."""
|
| 855 |
-
|
| 856 |
-
if keep >= keys.numel():
|
| 857 |
-
return torch.arange(keys.numel(), device=keys.device)
|
| 858 |
-
boundary = torch.topk(estimates, keep, sorted=False).values.min()
|
| 859 |
-
above = torch.nonzero(estimates > boundary, as_tuple=False).squeeze(1)
|
| 860 |
-
ties = torch.nonzero(estimates == boundary, as_tuple=False).squeeze(1)
|
| 861 |
-
remaining = keep - int(above.numel())
|
| 862 |
-
if remaining <= 0:
|
| 863 |
-
return above[:keep]
|
| 864 |
-
tie_choice = torch.topk(
|
| 865 |
-
-keys.index_select(0, ties), remaining, sorted=False
|
| 866 |
-
).indices
|
| 867 |
-
return torch.cat((above, ties.index_select(0, tie_choice)))
|
| 868 |
-
|
| 869 |
-
@torch.no_grad()
|
| 870 |
-
def observe(
|
| 871 |
-
self,
|
| 872 |
-
pair_key: torch.Tensor,
|
| 873 |
-
language: torch.Tensor,
|
| 874 |
-
probability: torch.Tensor,
|
| 875 |
-
gradient: torch.Tensor,
|
| 876 |
-
) -> None:
|
| 877 |
-
del language, probability, gradient
|
| 878 |
-
if not pair_key.numel():
|
| 879 |
-
return
|
| 880 |
-
pair_key = pair_key.detach().to(torch.long)
|
| 881 |
-
self.observed_occurrences += int(pair_key.numel())
|
| 882 |
-
unique, counts = torch.unique(pair_key, return_counts=True)
|
| 883 |
-
estimates = self._update_frequency_sketch(unique, counts)
|
| 884 |
-
|
| 885 |
-
self.unique_records_before_transfer += int(unique.numel())
|
| 886 |
-
keep = min(self.top_per_forward, unique.numel())
|
| 887 |
-
if keep < unique.numel():
|
| 888 |
-
self.dropped_by_top_per_forward += int(unique.numel() - keep)
|
| 889 |
-
chosen = self._largest_with_deterministic_boundary(
|
| 890 |
-
unique, estimates, keep
|
| 891 |
-
)
|
| 892 |
-
unique = unique.index_select(0, chosen)
|
| 893 |
-
estimates = estimates.index_select(0, chosen)
|
| 894 |
-
|
| 895 |
-
keys = unique.cpu().tolist()
|
| 896 |
-
supports = estimates.cpu().tolist()
|
| 897 |
-
self.transferred_records += len(keys)
|
| 898 |
-
incoming = {int(key): int(support) for key, support in zip(keys, supports)}
|
| 899 |
-
|
| 900 |
-
retained_keys = sorted(key for key in incoming if key in self.records)
|
| 901 |
-
self.retained_updates += len(retained_keys)
|
| 902 |
-
for key in retained_keys:
|
| 903 |
-
support = incoming.pop(key)
|
| 904 |
-
self.records[key][2] = max(int(self.records[key][2]), support)
|
| 905 |
-
self._push_heap(key)
|
| 906 |
-
|
| 907 |
-
unseen = sorted(
|
| 908 |
-
incoming.items(), key=lambda item: (item[1], -item[0]), reverse=True
|
| 909 |
-
)
|
| 910 |
-
for key, support in unseen:
|
| 911 |
-
row: list[float | int] = [0.0, 0.0, int(support)]
|
| 912 |
-
if len(self.records) < self.candidate_capacity:
|
| 913 |
-
self.records[key] = row
|
| 914 |
-
self._push_heap(key)
|
| 915 |
-
self.admitted_into_free_slots += 1
|
| 916 |
-
continue
|
| 917 |
-
self._discard_stale_heap_entries()
|
| 918 |
-
if not self._heap:
|
| 919 |
-
self._rebuild_heap()
|
| 920 |
-
admission_rank = (int(support), -int(key))
|
| 921 |
-
if admission_rank <= self._heap[0][:2]:
|
| 922 |
-
self.capacity_rejections += 1
|
| 923 |
-
continue
|
| 924 |
-
victim = heapq.heappop(self._heap)[2]
|
| 925 |
-
del self.records[victim]
|
| 926 |
-
self.records[key] = row
|
| 927 |
-
self._push_heap(key)
|
| 928 |
-
self.capacity_replacements += 1
|
| 929 |
-
|
| 930 |
-
if len(self._heap) > 2 * self.candidate_capacity:
|
| 931 |
-
self._rebuild_heap()
|
| 932 |
-
|
| 933 |
-
def _prune(self) -> None:
|
| 934 |
-
ranked = sorted(
|
| 935 |
-
self.records.items(),
|
| 936 |
-
key=lambda item: self._rank(item[0], item[1]),
|
| 937 |
-
reverse=True,
|
| 938 |
-
)[: self.candidate_capacity]
|
| 939 |
-
self.records = dict(ranked)
|
| 940 |
-
self._rebuild_heap()
|
| 941 |
-
|
| 942 |
-
def snapshot_records(self) -> tuple[dict[str, float | int], ...]:
|
| 943 |
-
"""Return one deterministic row per retained global pair key."""
|
| 944 |
-
|
| 945 |
-
self._prune()
|
| 946 |
-
return tuple(
|
| 947 |
-
{
|
| 948 |
-
"composite_key": int(pair_key),
|
| 949 |
-
"pair_key": int(pair_key),
|
| 950 |
-
"language_index": -1,
|
| 951 |
-
"utility": 0.0,
|
| 952 |
-
"probability_sum": 0.0,
|
| 953 |
-
"captured_support": int(row[2]),
|
| 954 |
-
"mean_probability": 0.0,
|
| 955 |
-
}
|
| 956 |
-
for pair_key, row in sorted(self.records.items())
|
| 957 |
-
)
|
| 958 |
-
|
| 959 |
-
def state_dict(self) -> dict[str, Any]:
|
| 960 |
-
"""Return the complete fixed-memory frequency state for exact resume."""
|
| 961 |
-
|
| 962 |
-
self._prune()
|
| 963 |
-
return {
|
| 964 |
-
"version": 1,
|
| 965 |
-
"protocol": "independent-frequency-count-min-v1",
|
| 966 |
-
"n_languages": self.n_languages,
|
| 967 |
-
"candidate_capacity": self.candidate_capacity,
|
| 968 |
-
"top_per_forward": self.top_per_forward,
|
| 969 |
-
"sketch_width": self._sketch_width,
|
| 970 |
-
"records": {
|
| 971 |
-
int(key): [0.0, 0.0, int(row[2])]
|
| 972 |
-
for key, row in sorted(self.records.items())
|
| 973 |
-
},
|
| 974 |
-
"frequency_sketch": self._frequency_sketch.detach().cpu().clone(),
|
| 975 |
-
"observed_occurrences": self.observed_occurrences,
|
| 976 |
-
"transferred_records": self.transferred_records,
|
| 977 |
-
"admission_pressure": self.admission_pressure(),
|
| 978 |
-
}
|
| 979 |
-
|
| 980 |
-
def load_state_dict(self, raw: dict[str, Any]) -> None:
|
| 981 |
-
"""Restore a state produced by :meth:`state_dict` after validation."""
|
| 982 |
-
|
| 983 |
-
if (
|
| 984 |
-
int(raw.get("version", -1)) != 1
|
| 985 |
-
or raw.get("protocol") != "independent-frequency-count-min-v1"
|
| 986 |
-
):
|
| 987 |
-
raise ValueError("unsupported frequency discovery state")
|
| 988 |
-
if int(raw.get("n_languages", -1)) != self.n_languages:
|
| 989 |
-
raise ValueError("frequency discovery language count does not match")
|
| 990 |
-
if int(raw.get("candidate_capacity", -1)) != self.candidate_capacity:
|
| 991 |
-
raise ValueError("frequency discovery candidate capacity does not match")
|
| 992 |
-
if int(raw.get("top_per_forward", -1)) != self.top_per_forward:
|
| 993 |
-
raise ValueError("frequency discovery top-per-forward does not match")
|
| 994 |
-
if int(raw.get("sketch_width", -1)) != self._sketch_width:
|
| 995 |
-
raise ValueError("frequency discovery sketch width does not match")
|
| 996 |
-
sketch = raw.get("frequency_sketch")
|
| 997 |
-
if (
|
| 998 |
-
not torch.is_tensor(sketch)
|
| 999 |
-
or sketch.dtype != torch.long
|
| 1000 |
-
or tuple(sketch.shape) != tuple(self._frequency_sketch.shape)
|
| 1001 |
-
or bool((sketch < 0).any())
|
| 1002 |
-
):
|
| 1003 |
-
raise ValueError("frequency discovery sketch is invalid")
|
| 1004 |
-
raw_records = raw.get("records")
|
| 1005 |
-
if not isinstance(raw_records, dict) or len(raw_records) > self.candidate_capacity:
|
| 1006 |
-
raise ValueError("frequency discovery records are invalid")
|
| 1007 |
-
records: dict[int, list[float | int]] = {}
|
| 1008 |
-
for raw_key, raw_row in raw_records.items():
|
| 1009 |
-
key = int(raw_key)
|
| 1010 |
-
if key < 0 or not isinstance(raw_row, (list, tuple)) or len(raw_row) != 3:
|
| 1011 |
-
raise ValueError("frequency discovery record is invalid")
|
| 1012 |
-
support = int(raw_row[2])
|
| 1013 |
-
if support < 0:
|
| 1014 |
-
raise ValueError("frequency discovery support is invalid")
|
| 1015 |
-
records[key] = [0.0, 0.0, support]
|
| 1016 |
-
observed = int(raw.get("observed_occurrences", -1))
|
| 1017 |
-
transferred = int(raw.get("transferred_records", -1))
|
| 1018 |
-
if observed < 0 or transferred < 0:
|
| 1019 |
-
raise ValueError("frequency discovery counters are invalid")
|
| 1020 |
-
counter_names = tuple(self.admission_pressure())
|
| 1021 |
-
pressure = raw.get("admission_pressure")
|
| 1022 |
-
if not isinstance(pressure, dict) or set(pressure) != set(counter_names):
|
| 1023 |
-
raise ValueError("frequency discovery admission counters are invalid")
|
| 1024 |
-
pressure = {name: int(pressure[name]) for name in counter_names}
|
| 1025 |
-
if any(value < 0 for value in pressure.values()):
|
| 1026 |
-
raise ValueError("frequency discovery admission counters are invalid")
|
| 1027 |
-
|
| 1028 |
-
self.records = records
|
| 1029 |
-
self._frequency_sketch = sketch.detach().cpu().clone()
|
| 1030 |
-
self.observed_occurrences = observed
|
| 1031 |
-
self.transferred_records = transferred
|
| 1032 |
-
for name, value in pressure.items():
|
| 1033 |
-
setattr(self, name, value)
|
| 1034 |
-
self._rebuild_heap()
|
| 1035 |
-
|
| 1036 |
-
def admission_pressure(self) -> dict[str, int]:
|
| 1037 |
-
return {
|
| 1038 |
-
"unique_records_before_transfer": self.unique_records_before_transfer,
|
| 1039 |
-
"dropped_by_top_per_forward": self.dropped_by_top_per_forward,
|
| 1040 |
-
"retained_updates": self.retained_updates,
|
| 1041 |
-
"admitted_into_free_slots": self.admitted_into_free_slots,
|
| 1042 |
-
"capacity_replacements": self.capacity_replacements,
|
| 1043 |
-
"capacity_rejections": self.capacity_rejections,
|
| 1044 |
-
}
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
class CollapseTable(nn.Module):
|
| 1049 |
-
def __init__(
|
| 1050 |
-
self,
|
| 1051 |
-
buckets: int,
|
| 1052 |
-
dim: int,
|
| 1053 |
-
n_languages: int,
|
| 1054 |
-
bias_init: float = -2.0,
|
| 1055 |
-
*,
|
| 1056 |
-
pair_key_base: int | None = None,
|
| 1057 |
-
pair_keys: list[int] | tuple[int, ...] | torch.Tensor | None = None,
|
| 1058 |
-
pair_slots: list[int] | tuple[int, ...] | torch.Tensor | None = None,
|
| 1059 |
-
reclaimed_token_ids: list[int] | tuple[int, ...] | torch.Tensor | None = None,
|
| 1060 |
-
residual_buckets: int = 0,
|
| 1061 |
-
) -> None:
|
| 1062 |
-
super().__init__()
|
| 1063 |
-
self.buckets = int(buckets)
|
| 1064 |
-
self.residual_buckets = int(residual_buckets)
|
| 1065 |
-
if self.residual_buckets < 0:
|
| 1066 |
-
raise ValueError("residual_buckets must be non-negative")
|
| 1067 |
-
self.pair_key_base = int(pair_key_base or 0)
|
| 1068 |
-
reclaimed = torch.as_tensor(
|
| 1069 |
-
reclaimed_token_ids if reclaimed_token_ids is not None else [], dtype=torch.long
|
| 1070 |
-
)
|
| 1071 |
-
self.merged = nn.Parameter(torch.zeros(
|
| 1072 |
-
0 if reclaimed.numel() else self.buckets, dim
|
| 1073 |
-
))
|
| 1074 |
-
self.score = nn.Parameter(torch.zeros(self.buckets))
|
| 1075 |
-
self.residual_merged = nn.Parameter(torch.zeros(self.residual_buckets, dim))
|
| 1076 |
-
self.residual_score = nn.Parameter(torch.zeros(self.residual_buckets))
|
| 1077 |
-
self.language_bias = nn.Parameter(torch.full((n_languages,), float(bias_init)))
|
| 1078 |
-
keys = torch.as_tensor(pair_keys if pair_keys is not None else [], dtype=torch.long)
|
| 1079 |
-
slots = torch.as_tensor(pair_slots if pair_slots is not None else [], dtype=torch.long)
|
| 1080 |
-
if keys.numel() != slots.numel():
|
| 1081 |
-
raise ValueError("pair_keys and pair_slots must have the same length")
|
| 1082 |
-
if keys.numel() and not bool((keys[1:] > keys[:-1]).all()):
|
| 1083 |
-
raise ValueError("pair_keys must be strictly increasing")
|
| 1084 |
-
if slots.numel() and (int(slots.min()) < 0 or int(slots.max()) >= self.buckets):
|
| 1085 |
-
raise ValueError("pair_slots must index the collapse table")
|
| 1086 |
-
if reclaimed.numel() and reclaimed.numel() != keys.numel():
|
| 1087 |
-
raise ValueError("reclaimed_token_ids must align one-to-one with pair_keys")
|
| 1088 |
-
if reclaimed.unique().numel() != reclaimed.numel():
|
| 1089 |
-
raise ValueError("reclaimed_token_ids must be unique")
|
| 1090 |
-
self.register_buffer("pair_keys", keys, persistent=True)
|
| 1091 |
-
self.register_buffer("pair_slots", slots, persistent=True)
|
| 1092 |
-
self.register_buffer("reclaimed_token_ids", reclaimed, persistent=True)
|
| 1093 |
-
self.discovery: (
|
| 1094 |
-
ContrastiveVocabularyDiscovery | FrequencyVocabularyDiscovery | None
|
| 1095 |
-
) = None
|
| 1096 |
-
self.diagnostic_discovery: (
|
| 1097 |
-
ContrastiveVocabularyDiscovery | FrequencyVocabularyDiscovery | None
|
| 1098 |
-
) = None
|
| 1099 |
-
self._vocabulary_telemetry: Any | None = None
|
| 1100 |
-
self._loss_path_scale: float | None = None
|
| 1101 |
-
self._loss_path_observer: Any | None = None
|
| 1102 |
-
|
| 1103 |
-
def extra_repr(self) -> str:
|
| 1104 |
-
return (f"buckets={self.buckets}, explicit_pairs={self.pair_keys.numel()}, "
|
| 1105 |
-
f"residual_buckets={self.residual_buckets}, "
|
| 1106 |
-
f"reclaimed_rows={self.reclaimed_token_ids.numel()}")
|
| 1107 |
-
|
| 1108 |
-
def set_vocabulary_telemetry(self, window: Any | None) -> None:
|
| 1109 |
-
"""Attach detached run telemetry without making it module state."""
|
| 1110 |
-
|
| 1111 |
-
self._vocabulary_telemetry = window
|
| 1112 |
-
|
| 1113 |
-
def begin_loss_path_pass(self, scale: float, observer: Any) -> None:
|
| 1114 |
-
"""Scale every soft pair gate and capture occurrence gradients.
|
| 1115 |
-
|
| 1116 |
-
This is intentionally transient (not module state): an audit pass may
|
| 1117 |
-
read the graph but must never alter a checkpoint or discovery table.
|
| 1118 |
-
"""
|
| 1119 |
-
|
| 1120 |
-
if self._loss_path_scale is not None or self._loss_path_observer is not None:
|
| 1121 |
-
raise RuntimeError("a loss-path pass is already active")
|
| 1122 |
-
if float(scale) not in (0.0, 0.5, 1.0):
|
| 1123 |
-
raise ValueError("registered loss-path scale must be 0, 0.5, or 1")
|
| 1124 |
-
if observer is None or not callable(observer):
|
| 1125 |
-
raise TypeError("loss-path observer must be callable")
|
| 1126 |
-
self._loss_path_scale = float(scale)
|
| 1127 |
-
self._loss_path_observer = observer
|
| 1128 |
-
|
| 1129 |
-
def end_loss_path_pass(self) -> None:
|
| 1130 |
-
if self._loss_path_scale is None or self._loss_path_observer is None:
|
| 1131 |
-
raise RuntimeError("no loss-path pass is active")
|
| 1132 |
-
self._loss_path_scale = None
|
| 1133 |
-
self._loss_path_observer = None
|
| 1134 |
-
|
| 1135 |
-
def abort_loss_path_pass(self) -> None:
|
| 1136 |
-
self._loss_path_scale = None
|
| 1137 |
-
self._loss_path_observer = None
|
| 1138 |
-
|
| 1139 |
-
def _hash(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 1140 |
-
return ((a * _MIX_A + b * _MIX_B).abs()) % self.buckets
|
| 1141 |
-
|
| 1142 |
-
def _hash_residual(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 1143 |
-
if self.residual_buckets <= 0:
|
| 1144 |
-
raise ValueError("residual hash requires a positive residual capacity")
|
| 1145 |
-
return ((a * _MIX_A + b * _MIX_B).abs()) % self.residual_buckets
|
| 1146 |
-
|
| 1147 |
-
def _exact_key(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 1148 |
-
if self.pair_key_base <= 0:
|
| 1149 |
-
raise ValueError("pair_key_base is required for exact-pair vocabulary")
|
| 1150 |
-
return a.to(torch.long) * self.pair_key_base + b.to(torch.long)
|
| 1151 |
-
|
| 1152 |
-
def configure_discovery(
|
| 1153 |
-
self,
|
| 1154 |
-
*,
|
| 1155 |
-
candidate_capacity: int,
|
| 1156 |
-
top_per_forward: int,
|
| 1157 |
-
admission_mode: str = "utility",
|
| 1158 |
-
) -> None:
|
| 1159 |
-
if self.pair_keys.numel() and not self.residual_buckets:
|
| 1160 |
-
raise ValueError("cannot discover after explicit vocabulary promotion")
|
| 1161 |
-
discovery_class = {
|
| 1162 |
-
"utility": ContrastiveVocabularyDiscovery,
|
| 1163 |
-
"frequency": FrequencyVocabularyDiscovery,
|
| 1164 |
-
}.get(str(admission_mode))
|
| 1165 |
-
if discovery_class is None:
|
| 1166 |
-
raise ValueError("discovery admission_mode must be utility or frequency")
|
| 1167 |
-
self.discovery = discovery_class(
|
| 1168 |
-
n_languages=self.language_bias.numel(),
|
| 1169 |
-
candidate_capacity=candidate_capacity,
|
| 1170 |
-
top_per_forward=top_per_forward,
|
| 1171 |
-
)
|
| 1172 |
-
|
| 1173 |
-
def configure_diagnostic_discovery(
|
| 1174 |
-
self,
|
| 1175 |
-
*,
|
| 1176 |
-
candidate_capacity: int,
|
| 1177 |
-
top_per_forward: int,
|
| 1178 |
-
admission_mode: str = "utility",
|
| 1179 |
-
) -> None:
|
| 1180 |
-
"""Attach a resettable observer which cannot affect promotion state."""
|
| 1181 |
-
|
| 1182 |
-
if self.pair_keys.numel() and not self.residual_buckets:
|
| 1183 |
-
raise ValueError("cannot diagnose discovery after explicit promotion")
|
| 1184 |
-
discovery_class = {
|
| 1185 |
-
"utility": ContrastiveVocabularyDiscovery,
|
| 1186 |
-
"frequency": FrequencyVocabularyDiscovery,
|
| 1187 |
-
}.get(str(admission_mode))
|
| 1188 |
-
if discovery_class is None:
|
| 1189 |
-
raise ValueError("discovery admission_mode must be utility or frequency")
|
| 1190 |
-
self.diagnostic_discovery = discovery_class(
|
| 1191 |
-
n_languages=self.language_bias.numel(),
|
| 1192 |
-
candidate_capacity=candidate_capacity,
|
| 1193 |
-
top_per_forward=top_per_forward,
|
| 1194 |
-
)
|
| 1195 |
-
|
| 1196 |
-
def _explicit_lookup(self, key: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 1197 |
-
if not self.pair_keys.numel():
|
| 1198 |
-
return torch.zeros_like(key, dtype=torch.bool), torch.zeros_like(key)
|
| 1199 |
-
positions = torch.searchsorted(self.pair_keys, key)
|
| 1200 |
-
safe = positions.clamp(max=self.pair_keys.numel() - 1)
|
| 1201 |
-
found = (positions < self.pair_keys.numel()) & (self.pair_keys[safe] == key)
|
| 1202 |
-
return found, self.pair_slots[safe]
|
| 1203 |
-
|
| 1204 |
-
@staticmethod
|
| 1205 |
-
def _fold_rows(value: torch.Tensor, rows: int) -> torch.Tensor:
|
| 1206 |
-
if value.ndim == 0:
|
| 1207 |
-
return value.detach().clone()
|
| 1208 |
-
if rows <= 0 or value.shape[0] < rows:
|
| 1209 |
-
raise ValueError("residual folding requires no more rows than its source")
|
| 1210 |
-
destination = torch.arange(value.shape[0], device=value.device) % rows
|
| 1211 |
-
folded = torch.zeros(
|
| 1212 |
-
(rows, *value.shape[1:]), dtype=value.dtype, device=value.device
|
| 1213 |
-
)
|
| 1214 |
-
folded.index_add_(0, destination, value)
|
| 1215 |
-
counts = torch.bincount(destination, minlength=rows).to(value.dtype)
|
| 1216 |
-
return folded / counts.reshape(rows, *([1] * (value.ndim - 1)))
|
| 1217 |
-
|
| 1218 |
-
@staticmethod
|
| 1219 |
-
def _row_state(
|
| 1220 |
-
state: dict[Any, Any],
|
| 1221 |
-
source: nn.Parameter,
|
| 1222 |
-
rows: torch.Tensor,
|
| 1223 |
-
) -> dict[Any, Any]:
|
| 1224 |
-
if not state:
|
| 1225 |
-
raise ValueError("growth requires initialized optimizer state")
|
| 1226 |
-
result: dict[Any, Any] = {}
|
| 1227 |
-
for name, value in state.items():
|
| 1228 |
-
if not torch.is_tensor(value):
|
| 1229 |
-
result[name] = value
|
| 1230 |
-
elif value.ndim == 0:
|
| 1231 |
-
result[name] = value.detach().clone()
|
| 1232 |
-
elif tuple(value.shape) == tuple(source.shape):
|
| 1233 |
-
result[name] = value.index_select(0, rows.to(value.device)).detach().clone()
|
| 1234 |
-
else:
|
| 1235 |
-
raise ValueError(f"unknown optimizer row-state shape: {name}")
|
| 1236 |
-
return result
|
| 1237 |
-
|
| 1238 |
-
@staticmethod
|
| 1239 |
-
def _concat_row_states(
|
| 1240 |
-
left: dict[Any, Any] | None,
|
| 1241 |
-
right: dict[Any, Any],
|
| 1242 |
-
order: torch.Tensor,
|
| 1243 |
-
) -> dict[Any, Any]:
|
| 1244 |
-
if left is None:
|
| 1245 |
-
return {
|
| 1246 |
-
name: (
|
| 1247 |
-
value.index_select(0, order.to(value.device))
|
| 1248 |
-
if torch.is_tensor(value) and value.ndim
|
| 1249 |
-
else value.detach().clone()
|
| 1250 |
-
if torch.is_tensor(value)
|
| 1251 |
-
else value
|
| 1252 |
-
)
|
| 1253 |
-
for name, value in right.items()
|
| 1254 |
-
}
|
| 1255 |
-
if set(left) != set(right):
|
| 1256 |
-
raise ValueError("optimizer state keys differ across growth tiers")
|
| 1257 |
-
result: dict[Any, Any] = {}
|
| 1258 |
-
for name in left:
|
| 1259 |
-
old_value, new_value = left[name], right[name]
|
| 1260 |
-
if torch.is_tensor(old_value) != torch.is_tensor(new_value):
|
| 1261 |
-
raise ValueError(f"optimizer state type differs: {name}")
|
| 1262 |
-
if not torch.is_tensor(old_value):
|
| 1263 |
-
if old_value != new_value:
|
| 1264 |
-
raise ValueError(f"optimizer scalar state differs: {name}")
|
| 1265 |
-
result[name] = old_value
|
| 1266 |
-
elif old_value.ndim == 0:
|
| 1267 |
-
if not torch.equal(old_value, new_value):
|
| 1268 |
-
raise ValueError(f"optimizer step differs: {name}")
|
| 1269 |
-
result[name] = old_value.detach().clone()
|
| 1270 |
-
else:
|
| 1271 |
-
combined = torch.cat((old_value, new_value))
|
| 1272 |
-
result[name] = combined.index_select(
|
| 1273 |
-
0, order.to(combined.device)
|
| 1274 |
-
).detach().clone()
|
| 1275 |
-
return result
|
| 1276 |
-
|
| 1277 |
-
def grow_exact_vocabulary(
|
| 1278 |
-
self,
|
| 1279 |
-
pair_keys: tuple[int, ...] | list[int],
|
| 1280 |
-
*,
|
| 1281 |
-
optimizer: torch.optim.Optimizer,
|
| 1282 |
-
residual_buckets: int,
|
| 1283 |
-
) -> dict[str, int]:
|
| 1284 |
-
"""Append exact rows while retaining a residual proposal channel."""
|
| 1285 |
-
|
| 1286 |
-
additions = torch.as_tensor(
|
| 1287 |
-
pair_keys, dtype=torch.long, device=self.merged.device
|
| 1288 |
-
)
|
| 1289 |
-
if not additions.numel():
|
| 1290 |
-
raise ValueError("growth requires at least one exact pair")
|
| 1291 |
-
if additions.numel() > 1 and not bool((additions[1:] > additions[:-1]).all()):
|
| 1292 |
-
raise ValueError("growth pair keys must be strictly increasing")
|
| 1293 |
-
if self.reclaimed_token_ids.numel():
|
| 1294 |
-
raise ValueError("reclaimed vocabulary cannot attach a residual tier")
|
| 1295 |
-
if int(residual_buckets) <= 0:
|
| 1296 |
-
raise ValueError("growth residual capacity must be positive")
|
| 1297 |
-
if self.residual_buckets and int(residual_buckets) != self.residual_buckets:
|
| 1298 |
-
raise ValueError("growth cannot resize an existing residual tier")
|
| 1299 |
-
if self.pair_keys.numel():
|
| 1300 |
-
positions = torch.searchsorted(self.pair_keys, additions)
|
| 1301 |
-
safe = positions.clamp(max=self.pair_keys.numel() - 1)
|
| 1302 |
-
if bool(
|
| 1303 |
-
((positions < self.pair_keys.numel()) & (self.pair_keys[safe] == additions)).any()
|
| 1304 |
-
):
|
| 1305 |
-
raise ValueError("growth pair already exists in the exact vocabulary")
|
| 1306 |
-
|
| 1307 |
-
old_merged, old_score = self.merged, self.score
|
| 1308 |
-
old_residual_merged, old_residual_score = (
|
| 1309 |
-
self.residual_merged,
|
| 1310 |
-
self.residual_score,
|
| 1311 |
-
)
|
| 1312 |
-
first = not self.residual_buckets
|
| 1313 |
-
tracked = (old_merged, old_score, old_residual_merged, old_residual_score)
|
| 1314 |
-
occurrences = {
|
| 1315 |
-
parameter: sum(
|
| 1316 |
-
candidate is parameter
|
| 1317 |
-
for group in optimizer.param_groups
|
| 1318 |
-
for candidate in group["params"]
|
| 1319 |
-
)
|
| 1320 |
-
for parameter in tracked
|
| 1321 |
-
}
|
| 1322 |
-
residual_occurrences = {0, 1} if first else {1}
|
| 1323 |
-
if (
|
| 1324 |
-
occurrences[old_merged] != 1
|
| 1325 |
-
or occurrences[old_score] != 1
|
| 1326 |
-
or occurrences[old_residual_merged] not in residual_occurrences
|
| 1327 |
-
or occurrences[old_residual_score] not in residual_occurrences
|
| 1328 |
-
):
|
| 1329 |
-
raise ValueError("growth parameters must occur once in the optimizer")
|
| 1330 |
-
|
| 1331 |
-
source_merged = old_merged if first else old_residual_merged
|
| 1332 |
-
source_score = old_score if first else old_residual_score
|
| 1333 |
-
source_index = (
|
| 1334 |
-
self._hash(
|
| 1335 |
-
torch.div(additions, self.pair_key_base, rounding_mode="floor"),
|
| 1336 |
-
additions.remainder(self.pair_key_base),
|
| 1337 |
-
)
|
| 1338 |
-
if first
|
| 1339 |
-
else self._hash_residual(
|
| 1340 |
-
torch.div(additions, self.pair_key_base, rounding_mode="floor"),
|
| 1341 |
-
additions.remainder(self.pair_key_base),
|
| 1342 |
-
)
|
| 1343 |
-
)
|
| 1344 |
-
source_merged_state = optimizer.state.get(source_merged, {})
|
| 1345 |
-
source_score_state = optimizer.state.get(source_score, {})
|
| 1346 |
-
added_merged_state = self._row_state(
|
| 1347 |
-
source_merged_state, source_merged, source_index
|
| 1348 |
-
)
|
| 1349 |
-
added_score_state = self._row_state(
|
| 1350 |
-
source_score_state, source_score, source_index
|
| 1351 |
-
)
|
| 1352 |
-
|
| 1353 |
-
old_keys = self.pair_keys
|
| 1354 |
-
old_slots = self.pair_slots
|
| 1355 |
-
combined_keys = torch.cat((old_keys, additions))
|
| 1356 |
-
order = torch.argsort(combined_keys, stable=True)
|
| 1357 |
-
combined_merged = torch.cat(
|
| 1358 |
-
(
|
| 1359 |
-
old_merged.index_select(0, old_slots),
|
| 1360 |
-
source_merged.index_select(0, source_index),
|
| 1361 |
-
)
|
| 1362 |
-
).index_select(0, order)
|
| 1363 |
-
combined_score = torch.cat(
|
| 1364 |
-
(
|
| 1365 |
-
old_score.index_select(0, old_slots),
|
| 1366 |
-
source_score.index_select(0, source_index),
|
| 1367 |
-
)
|
| 1368 |
-
).index_select(0, order)
|
| 1369 |
-
old_merged_state = (
|
| 1370 |
-
None
|
| 1371 |
-
if not old_keys.numel()
|
| 1372 |
-
else self._row_state(
|
| 1373 |
-
optimizer.state.get(old_merged, {}), old_merged, old_slots
|
| 1374 |
-
)
|
| 1375 |
-
)
|
| 1376 |
-
old_score_state = (
|
| 1377 |
-
None
|
| 1378 |
-
if not old_keys.numel()
|
| 1379 |
-
else self._row_state(
|
| 1380 |
-
optimizer.state.get(old_score, {}), old_score, old_slots
|
| 1381 |
-
)
|
| 1382 |
-
)
|
| 1383 |
-
new_merged_state = self._concat_row_states(
|
| 1384 |
-
old_merged_state, added_merged_state, order
|
| 1385 |
-
)
|
| 1386 |
-
new_score_state = self._concat_row_states(
|
| 1387 |
-
old_score_state, added_score_state, order
|
| 1388 |
-
)
|
| 1389 |
-
|
| 1390 |
-
if first:
|
| 1391 |
-
if source_merged.shape[0] < int(residual_buckets):
|
| 1392 |
-
raise ValueError("residual capacity exceeds the proposal table")
|
| 1393 |
-
residual_merged_value = self._fold_rows(
|
| 1394 |
-
source_merged.detach(), int(residual_buckets)
|
| 1395 |
-
)
|
| 1396 |
-
residual_score_value = self._fold_rows(
|
| 1397 |
-
source_score.detach(), int(residual_buckets)
|
| 1398 |
-
)
|
| 1399 |
-
new_residual_merged_state = {
|
| 1400 |
-
name: (
|
| 1401 |
-
self._fold_rows(value, int(residual_buckets))
|
| 1402 |
-
if torch.is_tensor(value) and value.ndim
|
| 1403 |
-
else value.detach().clone()
|
| 1404 |
-
if torch.is_tensor(value)
|
| 1405 |
-
else value
|
| 1406 |
-
)
|
| 1407 |
-
for name, value in source_merged_state.items()
|
| 1408 |
-
}
|
| 1409 |
-
new_residual_score_state = {
|
| 1410 |
-
name: (
|
| 1411 |
-
self._fold_rows(value, int(residual_buckets))
|
| 1412 |
-
if torch.is_tensor(value) and value.ndim
|
| 1413 |
-
else value.detach().clone()
|
| 1414 |
-
if torch.is_tensor(value)
|
| 1415 |
-
else value
|
| 1416 |
-
)
|
| 1417 |
-
for name, value in source_score_state.items()
|
| 1418 |
-
}
|
| 1419 |
-
else:
|
| 1420 |
-
residual_merged_value = old_residual_merged.detach().clone()
|
| 1421 |
-
residual_score_value = old_residual_score.detach().clone()
|
| 1422 |
-
new_residual_merged_state = optimizer.state[old_residual_merged]
|
| 1423 |
-
new_residual_score_state = optimizer.state[old_residual_score]
|
| 1424 |
-
|
| 1425 |
-
new_merged = nn.Parameter(
|
| 1426 |
-
combined_merged.detach().clone(), requires_grad=old_merged.requires_grad
|
| 1427 |
-
)
|
| 1428 |
-
new_score = nn.Parameter(
|
| 1429 |
-
combined_score.detach().clone(), requires_grad=old_score.requires_grad
|
| 1430 |
-
)
|
| 1431 |
-
new_residual_merged = nn.Parameter(
|
| 1432 |
-
residual_merged_value,
|
| 1433 |
-
requires_grad=source_merged.requires_grad,
|
| 1434 |
-
)
|
| 1435 |
-
new_residual_score = nn.Parameter(
|
| 1436 |
-
residual_score_value,
|
| 1437 |
-
requires_grad=source_score.requires_grad,
|
| 1438 |
-
)
|
| 1439 |
-
replacements = {
|
| 1440 |
-
old_merged: new_merged,
|
| 1441 |
-
old_score: new_score,
|
| 1442 |
-
old_residual_merged: new_residual_merged,
|
| 1443 |
-
old_residual_score: new_residual_score,
|
| 1444 |
-
}
|
| 1445 |
-
for group in optimizer.param_groups:
|
| 1446 |
-
replaced = []
|
| 1447 |
-
for parameter in group["params"]:
|
| 1448 |
-
replaced.append(replacements.get(parameter, parameter))
|
| 1449 |
-
# Resume compatibility deliberately removes zero-sized tensors
|
| 1450 |
-
# from AdamW. The first growth transition creates the real
|
| 1451 |
-
# residual tier, so attach each new parameter beside the source
|
| 1452 |
-
# parameter whose folded value and moments initialize it.
|
| 1453 |
-
if first and parameter is old_score:
|
| 1454 |
-
# Match ``CollapseTable`` registration order exactly.
|
| 1455 |
-
# Optimizer checkpoint loading binds state positionally, so
|
| 1456 |
-
# putting residual_merged beside merged would swap the
|
| 1457 |
-
# score vector and residual matrix after resume.
|
| 1458 |
-
if occurrences[old_residual_merged] == 0:
|
| 1459 |
-
replaced.append(new_residual_merged)
|
| 1460 |
-
if occurrences[old_residual_score] == 0:
|
| 1461 |
-
replaced.append(new_residual_score)
|
| 1462 |
-
group["params"] = replaced
|
| 1463 |
-
for parameter in tracked:
|
| 1464 |
-
optimizer.state.pop(parameter, None)
|
| 1465 |
-
optimizer.state[new_merged] = new_merged_state
|
| 1466 |
-
optimizer.state[new_score] = new_score_state
|
| 1467 |
-
optimizer.state[new_residual_merged] = new_residual_merged_state
|
| 1468 |
-
optimizer.state[new_residual_score] = new_residual_score_state
|
| 1469 |
-
|
| 1470 |
-
self.merged = new_merged
|
| 1471 |
-
self.score = new_score
|
| 1472 |
-
self.residual_merged = new_residual_merged
|
| 1473 |
-
self.residual_score = new_residual_score
|
| 1474 |
-
self.pair_keys = combined_keys.index_select(0, order)
|
| 1475 |
-
self.pair_slots = torch.arange(
|
| 1476 |
-
self.pair_keys.numel(), dtype=torch.long, device=self.pair_keys.device
|
| 1477 |
-
)
|
| 1478 |
-
old_exact_rows = int(old_keys.numel())
|
| 1479 |
-
self.buckets = int(self.pair_keys.numel())
|
| 1480 |
-
self.residual_buckets = int(residual_buckets)
|
| 1481 |
-
self.discovery = None
|
| 1482 |
-
self.diagnostic_discovery = None
|
| 1483 |
-
return {
|
| 1484 |
-
"old_exact_rows": old_exact_rows,
|
| 1485 |
-
"added_exact_rows": int(additions.numel()),
|
| 1486 |
-
"active_exact_rows": int(self.pair_keys.numel()),
|
| 1487 |
-
"residual_rows": self.residual_buckets,
|
| 1488 |
-
}
|
| 1489 |
-
|
| 1490 |
-
def promote_exact_vocabulary(
|
| 1491 |
-
self,
|
| 1492 |
-
pair_keys: tuple[int, ...] | list[int],
|
| 1493 |
-
*,
|
| 1494 |
-
optimizer: torch.optim.Optimizer | None = None,
|
| 1495 |
-
) -> dict[str, int]:
|
| 1496 |
-
"""Map selected exact pairs to collision-free live parameter rows."""
|
| 1497 |
-
|
| 1498 |
-
keys = torch.as_tensor(pair_keys, dtype=torch.long, device=self.merged.device)
|
| 1499 |
-
if keys.numel() and not bool((keys[1:] > keys[:-1]).all()):
|
| 1500 |
-
raise ValueError("promoted pair keys must be strictly increasing")
|
| 1501 |
-
left = torch.div(keys, self.pair_key_base, rounding_mode="floor")
|
| 1502 |
-
right = keys.remainder(self.pair_key_base)
|
| 1503 |
-
source = self._hash(left, right)
|
| 1504 |
-
|
| 1505 |
-
# Preserve each original hash row for one pair. Colliding extra pairs
|
| 1506 |
-
# receive unused rows initialized, including Adam moments, from the same
|
| 1507 |
-
# source bucket.
|
| 1508 |
-
reserved = set(int(value) for value in source.cpu().tolist())
|
| 1509 |
-
free = iter(index for index in range(self.buckets) if index not in reserved)
|
| 1510 |
-
seen: set[int] = set()
|
| 1511 |
-
slots = []
|
| 1512 |
-
copied = []
|
| 1513 |
-
for src in source.cpu().tolist():
|
| 1514 |
-
src = int(src)
|
| 1515 |
-
if src not in seen:
|
| 1516 |
-
slot = src
|
| 1517 |
-
seen.add(src)
|
| 1518 |
-
else:
|
| 1519 |
-
try:
|
| 1520 |
-
slot = next(free)
|
| 1521 |
-
except StopIteration as error:
|
| 1522 |
-
raise ValueError("not enough collapse rows for collision-free promotion") from error
|
| 1523 |
-
copied.append((src, slot))
|
| 1524 |
-
slots.append(slot)
|
| 1525 |
-
|
| 1526 |
-
with torch.no_grad():
|
| 1527 |
-
for src, dst in copied:
|
| 1528 |
-
self.merged[dst].copy_(self.merged[src])
|
| 1529 |
-
self.score[dst].copy_(self.score[src])
|
| 1530 |
-
if optimizer is not None:
|
| 1531 |
-
for parameter in (self.merged, self.score):
|
| 1532 |
-
state = optimizer.state.get(parameter, {})
|
| 1533 |
-
for value in state.values():
|
| 1534 |
-
if (torch.is_tensor(value) and value.ndim
|
| 1535 |
-
and value.shape[0] == self.buckets):
|
| 1536 |
-
for src, dst in copied:
|
| 1537 |
-
value[dst].copy_(value[src])
|
| 1538 |
-
|
| 1539 |
-
self.pair_keys = keys
|
| 1540 |
-
self.pair_slots = torch.tensor(slots, dtype=torch.long, device=self.merged.device)
|
| 1541 |
-
self.discovery = None
|
| 1542 |
-
self.diagnostic_discovery = None
|
| 1543 |
-
return {
|
| 1544 |
-
"active_pairs": int(keys.numel()),
|
| 1545 |
-
"unique_source_buckets": len(reserved),
|
| 1546 |
-
"promoted_hash_collisions": len(copied),
|
| 1547 |
-
}
|
| 1548 |
-
|
| 1549 |
-
def promote_exact_vocabulary_to_reclaimed(
|
| 1550 |
-
self,
|
| 1551 |
-
pair_keys: tuple[int, ...] | list[int],
|
| 1552 |
-
reclaimed_token_ids: tuple[int, ...] | list[int] | torch.Tensor,
|
| 1553 |
-
embedding_weight: nn.Parameter,
|
| 1554 |
-
*,
|
| 1555 |
-
optimizer: torch.optim.Optimizer,
|
| 1556 |
-
) -> dict[str, int]:
|
| 1557 |
-
"""Move a promoted exact vocabulary into existing base-table rows.
|
| 1558 |
-
|
| 1559 |
-
The transition is elementwise-equivalent to ``promote_exact_vocabulary``
|
| 1560 |
-
at the promotion boundary. AdamW moments follow the copied vectors, and
|
| 1561 |
-
the obsolete full merged parameter/state is removed before training
|
| 1562 |
-
continues.
|
| 1563 |
-
"""
|
| 1564 |
-
|
| 1565 |
-
keys = torch.as_tensor(pair_keys, dtype=torch.long, device=self.merged.device)
|
| 1566 |
-
reclaimed = torch.as_tensor(
|
| 1567 |
-
reclaimed_token_ids, dtype=torch.long, device=self.merged.device
|
| 1568 |
-
)
|
| 1569 |
-
if not keys.numel() or keys.numel() != reclaimed.numel():
|
| 1570 |
-
raise ValueError("direct reclaimed promotion needs one base row per exact pair")
|
| 1571 |
-
if not bool((keys[1:] > keys[:-1]).all()):
|
| 1572 |
-
raise ValueError("promoted pair keys must be strictly increasing")
|
| 1573 |
-
if (reclaimed.unique().numel() != reclaimed.numel()
|
| 1574 |
-
or int(reclaimed.min()) < 0
|
| 1575 |
-
or int(reclaimed.max()) >= embedding_weight.shape[0]):
|
| 1576 |
-
raise ValueError("invalid direct-promotion reclaimed token ids")
|
| 1577 |
-
if self.pair_keys.numel() or self.reclaimed_token_ids.numel():
|
| 1578 |
-
raise ValueError("direct reclaimed promotion can run only once")
|
| 1579 |
-
if optimizer is None:
|
| 1580 |
-
raise ValueError("direct reclaimed promotion requires the live optimizer")
|
| 1581 |
-
|
| 1582 |
-
left = torch.div(keys, self.pair_key_base, rounding_mode="floor")
|
| 1583 |
-
right = keys.remainder(self.pair_key_base)
|
| 1584 |
-
source = self._hash(left, right)
|
| 1585 |
-
unique_sources = int(source.unique().numel())
|
| 1586 |
-
old_merged, old_score = self.merged, self.score
|
| 1587 |
-
if tuple(old_merged.shape) != (self.buckets, embedding_weight.shape[1]):
|
| 1588 |
-
raise ValueError("direct promotion source table shape mismatch")
|
| 1589 |
-
|
| 1590 |
-
occurrences = {
|
| 1591 |
-
"merged": sum(parameter is old_merged for group in optimizer.param_groups
|
| 1592 |
-
for parameter in group["params"]),
|
| 1593 |
-
"score": sum(parameter is old_score for group in optimizer.param_groups
|
| 1594 |
-
for parameter in group["params"]),
|
| 1595 |
-
"embedding": sum(parameter is embedding_weight for group in optimizer.param_groups
|
| 1596 |
-
for parameter in group["params"]),
|
| 1597 |
-
}
|
| 1598 |
-
if occurrences != {"merged": 1, "score": 1, "embedding": 1}:
|
| 1599 |
-
raise ValueError(f"unexpected optimizer parameter membership: {occurrences}")
|
| 1600 |
-
merged_state = optimizer.state.get(old_merged)
|
| 1601 |
-
score_state = optimizer.state.get(old_score)
|
| 1602 |
-
embedding_state = optimizer.state.get(embedding_weight)
|
| 1603 |
-
if not merged_state or not score_state or not embedding_state:
|
| 1604 |
-
raise ValueError("AdamW state must exist before direct promotion")
|
| 1605 |
-
if set(merged_state) != set(score_state) or set(merged_state) != set(embedding_state):
|
| 1606 |
-
raise ValueError("optimizer state keys differ across promoted parameters")
|
| 1607 |
-
|
| 1608 |
-
new_score_state: dict[object, object] = {}
|
| 1609 |
-
with torch.no_grad():
|
| 1610 |
-
embedding_weight.index_copy_(
|
| 1611 |
-
0, reclaimed.to(embedding_weight.device),
|
| 1612 |
-
old_merged.index_select(0, source).to(embedding_weight.device),
|
| 1613 |
-
)
|
| 1614 |
-
for state_key in merged_state:
|
| 1615 |
-
merged_value = merged_state[state_key]
|
| 1616 |
-
score_value = score_state[state_key]
|
| 1617 |
-
embedding_value = embedding_state[state_key]
|
| 1618 |
-
if not torch.is_tensor(merged_value):
|
| 1619 |
-
if not (merged_value == score_value == embedding_value):
|
| 1620 |
-
raise ValueError(f"optimizer scalar state mismatch: {state_key}")
|
| 1621 |
-
new_score_state[state_key] = score_value
|
| 1622 |
-
continue
|
| 1623 |
-
if merged_value.ndim == 0:
|
| 1624 |
-
if not (torch.equal(merged_value, score_value)
|
| 1625 |
-
and torch.equal(merged_value, embedding_value)):
|
| 1626 |
-
raise ValueError(f"optimizer step mismatch: {state_key}")
|
| 1627 |
-
new_score_state[state_key] = score_value.detach().clone()
|
| 1628 |
-
continue
|
| 1629 |
-
if (tuple(merged_value.shape) != tuple(old_merged.shape)
|
| 1630 |
-
or tuple(embedding_value.shape) != tuple(embedding_weight.shape)
|
| 1631 |
-
or tuple(score_value.shape) != tuple(old_score.shape)):
|
| 1632 |
-
raise ValueError(f"unknown optimizer row-state shape: {state_key}")
|
| 1633 |
-
embedding_value.index_copy_(
|
| 1634 |
-
0, reclaimed.to(embedding_value.device),
|
| 1635 |
-
merged_value.index_select(0, source).to(embedding_value.device),
|
| 1636 |
-
)
|
| 1637 |
-
new_score_state[state_key] = score_value.index_select(
|
| 1638 |
-
0, source.to(score_value.device)
|
| 1639 |
-
).detach().clone()
|
| 1640 |
-
|
| 1641 |
-
new_score = nn.Parameter(
|
| 1642 |
-
old_score.index_select(0, source).detach().clone(),
|
| 1643 |
-
requires_grad=old_score.requires_grad,
|
| 1644 |
-
)
|
| 1645 |
-
for group in optimizer.param_groups:
|
| 1646 |
-
replacement = []
|
| 1647 |
-
for parameter in group["params"]:
|
| 1648 |
-
if parameter is old_merged:
|
| 1649 |
-
continue
|
| 1650 |
-
replacement.append(new_score if parameter is old_score else parameter)
|
| 1651 |
-
group["params"] = replacement
|
| 1652 |
-
optimizer.state.pop(old_merged)
|
| 1653 |
-
optimizer.state.pop(old_score)
|
| 1654 |
-
optimizer.state[new_score] = new_score_state
|
| 1655 |
-
|
| 1656 |
-
self.merged = nn.Parameter(
|
| 1657 |
-
torch.empty((0, embedding_weight.shape[1]), dtype=old_merged.dtype,
|
| 1658 |
-
device=old_merged.device),
|
| 1659 |
-
requires_grad=False,
|
| 1660 |
-
)
|
| 1661 |
-
self.score = new_score
|
| 1662 |
-
self.pair_keys = keys
|
| 1663 |
-
self.pair_slots = torch.arange(keys.numel(), device=keys.device)
|
| 1664 |
-
self.reclaimed_token_ids = reclaimed
|
| 1665 |
-
self.buckets = int(keys.numel())
|
| 1666 |
-
self.discovery = None
|
| 1667 |
-
self.diagnostic_discovery = None
|
| 1668 |
-
return {
|
| 1669 |
-
"active_pairs": int(keys.numel()),
|
| 1670 |
-
"unique_source_buckets": unique_sources,
|
| 1671 |
-
"promoted_hash_collisions": int(keys.numel()) - unique_sources,
|
| 1672 |
-
"reclaimed_rows": int(reclaimed.numel()),
|
| 1673 |
-
"released_merged_rows": int(old_merged.shape[0]),
|
| 1674 |
-
}
|
| 1675 |
-
|
| 1676 |
-
def compact_exact_vocabulary(self) -> dict[str, int]:
|
| 1677 |
-
"""Drop inactive discovery rows after training, before final save."""
|
| 1678 |
-
|
| 1679 |
-
active = int(self.pair_keys.numel())
|
| 1680 |
-
if not active:
|
| 1681 |
-
raise ValueError("cannot compact an empty exact vocabulary")
|
| 1682 |
-
old_buckets = self.buckets
|
| 1683 |
-
with torch.no_grad():
|
| 1684 |
-
merged = self.merged[self.pair_slots].detach().clone()
|
| 1685 |
-
score = self.score[self.pair_slots].detach().clone()
|
| 1686 |
-
self.merged = nn.Parameter(merged, requires_grad=self.merged.requires_grad)
|
| 1687 |
-
self.score = nn.Parameter(score, requires_grad=self.score.requires_grad)
|
| 1688 |
-
self.pair_slots = torch.arange(active, device=self.pair_keys.device)
|
| 1689 |
-
self.buckets = active
|
| 1690 |
-
return {"old_rows": old_buckets, "active_rows": active, "dropped_rows": old_buckets - active}
|
| 1691 |
-
|
| 1692 |
-
def pool(
|
| 1693 |
-
self,
|
| 1694 |
-
vectors: torch.Tensor, # (N, dim) token vectors, sentence-ordered
|
| 1695 |
-
content: torch.Tensor, # (N,) token ids
|
| 1696 |
-
segment: torch.Tensor, # (N,) sentence index
|
| 1697 |
-
language: torch.Tensor, # (B,) language per sentence
|
| 1698 |
-
n_sentences: int,
|
| 1699 |
-
embedding_weight: torch.Tensor | None = None,
|
| 1700 |
-
) -> torch.Tensor:
|
| 1701 |
-
n = content.numel()
|
| 1702 |
-
dim = vectors.shape[1]
|
| 1703 |
-
weight = torch.ones(n, dtype=vectors.dtype, device=vectors.device)
|
| 1704 |
-
numerator = torch.zeros(n_sentences, dim, dtype=vectors.dtype, device=vectors.device)
|
| 1705 |
-
denominator = torch.zeros(n_sentences, dtype=vectors.dtype, device=vectors.device)
|
| 1706 |
-
denominator.index_add_(0, segment, weight)
|
| 1707 |
-
telemetry_observed = False
|
| 1708 |
-
|
| 1709 |
-
if n > 1:
|
| 1710 |
-
left, right = content[:-1], content[1:]
|
| 1711 |
-
inside = segment[:-1] == segment[1:]
|
| 1712 |
-
if bool(inside.any()):
|
| 1713 |
-
pair_left = left[inside]
|
| 1714 |
-
pair_right = right[inside]
|
| 1715 |
-
pair_segment = segment[:-1][inside]
|
| 1716 |
-
pair_language = language[pair_segment]
|
| 1717 |
-
exact_key = self._exact_key(pair_left, pair_right)
|
| 1718 |
-
all_positions = torch.nonzero(inside, as_tuple=False).squeeze(1)
|
| 1719 |
-
discovery_probability = None
|
| 1720 |
-
discovery_key = None
|
| 1721 |
-
discovery_language = None
|
| 1722 |
-
if self.pair_keys.numel() and self.residual_buckets:
|
| 1723 |
-
found, exact_index = self._explicit_lookup(exact_key)
|
| 1724 |
-
missed = ~found
|
| 1725 |
-
exact_index = exact_index[found]
|
| 1726 |
-
residual_index = self._hash_residual(
|
| 1727 |
-
pair_left[missed], pair_right[missed]
|
| 1728 |
-
)
|
| 1729 |
-
exact_probability = torch.sigmoid(
|
| 1730 |
-
self.score[exact_index]
|
| 1731 |
-
+ self.language_bias[language[pair_segment[found]]]
|
| 1732 |
-
)
|
| 1733 |
-
residual_probability = torch.sigmoid(
|
| 1734 |
-
self.residual_score[residual_index]
|
| 1735 |
-
+ self.language_bias[language[pair_segment[missed]]]
|
| 1736 |
-
)
|
| 1737 |
-
p = torch.cat((exact_probability, residual_probability))
|
| 1738 |
-
pair_segment = torch.cat((pair_segment[found], pair_segment[missed]))
|
| 1739 |
-
pair_positions = torch.cat((all_positions[found], all_positions[missed]))
|
| 1740 |
-
merged = torch.cat(
|
| 1741 |
-
(
|
| 1742 |
-
self.merged[exact_index],
|
| 1743 |
-
self.residual_merged[residual_index],
|
| 1744 |
-
)
|
| 1745 |
-
)
|
| 1746 |
-
telemetry_index = torch.cat(
|
| 1747 |
-
(exact_index, residual_index + self.buckets)
|
| 1748 |
-
)
|
| 1749 |
-
telemetry_exact_hits = torch.cat(
|
| 1750 |
-
(
|
| 1751 |
-
torch.ones_like(exact_index, dtype=torch.bool),
|
| 1752 |
-
torch.zeros_like(residual_index, dtype=torch.bool),
|
| 1753 |
-
)
|
| 1754 |
-
)
|
| 1755 |
-
discovery_probability = residual_probability
|
| 1756 |
-
discovery_key = exact_key[missed]
|
| 1757 |
-
discovery_language = pair_language[missed]
|
| 1758 |
-
audit_key = torch.cat((exact_key[found], exact_key[missed]))
|
| 1759 |
-
elif self.pair_keys.numel():
|
| 1760 |
-
found, index = self._explicit_lookup(exact_key)
|
| 1761 |
-
pair_segment = pair_segment[found]
|
| 1762 |
-
pair_language = pair_language[found]
|
| 1763 |
-
exact_key = exact_key[found]
|
| 1764 |
-
index = index[found]
|
| 1765 |
-
pair_positions = all_positions[found]
|
| 1766 |
-
p = torch.sigmoid(
|
| 1767 |
-
self.score[index]
|
| 1768 |
-
+ self.language_bias[language[pair_segment]]
|
| 1769 |
-
)
|
| 1770 |
-
if self.reclaimed_token_ids.numel():
|
| 1771 |
-
if embedding_weight is None:
|
| 1772 |
-
raise ValueError(
|
| 1773 |
-
"reclaimed collapse requires the base embedding table"
|
| 1774 |
-
)
|
| 1775 |
-
merged = embedding_weight[
|
| 1776 |
-
self.reclaimed_token_ids.index_select(0, index)
|
| 1777 |
-
]
|
| 1778 |
-
else:
|
| 1779 |
-
merged = self.merged[index]
|
| 1780 |
-
telemetry_index = index
|
| 1781 |
-
telemetry_exact_hits = torch.ones_like(index, dtype=torch.bool)
|
| 1782 |
-
discovery_probability = p
|
| 1783 |
-
discovery_key = exact_key
|
| 1784 |
-
discovery_language = pair_language
|
| 1785 |
-
audit_key = exact_key
|
| 1786 |
-
else:
|
| 1787 |
-
index = self._hash(pair_left, pair_right)
|
| 1788 |
-
pair_positions = all_positions
|
| 1789 |
-
p = torch.sigmoid(
|
| 1790 |
-
self.score[index]
|
| 1791 |
-
+ self.language_bias[language[pair_segment]]
|
| 1792 |
-
)
|
| 1793 |
-
merged = self.merged[index]
|
| 1794 |
-
telemetry_index = index
|
| 1795 |
-
telemetry_exact_hits = torch.zeros_like(index, dtype=torch.bool)
|
| 1796 |
-
discovery_probability = p
|
| 1797 |
-
discovery_key = exact_key
|
| 1798 |
-
discovery_language = pair_language
|
| 1799 |
-
audit_key = exact_key
|
| 1800 |
-
if self._loss_path_scale is not None:
|
| 1801 |
-
if self._loss_path_observer is None:
|
| 1802 |
-
raise RuntimeError("loss-path scale has no observer")
|
| 1803 |
-
original_probability = p.detach()
|
| 1804 |
-
p = p * self._loss_path_scale
|
| 1805 |
-
saved_audit_key = audit_key.detach()
|
| 1806 |
-
saved_audit_probability = original_probability
|
| 1807 |
-
audit_observer = self._loss_path_observer
|
| 1808 |
-
|
| 1809 |
-
def observe_loss_path(gradient):
|
| 1810 |
-
audit_observer(
|
| 1811 |
-
saved_audit_key, saved_audit_probability, gradient
|
| 1812 |
-
)
|
| 1813 |
-
|
| 1814 |
-
if p.requires_grad:
|
| 1815 |
-
p.register_hook(observe_loss_path)
|
| 1816 |
-
if not p.numel():
|
| 1817 |
-
_weighted_index_add_(numerator, segment, vectors, weight)
|
| 1818 |
-
if self._vocabulary_telemetry is not None:
|
| 1819 |
-
self._vocabulary_telemetry.observe_q15(
|
| 1820 |
-
base_tokens=n,
|
| 1821 |
-
probabilities=torch.empty(
|
| 1822 |
-
0, dtype=torch.float32, device=content.device
|
| 1823 |
-
),
|
| 1824 |
-
hash_indices=telemetry_index,
|
| 1825 |
-
effective_tokens=denominator.sum(),
|
| 1826 |
-
exact_hits=telemetry_exact_hits,
|
| 1827 |
-
)
|
| 1828 |
-
telemetry_observed = True
|
| 1829 |
-
return numerator / denominator.clamp(min=1e-3).unsqueeze(1)
|
| 1830 |
-
discoveries = tuple(
|
| 1831 |
-
observer for observer in (
|
| 1832 |
-
self.discovery, self.diagnostic_discovery
|
| 1833 |
-
) if observer is not None
|
| 1834 |
-
)
|
| 1835 |
-
if (
|
| 1836 |
-
discoveries
|
| 1837 |
-
and self.training
|
| 1838 |
-
and discovery_probability is not None
|
| 1839 |
-
and discovery_probability.requires_grad
|
| 1840 |
-
and discovery_probability.numel()
|
| 1841 |
-
):
|
| 1842 |
-
saved_key = discovery_key.detach()
|
| 1843 |
-
saved_language = discovery_language.detach()
|
| 1844 |
-
saved_probability = discovery_probability.detach()
|
| 1845 |
-
def observe_all(gradient):
|
| 1846 |
-
for observer in discoveries:
|
| 1847 |
-
observer.observe(
|
| 1848 |
-
saved_key, saved_language, saved_probability, gradient
|
| 1849 |
-
)
|
| 1850 |
-
discovery_probability.register_hook(observe_all)
|
| 1851 |
-
# each participant of a merge gives up half a slot
|
| 1852 |
-
half = 0.5 * p
|
| 1853 |
-
weight = weight.index_add(0, pair_positions, -half)
|
| 1854 |
-
weight = weight.index_add(0, pair_positions + 1, -half)
|
| 1855 |
-
if self.reclaimed_token_ids.numel():
|
| 1856 |
-
if embedding_weight is None:
|
| 1857 |
-
raise ValueError("reclaimed collapse requires the base embedding table")
|
| 1858 |
-
merged = embedding_weight[
|
| 1859 |
-
self.reclaimed_token_ids.index_select(0, index)
|
| 1860 |
-
]
|
| 1861 |
-
_weighted_index_add_(numerator, pair_segment, merged, p)
|
| 1862 |
-
denominator.index_add_(0, pair_segment, -p)
|
| 1863 |
-
|
| 1864 |
-
if self._vocabulary_telemetry is not None:
|
| 1865 |
-
self._vocabulary_telemetry.observe_q15(
|
| 1866 |
-
base_tokens=n,
|
| 1867 |
-
probabilities=p,
|
| 1868 |
-
hash_indices=telemetry_index,
|
| 1869 |
-
effective_tokens=denominator.sum(),
|
| 1870 |
-
exact_hits=telemetry_exact_hits,
|
| 1871 |
-
)
|
| 1872 |
-
telemetry_observed = True
|
| 1873 |
-
|
| 1874 |
-
_weighted_index_add_(numerator, segment, vectors, weight)
|
| 1875 |
-
if not telemetry_observed and self._vocabulary_telemetry is not None:
|
| 1876 |
-
self._vocabulary_telemetry.observe_q15(
|
| 1877 |
-
base_tokens=n,
|
| 1878 |
-
probabilities=torch.empty(
|
| 1879 |
-
0, dtype=torch.float32, device=content.device
|
| 1880 |
-
),
|
| 1881 |
-
hash_indices=torch.empty(
|
| 1882 |
-
0, dtype=torch.long, device=content.device
|
| 1883 |
-
),
|
| 1884 |
-
effective_tokens=denominator.sum(),
|
| 1885 |
-
exact_hits=torch.empty(
|
| 1886 |
-
0, dtype=torch.bool, device=content.device
|
| 1887 |
-
),
|
| 1888 |
-
)
|
| 1889 |
-
return numerator / denominator.clamp(min=1e-3).unsqueeze(1)
|
| 1890 |
-
|
| 1891 |
-
def mean_merge_probability(self, content, segment, language) -> torch.Tensor:
|
| 1892 |
-
if content.numel() < 2:
|
| 1893 |
-
return torch.zeros((), device=content.device)
|
| 1894 |
-
inside = segment[:-1] == segment[1:]
|
| 1895 |
-
if not bool(inside.any()):
|
| 1896 |
-
return torch.zeros((), device=content.device)
|
| 1897 |
-
left, right = content[:-1][inside], content[1:][inside]
|
| 1898 |
-
pair_segment = segment[:-1][inside]
|
| 1899 |
-
if self.pair_keys.numel():
|
| 1900 |
-
found, index = self._explicit_lookup(self._exact_key(left, right))
|
| 1901 |
-
index = index[found]
|
| 1902 |
-
pair_segment = pair_segment[found]
|
| 1903 |
-
else:
|
| 1904 |
-
index = self._hash(left, right)
|
| 1905 |
-
if not index.numel():
|
| 1906 |
-
return torch.zeros((), device=content.device)
|
| 1907 |
-
p = torch.sigmoid(self.score[index] + self.language_bias[language[pair_segment]])
|
| 1908 |
-
return p.mean()
|
| 1909 |
-
|
| 1910 |
-
|
| 1911 |
-
def ablate_language_bias(module: nn.Module, mode: str = "none") -> None:
|
| 1912 |
-
"""Apply an evaluation-only necessity ablation to a loaded collapse model.
|
| 1913 |
-
|
| 1914 |
-
``mean`` replaces every language bias with their learned mean. It preserves
|
| 1915 |
-
the overall merge tendency while removing language-specific routing; every
|
| 1916 |
-
other learned parameter remains unchanged. Because the full model was
|
| 1917 |
-
trained with language routing active, this does not replace a separately
|
| 1918 |
-
trained shared-bias control for a training-time causal claim.
|
| 1919 |
-
"""
|
| 1920 |
-
|
| 1921 |
-
if mode == "none":
|
| 1922 |
-
return
|
| 1923 |
-
if mode != "mean":
|
| 1924 |
-
raise ValueError(f"Unknown collapse language-bias ablation: {mode!r}")
|
| 1925 |
-
tables = [child for child in module.modules() if isinstance(child, CollapseTable)]
|
| 1926 |
-
if len(tables) != 1:
|
| 1927 |
-
raise ValueError(f"Expected exactly one CollapseTable, found {len(tables)}")
|
| 1928 |
-
with torch.no_grad():
|
| 1929 |
-
bias = tables[0].language_bias
|
| 1930 |
-
bias.fill_(bias.float().mean().to(dtype=bias.dtype, device=bias.device))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/compiled_latent_encoder.py
DELETED
|
@@ -1,281 +0,0 @@
|
|
| 1 |
-
"""Benchmark adapter for a compiled latent-lexicon static encoder."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import hashlib
|
| 6 |
-
import json
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
import re
|
| 9 |
-
from typing import Any, Sequence
|
| 10 |
-
|
| 11 |
-
import numpy as np
|
| 12 |
-
from tokenizers import Tokenizer
|
| 13 |
-
import torch
|
| 14 |
-
import torch.nn.functional as F
|
| 15 |
-
|
| 16 |
-
from starse.latent_compilation import (
|
| 17 |
-
CompiledLatentLexicon,
|
| 18 |
-
_validated_pair_support_filter,
|
| 19 |
-
decision_raw_origins,
|
| 20 |
-
load_compiled_lexicon,
|
| 21 |
-
validate_decision_support_coupling,
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
_RUN_PROTOCOL = "compiled-latent-lexicon-run-v1"
|
| 26 |
-
_RUN_KEYS = {
|
| 27 |
-
"protocol",
|
| 28 |
-
"selection_uses_heldout_or_benchmark_data",
|
| 29 |
-
"quality_measured",
|
| 30 |
-
"quality_status",
|
| 31 |
-
"decision_policy",
|
| 32 |
-
"checkpoint",
|
| 33 |
-
"artifact",
|
| 34 |
-
"probe_selection",
|
| 35 |
-
"equivalence",
|
| 36 |
-
}
|
| 37 |
-
_ARTIFACT_KEYS = {"directory", "manifest_sha256", "identity"}
|
| 38 |
-
_CHECKPOINT_KEYS = {"completed_steps", "segments", "provenance_rows", "source"}
|
| 39 |
-
_PROBE_SELECTION = {
|
| 40 |
-
"protocol": "training-coordinate-sha256-full-chain-v1",
|
| 41 |
-
"count": 32,
|
| 42 |
-
"scope": "full_checkpoint_chain",
|
| 43 |
-
}
|
| 44 |
-
_EQUIVALENCE_KEYS = {
|
| 45 |
-
"protocol",
|
| 46 |
-
"probe_count",
|
| 47 |
-
"probe_sha256",
|
| 48 |
-
"emissions_exact",
|
| 49 |
-
"numerators_exact",
|
| 50 |
-
"max_abs_numerator_error",
|
| 51 |
-
}
|
| 52 |
-
_LANGUAGE_MARKER = re.compile(r"__[a-z]{3}_[A-Z][a-z]{3}__\Z")
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def _sha256_file(path: Path) -> str:
|
| 56 |
-
digest = hashlib.sha256()
|
| 57 |
-
with path.open("rb") as handle:
|
| 58 |
-
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
| 59 |
-
digest.update(chunk)
|
| 60 |
-
return digest.hexdigest()
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def _positive_integer(value: object, label: str) -> int:
|
| 64 |
-
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
| 65 |
-
raise ValueError(f"compiled run {label} is invalid")
|
| 66 |
-
return value
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def _valid_sha256(value: object) -> bool:
|
| 70 |
-
return (
|
| 71 |
-
isinstance(value, str)
|
| 72 |
-
and len(value) == 64
|
| 73 |
-
and not any(character not in "0123456789abcdef" for character in value)
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def _load_summary(
|
| 78 |
-
path: Path, *, expected_completed_steps: int
|
| 79 |
-
) -> dict[str, object]:
|
| 80 |
-
try:
|
| 81 |
-
with path.open(encoding="utf-8") as handle:
|
| 82 |
-
summary = json.load(handle)
|
| 83 |
-
except (OSError, json.JSONDecodeError) as error:
|
| 84 |
-
raise ValueError("compiled run summary is invalid") from error
|
| 85 |
-
if (
|
| 86 |
-
not isinstance(summary, dict)
|
| 87 |
-
or set(summary) not in (
|
| 88 |
-
_RUN_KEYS,
|
| 89 |
-
_RUN_KEYS | {"pair_support_filter"},
|
| 90 |
-
)
|
| 91 |
-
or summary.get("protocol") != _RUN_PROTOCOL
|
| 92 |
-
or summary.get("selection_uses_heldout_or_benchmark_data") is not False
|
| 93 |
-
or summary.get("quality_measured") is not False
|
| 94 |
-
or summary.get("quality_status") != "not measured"
|
| 95 |
-
):
|
| 96 |
-
raise ValueError("compiled run summary schema is invalid")
|
| 97 |
-
support_filter = None
|
| 98 |
-
if "pair_support_filter" in summary:
|
| 99 |
-
support_filter = _validated_pair_support_filter(
|
| 100 |
-
summary["pair_support_filter"]
|
| 101 |
-
)
|
| 102 |
-
expected_step = _positive_integer(
|
| 103 |
-
expected_completed_steps, "expected completed steps"
|
| 104 |
-
)
|
| 105 |
-
checkpoint = summary.get("checkpoint")
|
| 106 |
-
if not isinstance(checkpoint, dict) or set(checkpoint) != _CHECKPOINT_KEYS:
|
| 107 |
-
raise ValueError("compiled run summary checkpoint is invalid")
|
| 108 |
-
if (
|
| 109 |
-
_positive_integer(checkpoint.get("completed_steps"), "checkpoint step")
|
| 110 |
-
!= expected_step
|
| 111 |
-
or _positive_integer(checkpoint.get("segments"), "checkpoint segments") < 1
|
| 112 |
-
or _positive_integer(
|
| 113 |
-
checkpoint.get("provenance_rows"), "checkpoint provenance rows"
|
| 114 |
-
) < 1
|
| 115 |
-
or not isinstance(checkpoint.get("source"), dict)
|
| 116 |
-
):
|
| 117 |
-
raise ValueError("compiled run summary checkpoint is not final")
|
| 118 |
-
if summary.get("probe_selection") != _PROBE_SELECTION:
|
| 119 |
-
raise ValueError("compiled run summary probe selection is invalid")
|
| 120 |
-
policy = summary.get("decision_policy")
|
| 121 |
-
if not isinstance(policy, dict):
|
| 122 |
-
raise ValueError("compiled run decision policy is invalid")
|
| 123 |
-
decision_raw_origins(policy)
|
| 124 |
-
source = checkpoint.get("source")
|
| 125 |
-
validate_decision_support_coupling(
|
| 126 |
-
policy,
|
| 127 |
-
support_filter,
|
| 128 |
-
source_checkpoint=source if isinstance(source, dict) else None,
|
| 129 |
-
)
|
| 130 |
-
equivalence = summary.get("equivalence")
|
| 131 |
-
if (
|
| 132 |
-
not isinstance(equivalence, dict)
|
| 133 |
-
or set(equivalence) != _EQUIVALENCE_KEYS
|
| 134 |
-
or equivalence.get("protocol") != "compiled-latent-equivalence-v1"
|
| 135 |
-
or equivalence.get("probe_count") != 32
|
| 136 |
-
or not _valid_sha256(equivalence.get("probe_sha256"))
|
| 137 |
-
or equivalence.get("emissions_exact") is not True
|
| 138 |
-
or equivalence.get("numerators_exact") is not True
|
| 139 |
-
or isinstance(equivalence.get("max_abs_numerator_error"), bool)
|
| 140 |
-
or not isinstance(equivalence.get("max_abs_numerator_error"), (int, float))
|
| 141 |
-
or float(equivalence["max_abs_numerator_error"]) != 0.0
|
| 142 |
-
):
|
| 143 |
-
raise ValueError("compiled run summary equivalence is invalid")
|
| 144 |
-
artifact = summary.get("artifact")
|
| 145 |
-
if not isinstance(artifact, dict) or set(artifact) != _ARTIFACT_KEYS:
|
| 146 |
-
raise ValueError("compiled run artifact record is invalid")
|
| 147 |
-
digest = artifact.get("manifest_sha256")
|
| 148 |
-
if (
|
| 149 |
-
not _valid_sha256(digest)
|
| 150 |
-
or not isinstance(artifact.get("identity"), dict)
|
| 151 |
-
or artifact.get("directory") != "artifact"
|
| 152 |
-
):
|
| 153 |
-
raise ValueError("compiled run artifact identity is invalid")
|
| 154 |
-
return summary
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
class CompiledLatentEncoder:
|
| 158 |
-
"""Text encoder backed only by a compiled routing map and embedding table."""
|
| 159 |
-
|
| 160 |
-
def __init__(
|
| 161 |
-
self, run_path: str | Path, *, expected_completed_steps: int = 3000
|
| 162 |
-
) -> None:
|
| 163 |
-
self.run_path = Path(run_path).resolve()
|
| 164 |
-
if not self.run_path.is_dir():
|
| 165 |
-
raise ValueError("compiled run directory does not exist")
|
| 166 |
-
summary = _load_summary(
|
| 167 |
-
self.run_path / "summary.json",
|
| 168 |
-
expected_completed_steps=expected_completed_steps,
|
| 169 |
-
)
|
| 170 |
-
artifact_record = summary["artifact"]
|
| 171 |
-
assert isinstance(artifact_record, dict)
|
| 172 |
-
artifact_path = self.run_path / "artifact"
|
| 173 |
-
manifest_path = artifact_path / "manifest.json"
|
| 174 |
-
if _sha256_file(manifest_path) != artifact_record["manifest_sha256"]:
|
| 175 |
-
raise ValueError("compiled run manifest hash differs from summary")
|
| 176 |
-
compiled = load_compiled_lexicon(artifact_path)
|
| 177 |
-
try:
|
| 178 |
-
with manifest_path.open(encoding="utf-8") as handle:
|
| 179 |
-
artifact_manifest = json.load(handle)
|
| 180 |
-
except (OSError, json.JSONDecodeError) as error:
|
| 181 |
-
raise ValueError("compiled artifact manifest is invalid") from error
|
| 182 |
-
checkpoint = summary["checkpoint"]
|
| 183 |
-
assert isinstance(checkpoint, dict)
|
| 184 |
-
if (
|
| 185 |
-
not isinstance(artifact_manifest, dict)
|
| 186 |
-
or artifact_manifest.get("source_checkpoint") != checkpoint["source"]
|
| 187 |
-
or artifact_manifest.get("decision_policy")
|
| 188 |
-
!= summary["decision_policy"]
|
| 189 |
-
or artifact_manifest.get("pair_support_filter")
|
| 190 |
-
!= summary.get("pair_support_filter")
|
| 191 |
-
or checkpoint["source"].get("completed_steps")
|
| 192 |
-
!= checkpoint["completed_steps"]
|
| 193 |
-
):
|
| 194 |
-
raise ValueError("compiled run checkpoint source differs from artifact")
|
| 195 |
-
if compiled.identity() != artifact_record["identity"]:
|
| 196 |
-
raise ValueError("compiled run semantic identity differs from summary")
|
| 197 |
-
try:
|
| 198 |
-
tokenizer = Tokenizer.from_file(str(artifact_path / "tokenizer.json"))
|
| 199 |
-
except Exception as error:
|
| 200 |
-
raise ValueError("compiled run tokenizer is invalid") from error
|
| 201 |
-
if (
|
| 202 |
-
tokenizer.get_vocab_size(with_added_tokens=False)
|
| 203 |
-
!= compiled.model_vocab_size
|
| 204 |
-
):
|
| 205 |
-
raise ValueError("compiled tokenizer base vocabulary differs from model")
|
| 206 |
-
marker_ids: set[int] = set()
|
| 207 |
-
for token_id, token in tokenizer.get_added_tokens_decoder().items():
|
| 208 |
-
content = str(token.content)
|
| 209 |
-
if bool(token.special) and _LANGUAGE_MARKER.fullmatch(content):
|
| 210 |
-
marker_ids.add(int(token_id))
|
| 211 |
-
if not marker_ids:
|
| 212 |
-
raise ValueError("compiled tokenizer has no language markers")
|
| 213 |
-
|
| 214 |
-
self.compiled: CompiledLatentLexicon = compiled
|
| 215 |
-
self.tokenizer: Tokenizer = tokenizer
|
| 216 |
-
self.language_marker_ids = frozenset(marker_ids)
|
| 217 |
-
self.dimension = int(compiled.embedding.shape[1])
|
| 218 |
-
|
| 219 |
-
def _roots(self, token_ids: Sequence[int]) -> tuple[int, ...]:
|
| 220 |
-
roots = tuple(int(token_id) for token_id in token_ids)
|
| 221 |
-
if roots and roots[0] in self.language_marker_ids:
|
| 222 |
-
roots = roots[1:]
|
| 223 |
-
if not roots:
|
| 224 |
-
raise ValueError("compiled encoder received empty content")
|
| 225 |
-
if min(roots) < 0 or max(roots) >= self.compiled.model_vocab_size:
|
| 226 |
-
raise ValueError("compiled encoder content id is outside model vocabulary")
|
| 227 |
-
return roots
|
| 228 |
-
|
| 229 |
-
def encode(
|
| 230 |
-
self,
|
| 231 |
-
sentences: str | Sequence[str],
|
| 232 |
-
batch_size: int = 32,
|
| 233 |
-
normalize_embeddings: bool = False,
|
| 234 |
-
*,
|
| 235 |
-
convert_to_numpy: bool = True,
|
| 236 |
-
convert_to_tensor: bool = False,
|
| 237 |
-
**kwargs: Any,
|
| 238 |
-
) -> np.ndarray | torch.Tensor:
|
| 239 |
-
"""Encode marked or unmarked text with deterministic compiled MAP routing."""
|
| 240 |
-
del kwargs
|
| 241 |
-
if (
|
| 242 |
-
isinstance(batch_size, bool)
|
| 243 |
-
or not isinstance(batch_size, int)
|
| 244 |
-
or batch_size <= 0
|
| 245 |
-
):
|
| 246 |
-
raise ValueError("compiled encoder batch size must be a positive integer")
|
| 247 |
-
if not isinstance(normalize_embeddings, bool):
|
| 248 |
-
raise TypeError("normalize_embeddings must be a bool")
|
| 249 |
-
single = isinstance(sentences, str)
|
| 250 |
-
if single:
|
| 251 |
-
texts = [sentences]
|
| 252 |
-
else:
|
| 253 |
-
if isinstance(sentences, (bytes, bytearray)):
|
| 254 |
-
raise TypeError("compiled encoder sentences must be strings")
|
| 255 |
-
texts = list(sentences)
|
| 256 |
-
if not texts or any(not isinstance(text, str) for text in texts):
|
| 257 |
-
raise ValueError("compiled encoder needs at least one string")
|
| 258 |
-
|
| 259 |
-
vectors: list[torch.Tensor] = []
|
| 260 |
-
for start in range(0, len(texts), batch_size):
|
| 261 |
-
encodings = self.tokenizer.encode_batch(
|
| 262 |
-
texts[start:start + batch_size], add_special_tokens=False
|
| 263 |
-
)
|
| 264 |
-
for encoding in encodings:
|
| 265 |
-
roots = self._roots(encoding.ids)
|
| 266 |
-
vectors.append(self.compiled.encode_roots(roots).numerator)
|
| 267 |
-
output = torch.stack(vectors)
|
| 268 |
-
if normalize_embeddings:
|
| 269 |
-
norms = torch.linalg.vector_norm(output, dim=1)
|
| 270 |
-
if not bool(torch.isfinite(norms).all().item()) or bool(
|
| 271 |
-
(norms == 0).any().item()
|
| 272 |
-
):
|
| 273 |
-
raise ValueError("compiled encoder produced a non-normalizable vector")
|
| 274 |
-
output = F.normalize(output, p=2, dim=1)
|
| 275 |
-
if single:
|
| 276 |
-
output = output[0]
|
| 277 |
-
if convert_to_tensor:
|
| 278 |
-
return output
|
| 279 |
-
if not convert_to_numpy:
|
| 280 |
-
raise ValueError("compiled encoder supports NumPy or tensor output")
|
| 281 |
-
return output.numpy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/conditioned_module.py
DELETED
|
@@ -1,738 +0,0 @@
|
|
| 1 |
-
"""SentenceTransformer module for a language-conditioned static encoder.
|
| 2 |
-
|
| 3 |
-
Subclasses ``StaticEmbedding`` so tokenisation, saving and loading are inherited;
|
| 4 |
-
only ``forward`` changes. The language arrives as a marker token at the front of
|
| 5 |
-
each sequence, which ``forward`` peels off before pooling.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import json
|
| 11 |
-
import os
|
| 12 |
-
from pathlib import Path
|
| 13 |
-
|
| 14 |
-
import torch
|
| 15 |
-
from sentence_transformers.sentence_transformer.modules.static_embedding import StaticEmbedding
|
| 16 |
-
from tokenizers import Tokenizer
|
| 17 |
-
|
| 18 |
-
from starse.language_conditioned import GATING_MODES, LanguageConditioner, split_markers
|
| 19 |
-
from starse.ngram_table import NgramTable
|
| 20 |
-
from starse.online_tokenizer import OnlineMergeTable
|
| 21 |
-
from starse.recursive_cascade import DenseRecursiveCascade
|
| 22 |
-
from starse.split_vocabulary import SplitVocabulary
|
| 23 |
-
from starse.collapse import CollapseTable
|
| 24 |
-
from starse.vocab_assignment import VocabAssignment
|
| 25 |
-
|
| 26 |
-
CONFIG_NAME = "language_conditioning.json"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def marker_for(language: str) -> str:
|
| 30 |
-
return f"__{language}__"
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
class ConditionedStaticEmbedding(StaticEmbedding):
|
| 34 |
-
def __init__(
|
| 35 |
-
self,
|
| 36 |
-
tokenizer: Tokenizer,
|
| 37 |
-
embedding_weights=None,
|
| 38 |
-
embedding_dim: int | None = None,
|
| 39 |
-
*,
|
| 40 |
-
languages: list[str] | None = None,
|
| 41 |
-
mode: str = "none",
|
| 42 |
-
code_dim: int = 16,
|
| 43 |
-
n_senses: int = 64,
|
| 44 |
-
rank: int = 8,
|
| 45 |
-
split_index: str | None = None,
|
| 46 |
-
ngram_buckets: int = 0,
|
| 47 |
-
n_vocabs: int = 4,
|
| 48 |
-
weight_index: str | None = None,
|
| 49 |
-
dynamic_pair_keys: list[int] | None = None,
|
| 50 |
-
dynamic_pair_slots: list[int] | None = None,
|
| 51 |
-
reclaimed_token_ids: list[int] | None = None,
|
| 52 |
-
reclaimed_left_ids: list[int] | None = None,
|
| 53 |
-
reclaimed_right_ids: list[int] | None = None,
|
| 54 |
-
growth_residual_buckets: int = 0,
|
| 55 |
-
online_rank: int = 32,
|
| 56 |
-
compiled_pair_keys: list[int] | None = None,
|
| 57 |
-
compiled_pair_scores: list[float] | None = None,
|
| 58 |
-
recursive_max_span_length: int = 32,
|
| 59 |
-
recursive_restored_state: dict[str, torch.Tensor] | None = None,
|
| 60 |
-
**kwargs,
|
| 61 |
-
) -> None:
|
| 62 |
-
super().__init__(tokenizer, embedding_weights=embedding_weights,
|
| 63 |
-
embedding_dim=embedding_dim, **kwargs)
|
| 64 |
-
self.languages = list(languages or [])
|
| 65 |
-
self.mode = mode
|
| 66 |
-
vocab_size = self.embedding.weight.shape[0]
|
| 67 |
-
dim = self.embedding.weight.shape[1]
|
| 68 |
-
|
| 69 |
-
# marker token id -> language index; -1 for every ordinary token, which
|
| 70 |
-
# makes a missing marker fail loudly instead of silently picking language 0
|
| 71 |
-
lookup = torch.full((vocab_size,), -1, dtype=torch.long)
|
| 72 |
-
for index, language in enumerate(self.languages):
|
| 73 |
-
token_id = self.tokenizer.token_to_id(marker_for(language))
|
| 74 |
-
if token_id is None:
|
| 75 |
-
raise ValueError(f"tokenizer has no marker for {language!r}")
|
| 76 |
-
lookup[token_id] = index
|
| 77 |
-
self.register_buffer("marker_lookup", lookup, persistent=False)
|
| 78 |
-
|
| 79 |
-
self.split_index = split_index
|
| 80 |
-
self.split = None
|
| 81 |
-
if mode in ("split", "fuse"):
|
| 82 |
-
if not split_index:
|
| 83 |
-
raise ValueError("mode 'split' needs split_index")
|
| 84 |
-
self.split = SplitVocabulary(split_index, vocab_size, dim, len(self.languages))
|
| 85 |
-
self.split.seed_from(self.embedding.weight.data)
|
| 86 |
-
|
| 87 |
-
self.n_vocabs = int(n_vocabs)
|
| 88 |
-
self.assign = None
|
| 89 |
-
if mode == "vocabmoe":
|
| 90 |
-
if not split_index:
|
| 91 |
-
raise ValueError("mode 'vocabmoe' needs split_index")
|
| 92 |
-
self.assign = VocabAssignment(split_index, vocab_size, dim,
|
| 93 |
-
len(self.languages), self.n_vocabs)
|
| 94 |
-
|
| 95 |
-
self.ngram_buckets = int(ngram_buckets)
|
| 96 |
-
self.growth_residual_buckets = int(growth_residual_buckets)
|
| 97 |
-
self.ngram = None
|
| 98 |
-
self.collapse = None
|
| 99 |
-
self.online_rank = int(online_rank)
|
| 100 |
-
self.online_collapse = None
|
| 101 |
-
self.recursive_max_span_length = int(recursive_max_span_length)
|
| 102 |
-
self.recursive_cascade = None
|
| 103 |
-
if mode in ("collapse", "collapse_shared", "collapse_dynamic", "collapse_reclaimed"):
|
| 104 |
-
if not self.ngram_buckets:
|
| 105 |
-
raise ValueError(f"mode {mode!r} needs ngram_buckets")
|
| 106 |
-
bias_languages = 1 if mode == "collapse_shared" else len(self.languages)
|
| 107 |
-
self.collapse = CollapseTable(
|
| 108 |
-
self.ngram_buckets,
|
| 109 |
-
dim,
|
| 110 |
-
bias_languages,
|
| 111 |
-
pair_key_base=vocab_size,
|
| 112 |
-
pair_keys=dynamic_pair_keys,
|
| 113 |
-
pair_slots=dynamic_pair_slots,
|
| 114 |
-
reclaimed_token_ids=(
|
| 115 |
-
reclaimed_token_ids if mode == "collapse_reclaimed" else None
|
| 116 |
-
),
|
| 117 |
-
residual_buckets=self.growth_residual_buckets,
|
| 118 |
-
)
|
| 119 |
-
if mode in ("collapse_online", "collapse_online_compiled"):
|
| 120 |
-
if mode == "collapse_online" and not self.ngram_buckets:
|
| 121 |
-
raise ValueError("mode 'collapse_online' needs ngram_buckets")
|
| 122 |
-
if mode == "collapse_online_compiled" and (
|
| 123 |
-
compiled_pair_keys is None or compiled_pair_scores is None
|
| 124 |
-
):
|
| 125 |
-
raise ValueError(
|
| 126 |
-
"mode 'collapse_online_compiled' needs exact pair keys and scores"
|
| 127 |
-
)
|
| 128 |
-
self.online_collapse = OnlineMergeTable(
|
| 129 |
-
max(1, self.ngram_buckets),
|
| 130 |
-
dim,
|
| 131 |
-
len(self.languages),
|
| 132 |
-
rank=self.online_rank,
|
| 133 |
-
pair_key_base=vocab_size,
|
| 134 |
-
compiled_pair_keys=(
|
| 135 |
-
torch.as_tensor(compiled_pair_keys, dtype=torch.long)
|
| 136 |
-
if mode == "collapse_online_compiled" else None
|
| 137 |
-
),
|
| 138 |
-
compiled_pair_scores=(
|
| 139 |
-
torch.as_tensor(compiled_pair_scores, dtype=torch.float32)
|
| 140 |
-
if mode == "collapse_online_compiled" else None
|
| 141 |
-
),
|
| 142 |
-
)
|
| 143 |
-
if mode == "collapse_recursive":
|
| 144 |
-
if not self.ngram_buckets:
|
| 145 |
-
raise ValueError("mode 'collapse_recursive' needs proposal buckets")
|
| 146 |
-
self.recursive_cascade = DenseRecursiveCascade(
|
| 147 |
-
base_size=vocab_size,
|
| 148 |
-
dim=dim,
|
| 149 |
-
n_languages=len(self.languages),
|
| 150 |
-
residual_buckets=self.ngram_buckets,
|
| 151 |
-
max_span_length=self.recursive_max_span_length,
|
| 152 |
-
restored_state=recursive_restored_state,
|
| 153 |
-
)
|
| 154 |
-
|
| 155 |
-
self.reclaimed_token_ids = list(reclaimed_token_ids or [])
|
| 156 |
-
self.reclaimed_left_ids = list(reclaimed_left_ids or [])
|
| 157 |
-
self.reclaimed_right_ids = list(reclaimed_right_ids or [])
|
| 158 |
-
reclaim_left = torch.full((vocab_size,), -1, dtype=torch.long)
|
| 159 |
-
reclaim_right = torch.full((vocab_size,), -1, dtype=torch.long)
|
| 160 |
-
if mode == "collapse_reclaimed":
|
| 161 |
-
if not (len(self.reclaimed_token_ids) == len(self.reclaimed_left_ids)
|
| 162 |
-
== len(self.reclaimed_right_ids)):
|
| 163 |
-
raise ValueError("reclaimed token and parent arrays must align")
|
| 164 |
-
reclaimed = torch.tensor(self.reclaimed_token_ids, dtype=torch.long)
|
| 165 |
-
left_parent = torch.tensor(self.reclaimed_left_ids, dtype=torch.long)
|
| 166 |
-
right_parent = torch.tensor(self.reclaimed_right_ids, dtype=torch.long)
|
| 167 |
-
if reclaimed.numel() != self.ngram_buckets:
|
| 168 |
-
raise ValueError("collapse_reclaimed needs one physical row per exact pair")
|
| 169 |
-
if reclaimed.numel() and (
|
| 170 |
-
reclaimed.unique().numel() != reclaimed.numel()
|
| 171 |
-
or int(reclaimed.min()) < 0 or int(reclaimed.max()) >= vocab_size
|
| 172 |
-
or int(left_parent.min()) < 0 or int(left_parent.max()) >= vocab_size
|
| 173 |
-
or int(right_parent.min()) < 0 or int(right_parent.max()) >= vocab_size
|
| 174 |
-
):
|
| 175 |
-
raise ValueError("invalid reclaimed token or parent id")
|
| 176 |
-
selected = set(self.reclaimed_token_ids)
|
| 177 |
-
if selected & set(self.reclaimed_left_ids + self.reclaimed_right_ids):
|
| 178 |
-
raise ValueError("reclaimed split map contains direct recursion")
|
| 179 |
-
reclaim_left[reclaimed] = left_parent
|
| 180 |
-
reclaim_right[reclaimed] = right_parent
|
| 181 |
-
self.register_buffer("reclaim_left", reclaim_left, persistent=False)
|
| 182 |
-
self.register_buffer("reclaim_right", reclaim_right, persistent=False)
|
| 183 |
-
|
| 184 |
-
self.weight_index = weight_index
|
| 185 |
-
if mode == "idfpool":
|
| 186 |
-
if not weight_index:
|
| 187 |
-
raise ValueError("mode 'idfpool' needs weight_index")
|
| 188 |
-
import numpy as np
|
| 189 |
-
|
| 190 |
-
data = np.load(weight_index)
|
| 191 |
-
stored = [str(name) for name in data["languages"]]
|
| 192 |
-
if stored != self.languages:
|
| 193 |
-
raise ValueError(
|
| 194 |
-
"weight table languages do not match the model's, in order: "
|
| 195 |
-
f"{stored[:3]}... vs {self.languages[:3]}...")
|
| 196 |
-
table = torch.from_numpy(data["weight"].astype("float32"))
|
| 197 |
-
if table.shape[1] < vocab_size:
|
| 198 |
-
# Marker rows are appended after the weight table was built. They
|
| 199 |
-
# are stripped before pooling, so their weight is never read, but
|
| 200 |
-
# the buffer still has to be indexable by any id in the table.
|
| 201 |
-
pad = torch.ones(table.shape[0], vocab_size - table.shape[1])
|
| 202 |
-
table = torch.cat([table, pad], dim=1)
|
| 203 |
-
elif table.shape[1] > vocab_size:
|
| 204 |
-
raise ValueError(
|
| 205 |
-
f"weight table has {table.shape[1]} columns, vocabulary is {vocab_size}")
|
| 206 |
-
self.register_buffer("pool_weight", table, persistent=True)
|
| 207 |
-
|
| 208 |
-
if mode == "wordpool":
|
| 209 |
-
# A token that starts with the SentencePiece boundary marker opens a
|
| 210 |
-
# new word, so a running sum of that flag inside a sentence gives
|
| 211 |
-
# word ids without touching the tokenizer.
|
| 212 |
-
starts = torch.zeros(vocab_size, dtype=torch.bool)
|
| 213 |
-
for token_id in range(vocab_size):
|
| 214 |
-
piece = self.tokenizer.id_to_token(token_id)
|
| 215 |
-
if piece is None or piece.startswith("\u2581"):
|
| 216 |
-
starts[token_id] = True
|
| 217 |
-
self.register_buffer("word_start", starts, persistent=False)
|
| 218 |
-
|
| 219 |
-
if mode == "ngram_gate":
|
| 220 |
-
# One scalar per language: how much n-gram signal this language wants.
|
| 221 |
-
# It starts open enough that the n-gram rows receive gradient, and
|
| 222 |
-
# what it converges to is itself the result — a language that
|
| 223 |
-
# tokenises into words should learn to shut it.
|
| 224 |
-
self.ngram_gate = torch.nn.Parameter(torch.zeros(len(self.languages)))
|
| 225 |
-
|
| 226 |
-
if mode in ("ngram", "ngram3", "ngram_gate"):
|
| 227 |
-
if not self.ngram_buckets:
|
| 228 |
-
raise ValueError(f"mode {mode!r} needs ngram_buckets")
|
| 229 |
-
orders = (2, 3) if mode == "ngram3" else (2,)
|
| 230 |
-
self.ngram = NgramTable(self.ngram_buckets, dim, orders)
|
| 231 |
-
|
| 232 |
-
self.conditioner = LanguageConditioner(
|
| 233 |
-
mode=mode, dim=dim, vocab_size=vocab_size,
|
| 234 |
-
n_languages=len(self.languages), code_dim=code_dim,
|
| 235 |
-
n_senses=n_senses, rank=rank,
|
| 236 |
-
)
|
| 237 |
-
|
| 238 |
-
def forward(self, features: dict[str, torch.Tensor], **kwargs) -> dict[str, torch.Tensor]:
|
| 239 |
-
input_ids = features["input_ids"]
|
| 240 |
-
offsets = features["offsets"]
|
| 241 |
-
# Only the "marker" mode wants the marker inside the mean; there its
|
| 242 |
-
# presence IS the mechanism. Everywhere else it must be stripped, so the
|
| 243 |
-
# control is exactly the unconditioned model and the marker row never
|
| 244 |
-
# receives a gradient.
|
| 245 |
-
keep_marker = self.mode == "marker"
|
| 246 |
-
content, segment, lengths, language, new_offsets = split_markers(
|
| 247 |
-
input_ids, offsets, self.marker_lookup, keep_marker)
|
| 248 |
-
if (language < 0).any():
|
| 249 |
-
raise ValueError(
|
| 250 |
-
"a sequence did not start with a language marker; the stream must "
|
| 251 |
-
"set streaming.language_markers and the evaluator must prepend one")
|
| 252 |
-
if self.mode in ("collapse_online", "collapse_online_compiled"):
|
| 253 |
-
vectors = self.embedding.weight[content]
|
| 254 |
-
pooled, _ = self.online_collapse.pool(
|
| 255 |
-
vectors,
|
| 256 |
-
content,
|
| 257 |
-
segment,
|
| 258 |
-
language,
|
| 259 |
-
language.shape[0],
|
| 260 |
-
embedding_weight=self.embedding.weight,
|
| 261 |
-
)
|
| 262 |
-
features["sentence_embedding"] = pooled
|
| 263 |
-
return features
|
| 264 |
-
if self.mode == "collapse_recursive":
|
| 265 |
-
pooled, emitted, emitted_segment = self.recursive_cascade.pool(
|
| 266 |
-
self.embedding.weight,
|
| 267 |
-
content,
|
| 268 |
-
segment,
|
| 269 |
-
language,
|
| 270 |
-
language.shape[0],
|
| 271 |
-
)
|
| 272 |
-
features["sentence_embedding"] = pooled
|
| 273 |
-
# Training callbacks and lifecycle smokes may inspect this detached
|
| 274 |
-
# stream; SentenceTransformers ignores additional feature entries.
|
| 275 |
-
features["recursive_token_ids"] = emitted.detach()
|
| 276 |
-
features["recursive_token_segment"] = emitted_segment.detach()
|
| 277 |
-
return features
|
| 278 |
-
if self.mode in ("collapse", "collapse_shared", "collapse_dynamic", "collapse_reclaimed"):
|
| 279 |
-
if self.mode == "collapse_reclaimed" and content.numel():
|
| 280 |
-
selected = self.reclaim_left[content] >= 0
|
| 281 |
-
if bool(selected.any()):
|
| 282 |
-
lengths_per_token = 1 + selected.to(torch.long)
|
| 283 |
-
starts = lengths_per_token.cumsum(0) - lengths_per_token
|
| 284 |
-
expanded = torch.empty(
|
| 285 |
-
int(lengths_per_token.sum()), dtype=torch.long, device=content.device
|
| 286 |
-
)
|
| 287 |
-
first = content.clone()
|
| 288 |
-
first[selected] = self.reclaim_left[content[selected]]
|
| 289 |
-
expanded[starts] = first
|
| 290 |
-
expanded[starts[selected] + 1] = self.reclaim_right[content[selected]]
|
| 291 |
-
segment = torch.repeat_interleave(segment, lengths_per_token)
|
| 292 |
-
split_per_sentence = torch.zeros_like(lengths)
|
| 293 |
-
split_per_sentence.index_add_(0, segment[starts], selected.to(torch.long))
|
| 294 |
-
lengths = lengths + split_per_sentence
|
| 295 |
-
content = expanded
|
| 296 |
-
vectors = self.embedding.weight[content]
|
| 297 |
-
collapse_language = torch.zeros_like(language) if self.mode == "collapse_shared" else language
|
| 298 |
-
features["sentence_embedding"] = self.collapse.pool(
|
| 299 |
-
vectors, content, segment, collapse_language, language.shape[0],
|
| 300 |
-
embedding_weight=(
|
| 301 |
-
self.embedding.weight if self.mode == "collapse_reclaimed" else None
|
| 302 |
-
),
|
| 303 |
-
)
|
| 304 |
-
return features
|
| 305 |
-
|
| 306 |
-
if self.mode == "idfpool":
|
| 307 |
-
# A weighted mean with a fixed per-(language, token) weight. The
|
| 308 |
-
# denominator carries the same weights, so a sentence of common
|
| 309 |
-
# tokens is not simply scaled down — it is the *relative* weight
|
| 310 |
-
# inside a sentence that changes.
|
| 311 |
-
vectors = self.embedding.weight[content]
|
| 312 |
-
weight = self.pool_weight[language[segment], content].to(vectors.dtype)
|
| 313 |
-
numerator = torch.zeros(language.shape[0], vectors.shape[1],
|
| 314 |
-
dtype=vectors.dtype, device=vectors.device)
|
| 315 |
-
numerator.index_add_(0, segment, vectors * weight.unsqueeze(1))
|
| 316 |
-
denominator = torch.zeros(language.shape[0], dtype=vectors.dtype,
|
| 317 |
-
device=vectors.device)
|
| 318 |
-
denominator.index_add_(0, segment, weight)
|
| 319 |
-
features["sentence_embedding"] = numerator / denominator.clamp(min=1e-6).unsqueeze(1)
|
| 320 |
-
return features
|
| 321 |
-
|
| 322 |
-
if self.mode == "vocabmoe":
|
| 323 |
-
shared = self.embedding.weight[content]
|
| 324 |
-
vectors = self.assign(content, language[segment], shared)
|
| 325 |
-
numerator = torch.zeros(language.shape[0], vectors.shape[1],
|
| 326 |
-
dtype=vectors.dtype, device=vectors.device)
|
| 327 |
-
numerator.index_add_(0, segment, vectors)
|
| 328 |
-
features["sentence_embedding"] = numerator / lengths.clamp(min=1).unsqueeze(1).to(vectors.dtype)
|
| 329 |
-
return features
|
| 330 |
-
|
| 331 |
-
if self.mode == "wordpool":
|
| 332 |
-
vectors = self.embedding.weight[content]
|
| 333 |
-
starts = self.word_start[content].to(torch.long)
|
| 334 |
-
# Word index inside the sentence: restart the running sum at each
|
| 335 |
-
# sentence so words never merge across the batch.
|
| 336 |
-
within = torch.cumsum(starts, 0)
|
| 337 |
-
offset = torch.zeros_like(within)
|
| 338 |
-
first = torch.zeros(language.shape[0], dtype=within.dtype, device=within.device)
|
| 339 |
-
first.scatter_reduce_(0, segment, within, reduce="amin", include_self=False)
|
| 340 |
-
offset = first[segment]
|
| 341 |
-
word = (within - offset)
|
| 342 |
-
key = segment * (word.max() + 1) + word
|
| 343 |
-
uniq, inverse = torch.unique(key, return_inverse=True)
|
| 344 |
-
wsum = torch.zeros(uniq.numel(), vectors.shape[1],
|
| 345 |
-
dtype=vectors.dtype, device=vectors.device)
|
| 346 |
-
wsum.index_add_(0, inverse, vectors)
|
| 347 |
-
wcount = torch.zeros(uniq.numel(), dtype=vectors.dtype, device=vectors.device)
|
| 348 |
-
wcount.index_add_(0, inverse, torch.ones_like(inverse, dtype=vectors.dtype))
|
| 349 |
-
word_vectors = wsum / wcount.clamp(min=1.0).unsqueeze(1)
|
| 350 |
-
word_segment = torch.zeros(uniq.numel(), dtype=torch.long, device=vectors.device)
|
| 351 |
-
word_segment.scatter_(0, inverse, segment)
|
| 352 |
-
numerator = torch.zeros(language.shape[0], vectors.shape[1],
|
| 353 |
-
dtype=vectors.dtype, device=vectors.device)
|
| 354 |
-
numerator.index_add_(0, word_segment, word_vectors)
|
| 355 |
-
counts = torch.zeros(language.shape[0], dtype=vectors.dtype, device=vectors.device)
|
| 356 |
-
counts.index_add_(0, word_segment, torch.ones_like(word_segment, dtype=vectors.dtype))
|
| 357 |
-
features["sentence_embedding"] = numerator / counts.clamp(min=1.0).unsqueeze(1)
|
| 358 |
-
return features
|
| 359 |
-
|
| 360 |
-
if self.ngram is not None:
|
| 361 |
-
vectors = self.embedding.weight[content]
|
| 362 |
-
extra, extra_segment = self.ngram.gather(content, segment)
|
| 363 |
-
weight = None
|
| 364 |
-
if self.mode == "ngram_gate" and extra.numel():
|
| 365 |
-
weight = torch.sigmoid(self.ngram_gate)[language[extra_segment]].unsqueeze(1)
|
| 366 |
-
extra = extra * weight
|
| 367 |
-
allv = torch.cat([vectors, extra]) if extra.numel() else vectors
|
| 368 |
-
alls = torch.cat([segment, extra_segment]) if extra.numel() else segment
|
| 369 |
-
numerator = torch.zeros(language.shape[0], allv.shape[1],
|
| 370 |
-
dtype=allv.dtype, device=allv.device)
|
| 371 |
-
numerator.index_add_(0, alls, allv)
|
| 372 |
-
counts = torch.zeros(language.shape[0], dtype=allv.dtype, device=allv.device)
|
| 373 |
-
unit = torch.ones_like(alls, dtype=allv.dtype)
|
| 374 |
-
if weight is not None:
|
| 375 |
-
unit[vectors.shape[0]:] = weight.squeeze(1)
|
| 376 |
-
counts.index_add_(0, alls, unit)
|
| 377 |
-
features["sentence_embedding"] = numerator / counts.clamp(min=1.0).unsqueeze(1)
|
| 378 |
-
return features
|
| 379 |
-
if self.mode in ("split", "fuse"):
|
| 380 |
-
lang_per_token = language[segment]
|
| 381 |
-
shared = self.embedding.weight[content]
|
| 382 |
-
vectors = self.split(content, lang_per_token, shared,
|
| 383 |
-
use_gate=self.mode == "split")
|
| 384 |
-
numerator = torch.zeros(language.shape[0], vectors.shape[1],
|
| 385 |
-
dtype=vectors.dtype, device=vectors.device)
|
| 386 |
-
numerator.index_add_(0, segment, vectors)
|
| 387 |
-
pooled = numerator / lengths.clamp(min=1).unsqueeze(1).to(vectors.dtype)
|
| 388 |
-
features["sentence_embedding"] = pooled
|
| 389 |
-
return features
|
| 390 |
-
if self.mode in GATING_MODES:
|
| 391 |
-
# Gating reweights tokens, so pooling has to happen here rather than
|
| 392 |
-
# in the EmbeddingBag: a weighted mean is not a mean of weighted rows
|
| 393 |
-
# unless the denominator carries the same weights.
|
| 394 |
-
vectors = self.embedding.weight[content]
|
| 395 |
-
weight = self.conditioner.token_gate(content, language[segment])
|
| 396 |
-
numerator = torch.zeros(language.shape[0], vectors.shape[1],
|
| 397 |
-
dtype=vectors.dtype, device=vectors.device)
|
| 398 |
-
numerator.index_add_(0, segment, vectors * weight.unsqueeze(1))
|
| 399 |
-
denominator = torch.zeros(language.shape[0], dtype=vectors.dtype,
|
| 400 |
-
device=vectors.device)
|
| 401 |
-
denominator.index_add_(0, segment, weight)
|
| 402 |
-
pooled = numerator / denominator.clamp(min=1e-6).unsqueeze(1)
|
| 403 |
-
else:
|
| 404 |
-
pooled = self.embedding(content, new_offsets)
|
| 405 |
-
features["sentence_embedding"] = self.conditioner(
|
| 406 |
-
pooled, language, content, segment, lengths)
|
| 407 |
-
return features
|
| 408 |
-
|
| 409 |
-
def configure_dynamic_vocabulary_discovery(
|
| 410 |
-
self,
|
| 411 |
-
*,
|
| 412 |
-
candidate_capacity: int,
|
| 413 |
-
top_per_forward: int,
|
| 414 |
-
admission_mode: str = "utility",
|
| 415 |
-
) -> None:
|
| 416 |
-
if self.mode != "collapse_dynamic" or self.collapse is None:
|
| 417 |
-
raise ValueError("dynamic vocabulary discovery requires collapse_dynamic mode")
|
| 418 |
-
self.collapse.configure_discovery(
|
| 419 |
-
candidate_capacity=candidate_capacity,
|
| 420 |
-
top_per_forward=top_per_forward,
|
| 421 |
-
admission_mode=admission_mode,
|
| 422 |
-
)
|
| 423 |
-
|
| 424 |
-
def configure_dynamic_vocabulary_diagnostic_discovery(
|
| 425 |
-
self,
|
| 426 |
-
*,
|
| 427 |
-
candidate_capacity: int,
|
| 428 |
-
top_per_forward: int,
|
| 429 |
-
admission_mode: str = "utility",
|
| 430 |
-
) -> None:
|
| 431 |
-
if self.mode != "collapse_dynamic" or self.collapse is None:
|
| 432 |
-
raise ValueError("diagnostic discovery requires collapse_dynamic mode")
|
| 433 |
-
self.collapse.configure_diagnostic_discovery(
|
| 434 |
-
candidate_capacity=candidate_capacity,
|
| 435 |
-
top_per_forward=top_per_forward,
|
| 436 |
-
admission_mode=admission_mode,
|
| 437 |
-
)
|
| 438 |
-
|
| 439 |
-
def promote_dynamic_vocabulary(
|
| 440 |
-
self,
|
| 441 |
-
pair_keys: tuple[int, ...] | list[int],
|
| 442 |
-
*,
|
| 443 |
-
optimizer: torch.optim.Optimizer | None = None,
|
| 444 |
-
) -> dict[str, int]:
|
| 445 |
-
if self.mode != "collapse_dynamic" or self.collapse is None:
|
| 446 |
-
raise ValueError("dynamic vocabulary promotion requires collapse_dynamic mode")
|
| 447 |
-
return self.collapse.promote_exact_vocabulary(pair_keys, optimizer=optimizer)
|
| 448 |
-
|
| 449 |
-
def grow_dynamic_vocabulary(
|
| 450 |
-
self,
|
| 451 |
-
pair_keys: tuple[int, ...] | list[int],
|
| 452 |
-
*,
|
| 453 |
-
optimizer: torch.optim.Optimizer,
|
| 454 |
-
residual_buckets: int,
|
| 455 |
-
) -> dict[str, int]:
|
| 456 |
-
if self.mode != "collapse_dynamic" or self.collapse is None:
|
| 457 |
-
raise ValueError("dynamic vocabulary growth requires collapse_dynamic mode")
|
| 458 |
-
report = self.collapse.grow_exact_vocabulary(
|
| 459 |
-
pair_keys,
|
| 460 |
-
optimizer=optimizer,
|
| 461 |
-
residual_buckets=residual_buckets,
|
| 462 |
-
)
|
| 463 |
-
self.ngram_buckets = int(report["active_exact_rows"])
|
| 464 |
-
self.growth_residual_buckets = int(report["residual_rows"])
|
| 465 |
-
return report
|
| 466 |
-
|
| 467 |
-
def promote_dynamic_vocabulary_to_reclaimed(
|
| 468 |
-
self,
|
| 469 |
-
pair_keys: tuple[int, ...] | list[int],
|
| 470 |
-
reclaimed_token_ids: tuple[int, ...] | list[int],
|
| 471 |
-
reclaimed_left_ids: tuple[int, ...] | list[int],
|
| 472 |
-
reclaimed_right_ids: tuple[int, ...] | list[int],
|
| 473 |
-
*,
|
| 474 |
-
optimizer: torch.optim.Optimizer,
|
| 475 |
-
) -> dict[str, int]:
|
| 476 |
-
if self.mode != "collapse_dynamic" or self.collapse is None:
|
| 477 |
-
raise ValueError("direct reclaimed promotion requires collapse_dynamic mode")
|
| 478 |
-
if not (len(pair_keys) == len(reclaimed_token_ids) == len(reclaimed_left_ids)
|
| 479 |
-
== len(reclaimed_right_ids)):
|
| 480 |
-
raise ValueError("direct reclaimed promotion arrays must align")
|
| 481 |
-
reclaimed = torch.as_tensor(
|
| 482 |
-
reclaimed_token_ids, dtype=torch.long, device=self.embedding.weight.device
|
| 483 |
-
)
|
| 484 |
-
left = torch.as_tensor(
|
| 485 |
-
reclaimed_left_ids, dtype=torch.long, device=self.embedding.weight.device
|
| 486 |
-
)
|
| 487 |
-
right = torch.as_tensor(
|
| 488 |
-
reclaimed_right_ids, dtype=torch.long, device=self.embedding.weight.device
|
| 489 |
-
)
|
| 490 |
-
if reclaimed.numel() and (
|
| 491 |
-
reclaimed.unique().numel() != reclaimed.numel()
|
| 492 |
-
or int(reclaimed.min()) < 0 or int(reclaimed.max()) >= self.embedding.weight.shape[0]
|
| 493 |
-
or int(left.min()) < 0 or int(left.max()) >= self.embedding.weight.shape[0]
|
| 494 |
-
or int(right.min()) < 0 or int(right.max()) >= self.embedding.weight.shape[0]
|
| 495 |
-
):
|
| 496 |
-
raise ValueError("invalid direct reclaimed token or parent id")
|
| 497 |
-
selected = set(int(value) for value in reclaimed_token_ids)
|
| 498 |
-
if selected & set(int(value) for value in (*reclaimed_left_ids, *reclaimed_right_ids)):
|
| 499 |
-
raise ValueError("direct reclaimed split map contains recursion")
|
| 500 |
-
key_base = int(self.collapse.pair_key_base)
|
| 501 |
-
endpoints = {
|
| 502 |
-
endpoint for key in pair_keys
|
| 503 |
-
for endpoint in (int(key) // key_base, int(key) % key_base)
|
| 504 |
-
}
|
| 505 |
-
if selected & endpoints:
|
| 506 |
-
raise ValueError("a reclaimed token is an active exact-pair endpoint")
|
| 507 |
-
|
| 508 |
-
report = self.collapse.promote_exact_vocabulary_to_reclaimed(
|
| 509 |
-
pair_keys, reclaimed, self.embedding.weight, optimizer=optimizer
|
| 510 |
-
)
|
| 511 |
-
self.reclaimed_token_ids = [int(value) for value in reclaimed_token_ids]
|
| 512 |
-
self.reclaimed_left_ids = [int(value) for value in reclaimed_left_ids]
|
| 513 |
-
self.reclaimed_right_ids = [int(value) for value in reclaimed_right_ids]
|
| 514 |
-
self.reclaim_left.fill_(-1)
|
| 515 |
-
self.reclaim_right.fill_(-1)
|
| 516 |
-
self.reclaim_left[reclaimed] = left
|
| 517 |
-
self.reclaim_right[reclaimed] = right
|
| 518 |
-
self.ngram_buckets = int(reclaimed.numel())
|
| 519 |
-
self.mode = "collapse_reclaimed"
|
| 520 |
-
self.conditioner.mode = "collapse_reclaimed"
|
| 521 |
-
return report
|
| 522 |
-
|
| 523 |
-
def compact_dynamic_vocabulary(self) -> dict[str, int]:
|
| 524 |
-
if self.mode != "collapse_dynamic" or self.collapse is None:
|
| 525 |
-
raise ValueError("dynamic vocabulary compaction requires collapse_dynamic mode")
|
| 526 |
-
report = self.collapse.compact_exact_vocabulary()
|
| 527 |
-
self.ngram_buckets = self.collapse.buckets
|
| 528 |
-
return report
|
| 529 |
-
|
| 530 |
-
def configure_recursive_vocabulary_discovery(self) -> None:
|
| 531 |
-
if self.mode != "collapse_recursive" or self.recursive_cascade is None:
|
| 532 |
-
raise ValueError("recursive discovery requires collapse_recursive mode")
|
| 533 |
-
self.recursive_cascade.configure_discovery()
|
| 534 |
-
|
| 535 |
-
def grow_recursive_vocabulary(
|
| 536 |
-
self,
|
| 537 |
-
pair_keys: tuple[int, ...] | list[int],
|
| 538 |
-
*,
|
| 539 |
-
optimizer: torch.optim.Optimizer,
|
| 540 |
-
initialization: str = "parent_mean",
|
| 541 |
-
) -> dict[str, object]:
|
| 542 |
-
if self.mode != "collapse_recursive" or self.recursive_cascade is None:
|
| 543 |
-
raise ValueError("recursive growth requires collapse_recursive mode")
|
| 544 |
-
return self.recursive_cascade.grow(
|
| 545 |
-
pair_keys,
|
| 546 |
-
base_embedding=self.embedding.weight,
|
| 547 |
-
optimizer=optimizer,
|
| 548 |
-
initialization=initialization,
|
| 549 |
-
)
|
| 550 |
-
|
| 551 |
-
def deactivate_unobserved_recursive_vocabulary(
|
| 552 |
-
self, observed_token_ids: tuple[int, ...] | list[int]
|
| 553 |
-
) -> dict[str, object]:
|
| 554 |
-
if self.mode != "collapse_recursive" or self.recursive_cascade is None:
|
| 555 |
-
raise ValueError("recursive deactivation requires collapse_recursive mode")
|
| 556 |
-
return self.recursive_cascade.deactivate_unobserved(observed_token_ids)
|
| 557 |
-
|
| 558 |
-
def reactivate_recursive_vocabulary(
|
| 559 |
-
self, pair_keys: tuple[int, ...] | list[int]
|
| 560 |
-
) -> dict[str, object]:
|
| 561 |
-
if self.mode != "collapse_recursive" or self.recursive_cascade is None:
|
| 562 |
-
raise ValueError("recursive reactivation requires collapse_recursive mode")
|
| 563 |
-
return self.recursive_cascade.reactivate_pair_keys(pair_keys)
|
| 564 |
-
|
| 565 |
-
def save(self, output_path: str, *args, safe_serialization: bool = True, **kwargs) -> None:
|
| 566 |
-
super().save(output_path, *args, safe_serialization=safe_serialization, **kwargs)
|
| 567 |
-
Path(output_path, CONFIG_NAME).write_text(json.dumps({
|
| 568 |
-
"mode": self.mode,
|
| 569 |
-
"languages": self.languages,
|
| 570 |
-
"code_dim": self.conditioner.code_dim,
|
| 571 |
-
"n_senses": self.conditioner.n_senses,
|
| 572 |
-
"rank": self.conditioner.rank,
|
| 573 |
-
"split_index": self.split_index,
|
| 574 |
-
"ngram_buckets": self.ngram_buckets,
|
| 575 |
-
"growth_residual_buckets": self.growth_residual_buckets,
|
| 576 |
-
"n_vocabs": self.n_vocabs,
|
| 577 |
-
"weight_index": self.weight_index,
|
| 578 |
-
"dynamic_pair_keys": (
|
| 579 |
-
self.collapse.pair_keys.detach().cpu().tolist()
|
| 580 |
-
if self.mode in ("collapse_dynamic", "collapse_reclaimed")
|
| 581 |
-
and self.collapse is not None else None
|
| 582 |
-
),
|
| 583 |
-
"dynamic_pair_slots": (
|
| 584 |
-
self.collapse.pair_slots.detach().cpu().tolist()
|
| 585 |
-
if self.mode in ("collapse_dynamic", "collapse_reclaimed")
|
| 586 |
-
and self.collapse is not None else None
|
| 587 |
-
),
|
| 588 |
-
"reclaimed_token_ids": self.reclaimed_token_ids,
|
| 589 |
-
"reclaimed_left_ids": self.reclaimed_left_ids,
|
| 590 |
-
"reclaimed_right_ids": self.reclaimed_right_ids,
|
| 591 |
-
"online_rank": self.online_rank,
|
| 592 |
-
"compiled_pair_keys": None,
|
| 593 |
-
"compiled_pair_scores": None,
|
| 594 |
-
"compiled_pair_count": (
|
| 595 |
-
int(self.online_collapse.compiled_pair_keys.numel())
|
| 596 |
-
if self.mode == "collapse_online_compiled"
|
| 597 |
-
and self.online_collapse is not None else None
|
| 598 |
-
),
|
| 599 |
-
"recursive_max_span_length": self.recursive_max_span_length,
|
| 600 |
-
"recursive_learned_count": (
|
| 601 |
-
self.recursive_cascade.learned_count
|
| 602 |
-
if self.mode == "collapse_recursive"
|
| 603 |
-
and self.recursive_cascade is not None else None
|
| 604 |
-
),
|
| 605 |
-
"recursive_active_count": (
|
| 606 |
-
self.recursive_cascade.active_count
|
| 607 |
-
if self.mode == "collapse_recursive"
|
| 608 |
-
and self.recursive_cascade is not None else None
|
| 609 |
-
),
|
| 610 |
-
}, ensure_ascii=False, indent=2))
|
| 611 |
-
|
| 612 |
-
@classmethod
|
| 613 |
-
def load(
|
| 614 |
-
cls,
|
| 615 |
-
model_name_or_path: str,
|
| 616 |
-
subfolder: str = "",
|
| 617 |
-
token: bool | str | None = None,
|
| 618 |
-
cache_folder: str | None = None,
|
| 619 |
-
revision: str | None = None,
|
| 620 |
-
local_files_only: bool = False,
|
| 621 |
-
**kwargs,
|
| 622 |
-
):
|
| 623 |
-
from safetensors.torch import load_file
|
| 624 |
-
|
| 625 |
-
root_path = cls.load_dir_path(
|
| 626 |
-
model_name_or_path=model_name_or_path,
|
| 627 |
-
subfolder=subfolder,
|
| 628 |
-
token=token,
|
| 629 |
-
cache_folder=cache_folder,
|
| 630 |
-
revision=revision,
|
| 631 |
-
local_files_only=local_files_only,
|
| 632 |
-
)
|
| 633 |
-
if root_path is None:
|
| 634 |
-
raise FileNotFoundError(
|
| 635 |
-
f"could not resolve conditioned static model {model_name_or_path!r}"
|
| 636 |
-
)
|
| 637 |
-
root = Path(root_path)
|
| 638 |
-
config = json.loads((root / CONFIG_NAME).read_text())
|
| 639 |
-
tokenizer = Tokenizer.from_file(str(root / "tokenizer.json"))
|
| 640 |
-
state = load_file(str(root / "model.safetensors"))
|
| 641 |
-
weights = state["embedding.weight"]
|
| 642 |
-
compiled_pair_keys = config.get("compiled_pair_keys")
|
| 643 |
-
compiled_pair_scores = config.get("compiled_pair_scores")
|
| 644 |
-
if config["mode"] == "collapse_online_compiled" and compiled_pair_keys is None:
|
| 645 |
-
compiled_pair_keys = state["online_collapse.compiled_pair_keys"]
|
| 646 |
-
compiled_pair_scores = state["online_collapse.compiled_pair_scores"]
|
| 647 |
-
expected_count = config.get("compiled_pair_count")
|
| 648 |
-
if expected_count is not None and int(expected_count) != compiled_pair_keys.numel():
|
| 649 |
-
raise ValueError("compiled pair count disagrees with model state")
|
| 650 |
-
recursive_restored_state = None
|
| 651 |
-
if config["mode"] == "collapse_recursive":
|
| 652 |
-
prefix = "recursive_cascade."
|
| 653 |
-
recursive_names = {
|
| 654 |
-
"learned", "rule_left", "rule_right", "rule_generation",
|
| 655 |
-
"span_offsets", "span_values",
|
| 656 |
-
}
|
| 657 |
-
if f"{prefix}rule_active" in state:
|
| 658 |
-
recursive_names.add("rule_active")
|
| 659 |
-
recursive_restored_state = {
|
| 660 |
-
name: state[f"{prefix}{name}"] for name in recursive_names
|
| 661 |
-
}
|
| 662 |
-
expected_count = config.get("recursive_learned_count")
|
| 663 |
-
if (
|
| 664 |
-
expected_count is not None
|
| 665 |
-
and int(expected_count) != recursive_restored_state["learned"].shape[0]
|
| 666 |
-
):
|
| 667 |
-
raise ValueError("recursive learned count disagrees with model state")
|
| 668 |
-
expected_active = config.get("recursive_active_count")
|
| 669 |
-
active = recursive_restored_state.get(
|
| 670 |
-
"rule_active",
|
| 671 |
-
torch.ones(
|
| 672 |
-
recursive_restored_state["learned"].shape[0], dtype=torch.bool
|
| 673 |
-
),
|
| 674 |
-
)
|
| 675 |
-
if expected_active is not None and int(expected_active) != int(active.sum()):
|
| 676 |
-
raise ValueError("recursive active count disagrees with model state")
|
| 677 |
-
module = cls(tokenizer, embedding_weights=weights,
|
| 678 |
-
languages=config["languages"], mode=config["mode"],
|
| 679 |
-
code_dim=config["code_dim"], n_senses=config["n_senses"],
|
| 680 |
-
rank=config["rank"], split_index=config.get("split_index"),
|
| 681 |
-
ngram_buckets=config.get("ngram_buckets", 0),
|
| 682 |
-
n_vocabs=config.get("n_vocabs", 4),
|
| 683 |
-
weight_index=config.get("weight_index"),
|
| 684 |
-
dynamic_pair_keys=config.get("dynamic_pair_keys"),
|
| 685 |
-
dynamic_pair_slots=config.get("dynamic_pair_slots"),
|
| 686 |
-
growth_residual_buckets=config.get("growth_residual_buckets", 0),
|
| 687 |
-
reclaimed_token_ids=config.get("reclaimed_token_ids"),
|
| 688 |
-
reclaimed_left_ids=config.get("reclaimed_left_ids"),
|
| 689 |
-
reclaimed_right_ids=config.get("reclaimed_right_ids"),
|
| 690 |
-
online_rank=config.get("online_rank", 32),
|
| 691 |
-
compiled_pair_keys=compiled_pair_keys,
|
| 692 |
-
compiled_pair_scores=compiled_pair_scores,
|
| 693 |
-
recursive_max_span_length=config.get(
|
| 694 |
-
"recursive_max_span_length", 32
|
| 695 |
-
),
|
| 696 |
-
recursive_restored_state=recursive_restored_state)
|
| 697 |
-
module.load_state_dict(state, strict=False)
|
| 698 |
-
return module
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
def build(
|
| 702 |
-
base_dir: str | os.PathLike,
|
| 703 |
-
languages: list[str],
|
| 704 |
-
mode: str,
|
| 705 |
-
*,
|
| 706 |
-
code_dim: int = 16,
|
| 707 |
-
n_senses: int = 64,
|
| 708 |
-
rank: int = 8,
|
| 709 |
-
split_index: str | None = None,
|
| 710 |
-
ngram_buckets: int = 0,
|
| 711 |
-
n_vocabs: int = 4,
|
| 712 |
-
weight_index: str | None = None,
|
| 713 |
-
online_rank: int = 32,
|
| 714 |
-
) -> ConditionedStaticEmbedding:
|
| 715 |
-
"""Extend an unconditioned StaRSE checkpoint with language markers.
|
| 716 |
-
|
| 717 |
-
The marker rows are appended to the table and initialised to zero, and every
|
| 718 |
-
conditioning parameter is initialised so the conditioner is the identity, so
|
| 719 |
-
the model starts numerically equal to the checkpoint it came from. Any later
|
| 720 |
-
difference is attributable to the mechanism rather than to a different
|
| 721 |
-
starting point.
|
| 722 |
-
"""
|
| 723 |
-
from safetensors.torch import load_file
|
| 724 |
-
|
| 725 |
-
base = Path(base_dir)
|
| 726 |
-
tokenizer = Tokenizer.from_file(str(base / "tokenizer.json"))
|
| 727 |
-
weights = load_file(str(base / "model.safetensors"))["embedding.weight"]
|
| 728 |
-
|
| 729 |
-
markers = [marker_for(language) for language in languages]
|
| 730 |
-
added = tokenizer.add_special_tokens(markers)
|
| 731 |
-
if added:
|
| 732 |
-
extra = torch.zeros(added, weights.shape[1], dtype=weights.dtype)
|
| 733 |
-
weights = torch.cat([weights, extra], dim=0)
|
| 734 |
-
return ConditionedStaticEmbedding(
|
| 735 |
-
tokenizer, embedding_weights=weights, languages=languages, mode=mode,
|
| 736 |
-
code_dim=code_dim, n_senses=n_senses, rank=rank, split_index=split_index,
|
| 737 |
-
ngram_buckets=ngram_buckets, n_vocabs=n_vocabs, weight_index=weight_index,
|
| 738 |
-
online_rank=online_rank)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/gradient_safety.py
DELETED
|
@@ -1,161 +0,0 @@
|
|
| 1 |
-
"""Overflow-safe gradient validation, measurement and clipping."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections import defaultdict
|
| 6 |
-
from collections.abc import Callable, Iterable
|
| 7 |
-
from dataclasses import dataclass
|
| 8 |
-
import math
|
| 9 |
-
from typing import Any
|
| 10 |
-
|
| 11 |
-
import torch
|
| 12 |
-
from transformers import TrainerCallback
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@dataclass(frozen=True)
|
| 16 |
-
class GradientReport:
|
| 17 |
-
"""Detached scalar evidence from one pre-optimizer gradient pass."""
|
| 18 |
-
|
| 19 |
-
total_norm: float
|
| 20 |
-
clip_coefficient: float
|
| 21 |
-
max_abs: float
|
| 22 |
-
group_norms: dict[str, float]
|
| 23 |
-
parameter_count: int
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def stable_gradient_target(config: dict[str, Any]) -> float | None:
|
| 27 |
-
"""Validate the opt-in training contract and return its fixed norm target."""
|
| 28 |
-
|
| 29 |
-
raw = config.get("vocabulary_telemetry")
|
| 30 |
-
if not raw:
|
| 31 |
-
return None
|
| 32 |
-
if not isinstance(raw, dict):
|
| 33 |
-
raise ValueError("vocabulary telemetry settings must be an object")
|
| 34 |
-
if raw.get("protocol") != "vocabulary-telemetry-v1":
|
| 35 |
-
raise ValueError("vocabulary telemetry has an unknown protocol")
|
| 36 |
-
target = raw.get("stable_max_grad_norm")
|
| 37 |
-
if isinstance(target, bool) or not isinstance(target, (int, float)):
|
| 38 |
-
raise ValueError("vocabulary telemetry requires stable_max_grad_norm=1.0")
|
| 39 |
-
if not math.isfinite(float(target)) or float(target) != 1.0:
|
| 40 |
-
raise ValueError("vocabulary telemetry requires stable_max_grad_norm=1.0")
|
| 41 |
-
return 1.0
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def parameter_group_name(name: str) -> str:
|
| 45 |
-
"""Map SentenceTransformer parameter names to stable telemetry groups."""
|
| 46 |
-
|
| 47 |
-
if "embedding.weight" in name:
|
| 48 |
-
return "base_embedding"
|
| 49 |
-
if name.endswith("collapse.merged") or name.endswith("online_collapse.merged"):
|
| 50 |
-
return "merge_vectors"
|
| 51 |
-
if name.endswith("collapse.score") or name.endswith("online_collapse.score_hash"):
|
| 52 |
-
return "global_scores"
|
| 53 |
-
if name.endswith("collapse.language_bias") or name.endswith(
|
| 54 |
-
"online_collapse.language_bias"
|
| 55 |
-
):
|
| 56 |
-
return "language_bias"
|
| 57 |
-
if "online_collapse." in name and name.rsplit(".", 1)[-1] in {
|
| 58 |
-
"left_projection",
|
| 59 |
-
"right_projection",
|
| 60 |
-
"interaction_weight",
|
| 61 |
-
}:
|
| 62 |
-
return "amortized_scorer"
|
| 63 |
-
return "other"
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def _scaled_squared_norm(gradient: torch.Tensor, *, name: str) -> tuple[float, float]:
|
| 67 |
-
if gradient.is_sparse:
|
| 68 |
-
raise TypeError(f"sparse gradient is unsupported by AdamW: {name}")
|
| 69 |
-
detached = gradient.detach()
|
| 70 |
-
maximum = detached.abs().max()
|
| 71 |
-
if not bool(torch.isfinite(maximum)):
|
| 72 |
-
raise FloatingPointError(f"nonfinite gradient in {name}")
|
| 73 |
-
max_abs = float(maximum.item())
|
| 74 |
-
if max_abs == 0.0:
|
| 75 |
-
return 0.0, 0.0
|
| 76 |
-
scaled_squared = (
|
| 77 |
-
(detached.float() / maximum.float()).square().sum(dtype=torch.float64)
|
| 78 |
-
)
|
| 79 |
-
squared_norm = maximum.double().square() * scaled_squared
|
| 80 |
-
if not bool(torch.isfinite(squared_norm)):
|
| 81 |
-
raise FloatingPointError(f"nonfinite stable gradient norm in {name}")
|
| 82 |
-
return float(squared_norm.item()), max_abs
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def stable_clip_gradients(
|
| 86 |
-
named_parameters: Iterable[tuple[str, torch.nn.Parameter]],
|
| 87 |
-
*,
|
| 88 |
-
max_norm: float,
|
| 89 |
-
group_name: Callable[[str], str],
|
| 90 |
-
) -> GradientReport:
|
| 91 |
-
"""Validate and clip dense gradients without float32 norm overflow."""
|
| 92 |
-
|
| 93 |
-
if not math.isfinite(max_norm) or max_norm <= 0.0:
|
| 94 |
-
raise ValueError("max_norm must be positive and finite")
|
| 95 |
-
|
| 96 |
-
gradients: list[tuple[str, torch.Tensor]] = []
|
| 97 |
-
squared_norms: list[float] = []
|
| 98 |
-
grouped: dict[str, list[float]] = defaultdict(list)
|
| 99 |
-
maximum = 0.0
|
| 100 |
-
for name, parameter in named_parameters:
|
| 101 |
-
gradient = parameter.grad
|
| 102 |
-
if gradient is None:
|
| 103 |
-
continue
|
| 104 |
-
squared_norm, max_abs = _scaled_squared_norm(gradient, name=name)
|
| 105 |
-
gradients.append((name, gradient))
|
| 106 |
-
squared_norms.append(squared_norm)
|
| 107 |
-
grouped[group_name(name)].append(squared_norm)
|
| 108 |
-
maximum = max(maximum, max_abs)
|
| 109 |
-
if not gradients:
|
| 110 |
-
raise RuntimeError("optimizer step has no gradients")
|
| 111 |
-
|
| 112 |
-
total_norm = math.sqrt(math.fsum(squared_norms))
|
| 113 |
-
if not math.isfinite(total_norm):
|
| 114 |
-
raise FloatingPointError("nonfinite stable total gradient norm")
|
| 115 |
-
coefficient = 1.0 if total_norm == 0.0 else min(1.0, max_norm / total_norm)
|
| 116 |
-
with torch.no_grad():
|
| 117 |
-
if coefficient < 1.0:
|
| 118 |
-
for _name, gradient in gradients:
|
| 119 |
-
gradient.mul_(coefficient)
|
| 120 |
-
for name, gradient in gradients:
|
| 121 |
-
if not bool(torch.isfinite(gradient).all()):
|
| 122 |
-
raise FloatingPointError(f"nonfinite post-clip gradient in {name}")
|
| 123 |
-
|
| 124 |
-
return GradientReport(
|
| 125 |
-
total_norm=total_norm,
|
| 126 |
-
clip_coefficient=coefficient,
|
| 127 |
-
max_abs=maximum,
|
| 128 |
-
group_norms={
|
| 129 |
-
name: math.sqrt(math.fsum(values))
|
| 130 |
-
for name, values in sorted(grouped.items())
|
| 131 |
-
},
|
| 132 |
-
parameter_count=len(gradients),
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
class StableGradientCallback(TrainerCallback):
|
| 137 |
-
"""Close online cache auditing, then safely clip either training arm."""
|
| 138 |
-
|
| 139 |
-
def __init__(self, module: torch.nn.Module, *, max_norm: float) -> None:
|
| 140 |
-
if not math.isfinite(max_norm) or max_norm <= 0.0:
|
| 141 |
-
raise ValueError("max_norm must be positive and finite")
|
| 142 |
-
self.module = module
|
| 143 |
-
self.max_norm = float(max_norm)
|
| 144 |
-
self.completed_cache_audits = 0
|
| 145 |
-
self.completed_steps = 0
|
| 146 |
-
self.last_report: GradientReport | None = None
|
| 147 |
-
|
| 148 |
-
def on_pre_optimizer_step(self, args, state, control, model=None, **kwargs):
|
| 149 |
-
del args, state, control, kwargs
|
| 150 |
-
if model is None:
|
| 151 |
-
raise RuntimeError("stable gradient callback requires the trainer model")
|
| 152 |
-
if getattr(self.module, "mode", "") == "collapse_online":
|
| 153 |
-
self.module.online_collapse.assert_cache_audit_empty()
|
| 154 |
-
self.completed_cache_audits += 1
|
| 155 |
-
report = stable_clip_gradients(
|
| 156 |
-
model.named_parameters(),
|
| 157 |
-
max_norm=self.max_norm,
|
| 158 |
-
group_name=parameter_group_name,
|
| 159 |
-
)
|
| 160 |
-
self.last_report = report
|
| 161 |
-
self.completed_steps += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/grove_counterfactual.py
DELETED
|
@@ -1,293 +0,0 @@
|
|
| 1 |
-
"""Pure primitives for cross-window dormant residual evaluation."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import itertools
|
| 6 |
-
from collections import Counter
|
| 7 |
-
from collections.abc import Sequence
|
| 8 |
-
from dataclasses import dataclass
|
| 9 |
-
|
| 10 |
-
import numpy as np
|
| 11 |
-
import torch
|
| 12 |
-
import torch.nn.functional as F
|
| 13 |
-
|
| 14 |
-
PAIR_KEY_BASE = 1 << 32
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
@dataclass(frozen=True)
|
| 18 |
-
class DormantBatchStatistics:
|
| 19 |
-
gradient_sum: torch.Tensor
|
| 20 |
-
fisher_diagonal: torch.Tensor
|
| 21 |
-
occurrence_support: torch.Tensor
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
class CandidateKeyIndex:
|
| 25 |
-
"""Validate a sorted candidate universe once for repeated sentence scans."""
|
| 26 |
-
|
| 27 |
-
def __init__(self, candidate_keys: np.ndarray) -> None:
|
| 28 |
-
self.keys = _validated_candidate_keys(candidate_keys)
|
| 29 |
-
|
| 30 |
-
def sentence_counts(
|
| 31 |
-
self, token_ids: Sequence[int]
|
| 32 |
-
) -> tuple[np.ndarray, np.ndarray]:
|
| 33 |
-
return _sentence_candidate_counts(token_ids, self.keys)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def pack_pair_keys(left: np.ndarray, right: np.ndarray) -> np.ndarray:
|
| 37 |
-
left_array = np.asarray(left)
|
| 38 |
-
right_array = np.asarray(right)
|
| 39 |
-
if left_array.shape != right_array.shape:
|
| 40 |
-
raise ValueError("pair key sides must have the same shape")
|
| 41 |
-
if left_array.ndim != 1:
|
| 42 |
-
raise ValueError("pair key sides must be one-dimensional")
|
| 43 |
-
if not np.issubdtype(left_array.dtype, np.integer) or not np.issubdtype(
|
| 44 |
-
right_array.dtype, np.integer
|
| 45 |
-
):
|
| 46 |
-
raise ValueError("pair token ids must be integers")
|
| 47 |
-
if (
|
| 48 |
-
(left_array < 0).any()
|
| 49 |
-
or (right_array < 0).any()
|
| 50 |
-
or (left_array >= PAIR_KEY_BASE).any()
|
| 51 |
-
or (right_array >= PAIR_KEY_BASE).any()
|
| 52 |
-
):
|
| 53 |
-
raise ValueError("pair token ids must fit uint32")
|
| 54 |
-
left_u64 = left_array.astype(np.uint64, copy=False)
|
| 55 |
-
right_u64 = right_array.astype(np.uint64, copy=False)
|
| 56 |
-
return (left_u64 << np.uint64(32)) | right_u64
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def _validated_candidate_keys(candidate_keys: np.ndarray) -> np.ndarray:
|
| 60 |
-
keys = np.asarray(candidate_keys)
|
| 61 |
-
if keys.ndim != 1 or keys.dtype != np.uint64:
|
| 62 |
-
raise ValueError("candidate keys must be a one-dimensional uint64 array")
|
| 63 |
-
if len(keys) > 1 and bool((keys[1:] <= keys[:-1]).any()):
|
| 64 |
-
raise ValueError("candidate keys must be strictly increasing")
|
| 65 |
-
return keys
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def _sentence_candidate_counts(
|
| 69 |
-
token_ids: Sequence[int], keys: np.ndarray
|
| 70 |
-
) -> tuple[np.ndarray, np.ndarray]:
|
| 71 |
-
tokens = [int(token) for token in token_ids]
|
| 72 |
-
if any(token < 0 or token >= PAIR_KEY_BASE for token in tokens):
|
| 73 |
-
raise ValueError("sentence token ids must fit uint32")
|
| 74 |
-
if len(tokens) < 2 or len(keys) == 0:
|
| 75 |
-
return np.empty(0, dtype=np.int64), np.empty(0, dtype=np.int32)
|
| 76 |
-
|
| 77 |
-
counts: Counter[int] = Counter()
|
| 78 |
-
for left, right in itertools.pairwise(tokens):
|
| 79 |
-
if left != right:
|
| 80 |
-
counts[(left << 32) | right] += 1
|
| 81 |
-
start = 0
|
| 82 |
-
while start < len(tokens):
|
| 83 |
-
end = start + 1
|
| 84 |
-
while end < len(tokens) and tokens[end] == tokens[start]:
|
| 85 |
-
end += 1
|
| 86 |
-
matched = (end - start) // 2
|
| 87 |
-
if matched:
|
| 88 |
-
counts[(tokens[start] << 32) | tokens[start]] += matched
|
| 89 |
-
start = end
|
| 90 |
-
|
| 91 |
-
rows_and_counts: list[tuple[int, int]] = []
|
| 92 |
-
for key, count in counts.items():
|
| 93 |
-
row = int(np.searchsorted(keys, np.uint64(key)))
|
| 94 |
-
if row < len(keys) and int(keys[row]) == key:
|
| 95 |
-
rows_and_counts.append((row, count))
|
| 96 |
-
rows_and_counts.sort()
|
| 97 |
-
return (
|
| 98 |
-
np.asarray([row for row, _count in rows_and_counts], dtype=np.int64),
|
| 99 |
-
np.asarray([count for _row, count in rows_and_counts], dtype=np.int32),
|
| 100 |
-
)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
def sentence_candidate_counts(
|
| 104 |
-
token_ids: Sequence[int], candidate_keys: np.ndarray
|
| 105 |
-
) -> tuple[np.ndarray, np.ndarray]:
|
| 106 |
-
return _sentence_candidate_counts(
|
| 107 |
-
token_ids, _validated_candidate_keys(candidate_keys)
|
| 108 |
-
)
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def batch_dormant_statistics(
|
| 112 |
-
candidate_rows: torch.Tensor,
|
| 113 |
-
sentence_rows: torch.Tensor,
|
| 114 |
-
multiplicities: torch.Tensor,
|
| 115 |
-
sentence_gradients: torch.Tensor,
|
| 116 |
-
candidate_count: int,
|
| 117 |
-
) -> DormantBatchStatistics:
|
| 118 |
-
if (
|
| 119 |
-
sentence_gradients.ndim != 2
|
| 120 |
-
or not sentence_gradients.is_floating_point()
|
| 121 |
-
or sentence_gradients.shape[0] <= 0
|
| 122 |
-
or sentence_gradients.shape[1] <= 0
|
| 123 |
-
or not bool(torch.isfinite(sentence_gradients).all())
|
| 124 |
-
):
|
| 125 |
-
raise ValueError("sentence gradients must be a finite floating matrix")
|
| 126 |
-
if int(candidate_count) <= 0:
|
| 127 |
-
raise ValueError("candidate count must be positive")
|
| 128 |
-
tensors = (candidate_rows, sentence_rows, multiplicities)
|
| 129 |
-
if any(tensor.ndim != 1 for tensor in tensors) or len(
|
| 130 |
-
{int(tensor.numel()) for tensor in tensors}
|
| 131 |
-
) != 1:
|
| 132 |
-
raise ValueError("sparse occurrence arrays must be aligned vectors")
|
| 133 |
-
if any(tensor.is_floating_point() for tensor in tensors):
|
| 134 |
-
raise ValueError("sparse occurrence arrays must be integral")
|
| 135 |
-
|
| 136 |
-
device = sentence_gradients.device
|
| 137 |
-
candidates = candidate_rows.to(device=device, dtype=torch.long)
|
| 138 |
-
sentences = sentence_rows.to(device=device, dtype=torch.long)
|
| 139 |
-
counts = multiplicities.to(device=device, dtype=torch.long)
|
| 140 |
-
if candidates.numel() and (
|
| 141 |
-
int(candidates.min()) < 0
|
| 142 |
-
or int(candidates.max()) >= int(candidate_count)
|
| 143 |
-
or int(sentences.min()) < 0
|
| 144 |
-
or int(sentences.max()) >= sentence_gradients.shape[0]
|
| 145 |
-
or int(counts.min()) <= 0
|
| 146 |
-
):
|
| 147 |
-
raise ValueError("sparse occurrence values are out of bounds")
|
| 148 |
-
|
| 149 |
-
dimension = int(sentence_gradients.shape[1])
|
| 150 |
-
gradient_sum = torch.zeros(
|
| 151 |
-
(int(candidate_count), dimension), dtype=torch.float64, device=device
|
| 152 |
-
)
|
| 153 |
-
fisher = torch.zeros_like(gradient_sum)
|
| 154 |
-
support = torch.zeros(int(candidate_count), dtype=torch.int64, device=device)
|
| 155 |
-
if not candidates.numel():
|
| 156 |
-
return DormantBatchStatistics(gradient_sum, fisher, support)
|
| 157 |
-
|
| 158 |
-
pair_rows = candidates * sentence_gradients.shape[0] + sentences
|
| 159 |
-
unique_pairs, inverse = torch.unique(pair_rows, sorted=True, return_inverse=True)
|
| 160 |
-
coalesced = torch.zeros(
|
| 161 |
-
unique_pairs.shape[0], dtype=torch.int64, device=device
|
| 162 |
-
)
|
| 163 |
-
coalesced.index_add_(0, inverse, counts)
|
| 164 |
-
coalesced_candidates = torch.div(
|
| 165 |
-
unique_pairs, sentence_gradients.shape[0], rounding_mode="floor"
|
| 166 |
-
)
|
| 167 |
-
coalesced_sentences = unique_pairs % sentence_gradients.shape[0]
|
| 168 |
-
q = sentence_gradients[coalesced_sentences].to(torch.float64)
|
| 169 |
-
k = coalesced.to(torch.float64)
|
| 170 |
-
gradient_sum.index_add_(0, coalesced_candidates, k[:, None] * q)
|
| 171 |
-
fisher.index_add_(0, coalesced_candidates, k.square()[:, None] * q.square())
|
| 172 |
-
support.index_add_(0, coalesced_candidates, coalesced)
|
| 173 |
-
return DormantBatchStatistics(gradient_sum, fisher, support)
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def _validate_statistics(
|
| 177 |
-
gradient_sum: torch.Tensor, fisher_diagonal: torch.Tensor
|
| 178 |
-
) -> None:
|
| 179 |
-
if (
|
| 180 |
-
gradient_sum.ndim != 2
|
| 181 |
-
or gradient_sum.shape != fisher_diagonal.shape
|
| 182 |
-
or not gradient_sum.is_floating_point()
|
| 183 |
-
or not fisher_diagonal.is_floating_point()
|
| 184 |
-
or gradient_sum.device != fisher_diagonal.device
|
| 185 |
-
or not bool(torch.isfinite(gradient_sum).all())
|
| 186 |
-
or not bool(torch.isfinite(fisher_diagonal).all())
|
| 187 |
-
or bool((fisher_diagonal < 0).any())
|
| 188 |
-
):
|
| 189 |
-
raise ValueError("dormant statistics are invalid")
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def coherence_scores(
|
| 193 |
-
gradient_sum: torch.Tensor, fisher_diagonal: torch.Tensor
|
| 194 |
-
) -> torch.Tensor:
|
| 195 |
-
_validate_statistics(gradient_sum, fisher_diagonal)
|
| 196 |
-
return gradient_sum.square().sum(dim=1) / (
|
| 197 |
-
fisher_diagonal.sum(dim=1) + 1e-12
|
| 198 |
-
)
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
def diagonal_fisher_scores(
|
| 202 |
-
gradient_sum: torch.Tensor, fisher_diagonal: torch.Tensor
|
| 203 |
-
) -> torch.Tensor:
|
| 204 |
-
_validate_statistics(gradient_sum, fisher_diagonal)
|
| 205 |
-
ridge = 1e-3 * fisher_diagonal.mean(dim=1) + 1e-12
|
| 206 |
-
return 0.5 * (
|
| 207 |
-
gradient_sum.square() / (fisher_diagonal + ridge[:, None])
|
| 208 |
-
).sum(dim=1)
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
def unit_descent_directions(gradient_sum: torch.Tensor) -> torch.Tensor:
|
| 212 |
-
if (
|
| 213 |
-
gradient_sum.ndim != 2
|
| 214 |
-
or not gradient_sum.is_floating_point()
|
| 215 |
-
or not bool(torch.isfinite(gradient_sum).all())
|
| 216 |
-
):
|
| 217 |
-
raise ValueError("gradient sum must be a finite floating matrix")
|
| 218 |
-
norms = gradient_sum.norm(dim=1, keepdim=True)
|
| 219 |
-
return torch.where(norms > 0, -gradient_sum / norms.clamp_min(1e-300), 0.0)
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
def candidate_info_nce_losses(
|
| 223 |
-
anchor: torch.Tensor,
|
| 224 |
-
positive: torch.Tensor,
|
| 225 |
-
anchor_counts: torch.Tensor,
|
| 226 |
-
positive_counts: torch.Tensor,
|
| 227 |
-
residuals: torch.Tensor,
|
| 228 |
-
scale: float,
|
| 229 |
-
) -> torch.Tensor:
|
| 230 |
-
if (
|
| 231 |
-
anchor.ndim != 2
|
| 232 |
-
or anchor.shape != positive.shape
|
| 233 |
-
or not anchor.is_floating_point()
|
| 234 |
-
or positive.dtype != anchor.dtype
|
| 235 |
-
or positive.device != anchor.device
|
| 236 |
-
or not bool(torch.isfinite(anchor).all())
|
| 237 |
-
or not bool(torch.isfinite(positive).all())
|
| 238 |
-
):
|
| 239 |
-
raise ValueError("base InfoNCE numerators are invalid")
|
| 240 |
-
candidate_count, dimension = residuals.shape if residuals.ndim == 2 else (-1, -1)
|
| 241 |
-
expected_counts = (candidate_count, anchor.shape[0])
|
| 242 |
-
if (
|
| 243 |
-
residuals.ndim != 2
|
| 244 |
-
or dimension != anchor.shape[1]
|
| 245 |
-
or not residuals.is_floating_point()
|
| 246 |
-
or residuals.dtype != anchor.dtype
|
| 247 |
-
or residuals.device != anchor.device
|
| 248 |
-
or anchor_counts.shape != expected_counts
|
| 249 |
-
or positive_counts.shape != expected_counts
|
| 250 |
-
or anchor_counts.device != anchor.device
|
| 251 |
-
or positive_counts.device != anchor.device
|
| 252 |
-
or not bool(torch.isfinite(residuals).all())
|
| 253 |
-
or bool((anchor_counts < 0).any())
|
| 254 |
-
or bool((positive_counts < 0).any())
|
| 255 |
-
):
|
| 256 |
-
raise ValueError("candidate residual inputs are invalid")
|
| 257 |
-
if candidate_count == 0:
|
| 258 |
-
return torch.empty(0, dtype=anchor.dtype, device=anchor.device)
|
| 259 |
-
|
| 260 |
-
changed_anchor = (
|
| 261 |
-
anchor[None, :, :]
|
| 262 |
-
+ anchor_counts.to(anchor.dtype)[:, :, None] * residuals[:, None, :]
|
| 263 |
-
)
|
| 264 |
-
changed_positive = (
|
| 265 |
-
positive[None, :, :]
|
| 266 |
-
+ positive_counts.to(anchor.dtype)[:, :, None] * residuals[:, None, :]
|
| 267 |
-
)
|
| 268 |
-
normalized_anchor = F.normalize(changed_anchor, dim=2)
|
| 269 |
-
normalized_positive = F.normalize(changed_positive, dim=2)
|
| 270 |
-
scores = torch.matmul(
|
| 271 |
-
normalized_anchor, normalized_positive.transpose(1, 2)
|
| 272 |
-
) * float(scale)
|
| 273 |
-
labels = torch.arange(anchor.shape[0], device=anchor.device).expand(
|
| 274 |
-
candidate_count, -1
|
| 275 |
-
)
|
| 276 |
-
losses = 0.5 * (
|
| 277 |
-
F.cross_entropy(scores, labels, reduction="none").mean(dim=1)
|
| 278 |
-
+ F.cross_entropy(scores.transpose(1, 2), labels, reduction="none").mean(
|
| 279 |
-
dim=1
|
| 280 |
-
)
|
| 281 |
-
)
|
| 282 |
-
zero_residual = residuals.eq(0).all(dim=1)
|
| 283 |
-
if bool(zero_residual.any()):
|
| 284 |
-
base_scores = F.normalize(anchor, dim=1) @ F.normalize(
|
| 285 |
-
positive, dim=1
|
| 286 |
-
).t() * float(scale)
|
| 287 |
-
base_labels = torch.arange(anchor.shape[0], device=anchor.device)
|
| 288 |
-
base_loss = 0.5 * (
|
| 289 |
-
F.cross_entropy(base_scores, base_labels)
|
| 290 |
-
+ F.cross_entropy(base_scores.t(), base_labels)
|
| 291 |
-
)
|
| 292 |
-
losses = torch.where(zero_residual, base_loss, losses)
|
| 293 |
-
return losses
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/grove_selection.py
DELETED
|
@@ -1,202 +0,0 @@
|
|
| 1 |
-
"""Deterministic candidate selection and the frozen GROVE gate."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections.abc import Mapping, Sequence
|
| 6 |
-
from typing import Any
|
| 7 |
-
|
| 8 |
-
import numpy as np
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def language_quotas(total: int, language_count: int) -> np.ndarray:
|
| 12 |
-
if int(total) <= 0 or int(language_count) <= 0:
|
| 13 |
-
raise ValueError("selection total and language count must be positive")
|
| 14 |
-
base, remainder = divmod(int(total), int(language_count))
|
| 15 |
-
if base == 0:
|
| 16 |
-
raise ValueError("selection total must cover every language")
|
| 17 |
-
quotas = np.full(int(language_count), base, dtype=np.int64)
|
| 18 |
-
quotas[:remainder] += 1
|
| 19 |
-
return quotas
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def _validate_aligned(keys: np.ndarray, *arrays: np.ndarray) -> None:
|
| 23 |
-
if keys.ndim != 1 or keys.dtype != np.uint64:
|
| 24 |
-
raise ValueError("candidate keys must be a uint64 vector")
|
| 25 |
-
if len(np.unique(keys)) != len(keys):
|
| 26 |
-
raise ValueError("candidate keys must be unique")
|
| 27 |
-
if any(array.ndim != 1 or len(array) != len(keys) for array in arrays):
|
| 28 |
-
raise ValueError("candidate selection arrays must be aligned vectors")
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def rank_with_quotas(
|
| 32 |
-
keys: np.ndarray,
|
| 33 |
-
anchor_language: np.ndarray,
|
| 34 |
-
eligible: np.ndarray,
|
| 35 |
-
primary: np.ndarray,
|
| 36 |
-
secondary: np.ndarray,
|
| 37 |
-
quotas: Sequence[int],
|
| 38 |
-
) -> np.ndarray:
|
| 39 |
-
key_array = np.asarray(keys)
|
| 40 |
-
anchors = np.asarray(anchor_language)
|
| 41 |
-
mask = np.asarray(eligible)
|
| 42 |
-
primary_values = np.asarray(primary)
|
| 43 |
-
secondary_values = np.asarray(secondary)
|
| 44 |
-
_validate_aligned(
|
| 45 |
-
key_array, anchors, mask, primary_values, secondary_values
|
| 46 |
-
)
|
| 47 |
-
quota_array = np.asarray(quotas, dtype=np.int64)
|
| 48 |
-
if quota_array.ndim != 1 or not len(quota_array) or bool((quota_array < 0).any()):
|
| 49 |
-
raise ValueError("language quotas are invalid")
|
| 50 |
-
if mask.dtype != np.bool_:
|
| 51 |
-
raise ValueError("candidate eligibility must be boolean")
|
| 52 |
-
if not np.isfinite(primary_values).all() or not np.isfinite(
|
| 53 |
-
secondary_values
|
| 54 |
-
).all():
|
| 55 |
-
raise ValueError("candidate ranking values must be finite")
|
| 56 |
-
|
| 57 |
-
selected: list[int] = []
|
| 58 |
-
for language, quota in enumerate(quota_array):
|
| 59 |
-
candidates = np.flatnonzero(mask & (anchors == language)).tolist()
|
| 60 |
-
candidates.sort(
|
| 61 |
-
key=lambda row: (
|
| 62 |
-
-float(primary_values[row]),
|
| 63 |
-
-float(secondary_values[row]),
|
| 64 |
-
int(key_array[row]),
|
| 65 |
-
)
|
| 66 |
-
)
|
| 67 |
-
if len(candidates) < int(quota):
|
| 68 |
-
raise ValueError(
|
| 69 |
-
f"cannot fill GROVE language quota {language}: "
|
| 70 |
-
f"need {quota}, have {len(candidates)}"
|
| 71 |
-
)
|
| 72 |
-
selected.extend(candidates[: int(quota)])
|
| 73 |
-
result = np.asarray(selected, dtype=np.int64)
|
| 74 |
-
if len(np.unique(result)) != len(result):
|
| 75 |
-
raise RuntimeError("language quota selection produced duplicates")
|
| 76 |
-
return result
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def support_deciles(values: np.ndarray, reference: np.ndarray) -> np.ndarray:
|
| 80 |
-
value_array = np.asarray(values, dtype=np.float64)
|
| 81 |
-
reference_array = np.asarray(reference, dtype=np.float64)
|
| 82 |
-
if (
|
| 83 |
-
value_array.ndim != 1
|
| 84 |
-
or reference_array.ndim != 1
|
| 85 |
-
or not len(reference_array)
|
| 86 |
-
or not np.isfinite(value_array).all()
|
| 87 |
-
or not np.isfinite(reference_array).all()
|
| 88 |
-
):
|
| 89 |
-
raise ValueError("support decile inputs must be finite vectors")
|
| 90 |
-
thresholds = np.quantile(reference_array, np.arange(1, 10) / 10)
|
| 91 |
-
return np.searchsorted(thresholds, value_array, side="right").astype(np.int8)
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def matched_random_selection(
|
| 95 |
-
keys: np.ndarray,
|
| 96 |
-
anchor_language: np.ndarray,
|
| 97 |
-
eligible: np.ndarray,
|
| 98 |
-
support: np.ndarray,
|
| 99 |
-
target_rows: np.ndarray,
|
| 100 |
-
*,
|
| 101 |
-
seed: int,
|
| 102 |
-
) -> np.ndarray:
|
| 103 |
-
key_array = np.asarray(keys)
|
| 104 |
-
anchors = np.asarray(anchor_language)
|
| 105 |
-
mask = np.asarray(eligible)
|
| 106 |
-
support_array = np.asarray(support)
|
| 107 |
-
targets = np.asarray(target_rows, dtype=np.int64)
|
| 108 |
-
_validate_aligned(key_array, anchors, mask, support_array)
|
| 109 |
-
if targets.ndim != 1 or (
|
| 110 |
-
len(targets)
|
| 111 |
-
and (int(targets.min()) < 0 or int(targets.max()) >= len(keys))
|
| 112 |
-
):
|
| 113 |
-
raise ValueError("random-control target rows are invalid")
|
| 114 |
-
if len(np.unique(targets)) != len(targets):
|
| 115 |
-
raise ValueError("random-control target rows must be unique")
|
| 116 |
-
rng = np.random.default_rng(int(seed))
|
| 117 |
-
target_set = set(int(row) for row in targets)
|
| 118 |
-
selected: list[int] = []
|
| 119 |
-
for language in sorted(set(int(value) for value in anchors[targets])):
|
| 120 |
-
pool = np.flatnonzero(mask & (anchors == language))
|
| 121 |
-
reference_decile = support_deciles(support_array[pool], support_array[pool])
|
| 122 |
-
pool_decile = {int(row): int(decile) for row, decile in zip(pool, reference_decile)}
|
| 123 |
-
language_targets = targets[anchors[targets] == language]
|
| 124 |
-
target_decile = np.asarray(
|
| 125 |
-
[pool_decile[int(row)] for row in language_targets], dtype=np.int8
|
| 126 |
-
)
|
| 127 |
-
for decile in range(10):
|
| 128 |
-
needed = int((target_decile == decile).sum())
|
| 129 |
-
if not needed:
|
| 130 |
-
continue
|
| 131 |
-
candidates = [
|
| 132 |
-
int(row)
|
| 133 |
-
for row in pool
|
| 134 |
-
if pool_decile[int(row)] == decile and int(row) not in target_set
|
| 135 |
-
]
|
| 136 |
-
if len(candidates) < needed:
|
| 137 |
-
candidates = [
|
| 138 |
-
int(row) for row in pool if pool_decile[int(row)] == decile
|
| 139 |
-
]
|
| 140 |
-
if len(candidates) < needed:
|
| 141 |
-
raise ValueError(
|
| 142 |
-
f"cannot fill matched-random stratum {language}/{decile}"
|
| 143 |
-
)
|
| 144 |
-
candidates = sorted(candidates)
|
| 145 |
-
order = rng.permutation(len(candidates))[:needed]
|
| 146 |
-
selected.extend(candidates[int(index)] for index in order)
|
| 147 |
-
result = np.asarray(selected, dtype=np.int64)
|
| 148 |
-
if len(result) != len(targets) or len(np.unique(result)) != len(result):
|
| 149 |
-
raise ValueError("matched-random selection is not cardinality-exact")
|
| 150 |
-
return result
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
def fold_gate_summary(
|
| 154 |
-
folds: Sequence[Mapping[str, Sequence[float]]],
|
| 155 |
-
) -> dict[str, Any]:
|
| 156 |
-
if len(folds) != 3:
|
| 157 |
-
raise ValueError("GROVE gate requires exactly three folds")
|
| 158 |
-
grove_means: list[float] = []
|
| 159 |
-
frequency_means: list[float] = []
|
| 160 |
-
fold_records: list[dict[str, float]] = []
|
| 161 |
-
for fold, arms in enumerate(folds):
|
| 162 |
-
if "grove_coherence" not in arms or "frequency" not in arms:
|
| 163 |
-
raise ValueError("GROVE gate fold is missing a primary arm")
|
| 164 |
-
grove = np.asarray(arms["grove_coherence"], dtype=np.float64)
|
| 165 |
-
frequency = np.asarray(arms["frequency"], dtype=np.float64)
|
| 166 |
-
if (
|
| 167 |
-
grove.ndim != 1
|
| 168 |
-
or frequency.ndim != 1
|
| 169 |
-
or not len(grove)
|
| 170 |
-
or not len(frequency)
|
| 171 |
-
or not np.isfinite(grove).all()
|
| 172 |
-
or not np.isfinite(frequency).all()
|
| 173 |
-
):
|
| 174 |
-
raise ValueError("GROVE gate values must be finite nonempty vectors")
|
| 175 |
-
grove_mean = float(grove.mean())
|
| 176 |
-
frequency_mean = float(frequency.mean())
|
| 177 |
-
grove_means.append(grove_mean)
|
| 178 |
-
frequency_means.append(frequency_mean)
|
| 179 |
-
fold_records.append(
|
| 180 |
-
{
|
| 181 |
-
"fold": fold,
|
| 182 |
-
"grove_mean": grove_mean,
|
| 183 |
-
"frequency_mean": frequency_mean,
|
| 184 |
-
"delta": grove_mean - frequency_mean,
|
| 185 |
-
}
|
| 186 |
-
)
|
| 187 |
-
deltas = np.asarray(grove_means) - np.asarray(frequency_means)
|
| 188 |
-
sample_sd = float(deltas.std(ddof=1))
|
| 189 |
-
mean_delta = float(deltas.mean())
|
| 190 |
-
flags = {
|
| 191 |
-
"grove_positive_all_folds": bool((np.asarray(grove_means) > 0).all()),
|
| 192 |
-
"delta_positive_all_folds": bool((deltas > 0).all()),
|
| 193 |
-
"mean_delta_gt_2sd": bool(mean_delta > 2.0 * sample_sd),
|
| 194 |
-
}
|
| 195 |
-
return {
|
| 196 |
-
"folds": fold_records,
|
| 197 |
-
"mean_delta": mean_delta,
|
| 198 |
-
"sample_sd": sample_sd,
|
| 199 |
-
"two_sd": 2.0 * sample_sd,
|
| 200 |
-
**flags,
|
| 201 |
-
"passed": bool(all(flags.values())),
|
| 202 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/init_model.py
DELETED
|
@@ -1,362 +0,0 @@
|
|
| 1 |
-
"""Create a StaRSE static-table initialization from one or more encoders."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import argparse
|
| 6 |
-
import json
|
| 7 |
-
import math
|
| 8 |
-
from collections.abc import Sequence
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
from typing import Any
|
| 11 |
-
|
| 12 |
-
import numpy as np
|
| 13 |
-
import torch
|
| 14 |
-
from sklearn.decomposition import PCA
|
| 15 |
-
from tokenizers import Tokenizer as BackendTokenizer
|
| 16 |
-
from transformers import AutoModel, AutoTokenizer, LlamaTokenizerFast, PreTrainedTokenizerFast
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
REPRESENTATION_CHOICES = (
|
| 20 |
-
"input",
|
| 21 |
-
"swe_mean",
|
| 22 |
-
"hidden_middle_token",
|
| 23 |
-
"hidden_last_token",
|
| 24 |
-
"hidden_last4_mean_token",
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def build_initial_matrix(
|
| 29 |
-
matrix: np.ndarray,
|
| 30 |
-
*,
|
| 31 |
-
target_dim: int = 512,
|
| 32 |
-
sif_a: float = 5e-5,
|
| 33 |
-
zipf_exponent: float = 1.0,
|
| 34 |
-
) -> tuple[np.ndarray, dict[str, Any]]:
|
| 35 |
-
"""Apply the PCA + SIF/Zipf recipe used for the released model."""
|
| 36 |
-
|
| 37 |
-
vocab_size, hidden_dim = matrix.shape
|
| 38 |
-
if target_dim > hidden_dim:
|
| 39 |
-
raise ValueError(f"target_dim={target_dim} exceeds source dimension {hidden_dim}")
|
| 40 |
-
|
| 41 |
-
components_to_remove = math.ceil(target_dim / 100)
|
| 42 |
-
n_components = min(hidden_dim, target_dim + components_to_remove)
|
| 43 |
-
components_to_remove = max(0, n_components - target_dim)
|
| 44 |
-
# These token tables are extremely tall. The covariance solver is exact
|
| 45 |
-
# and avoids sklearn's width-based switch to a much slower randomized SVD
|
| 46 |
-
# when several aligned representation tables are concatenated.
|
| 47 |
-
pca_solver = "covariance_eigh" if vocab_size >= 10 * hidden_dim else "auto"
|
| 48 |
-
pca = PCA(n_components=n_components, svd_solver=pca_solver, whiten=False, random_state=42)
|
| 49 |
-
reduced = pca.fit_transform(matrix)
|
| 50 |
-
if components_to_remove:
|
| 51 |
-
reduced = reduced[:, components_to_remove:]
|
| 52 |
-
reduced = reduced[:, :target_dim]
|
| 53 |
-
|
| 54 |
-
ranks = np.arange(1, vocab_size + 1, dtype=np.float64)
|
| 55 |
-
zipf = 1.0 / np.power(ranks, zipf_exponent)
|
| 56 |
-
probabilities = zipf / zipf.sum()
|
| 57 |
-
weights = sif_a / (sif_a + probabilities)
|
| 58 |
-
initialized = np.ascontiguousarray((reduced * weights[:, None]).astype(np.float32))
|
| 59 |
-
|
| 60 |
-
metadata = {
|
| 61 |
-
"source": "AutoModel.get_input_embeddings().weight",
|
| 62 |
-
"source_shape": [int(vocab_size), int(hidden_dim)],
|
| 63 |
-
"final_shape": list(initialized.shape),
|
| 64 |
-
"pca_components": int(n_components),
|
| 65 |
-
"pca_solver": pca_solver,
|
| 66 |
-
"leading_components_removed": int(components_to_remove),
|
| 67 |
-
"explained_variance_ratio_sum": float(pca.explained_variance_ratio_.sum()),
|
| 68 |
-
"sif_a": float(sif_a),
|
| 69 |
-
"zipf_exponent": float(zipf_exponent),
|
| 70 |
-
}
|
| 71 |
-
return initialized, metadata
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def load_static_tokenizer(
|
| 75 |
-
model_name: str,
|
| 76 |
-
*,
|
| 77 |
-
trust_remote_code: bool,
|
| 78 |
-
revision: str | None = None,
|
| 79 |
-
) -> Any:
|
| 80 |
-
"""Load the Rust-backed tokenizer required by StaticEmbedding."""
|
| 81 |
-
|
| 82 |
-
auto_error: Exception | None = None
|
| 83 |
-
try:
|
| 84 |
-
tokenizer = AutoTokenizer.from_pretrained(
|
| 85 |
-
model_name,
|
| 86 |
-
revision=revision,
|
| 87 |
-
trust_remote_code=trust_remote_code,
|
| 88 |
-
)
|
| 89 |
-
if getattr(tokenizer, "is_fast", False):
|
| 90 |
-
return tokenizer
|
| 91 |
-
except Exception as exc:
|
| 92 |
-
auto_error = exc
|
| 93 |
-
|
| 94 |
-
try:
|
| 95 |
-
backend = BackendTokenizer.from_pretrained(model_name, revision=revision or "main")
|
| 96 |
-
return PreTrainedTokenizerFast(tokenizer_object=backend)
|
| 97 |
-
except Exception:
|
| 98 |
-
try:
|
| 99 |
-
return LlamaTokenizerFast.from_pretrained(model_name, revision=revision)
|
| 100 |
-
except Exception as fallback_error:
|
| 101 |
-
detail = f" AutoTokenizer failed with: {auto_error}" if auto_error is not None else ""
|
| 102 |
-
raise TypeError(
|
| 103 |
-
f"{model_name!r} has no usable Rust-backed tokenizer.{detail} "
|
| 104 |
-
f"Fast tokenizer fallback failed with: {fallback_error}"
|
| 105 |
-
) from fallback_error
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def _special_token_id(tokenizer: Any, model: Any, name: str, fallback_token: str) -> int:
|
| 109 |
-
token_id = getattr(tokenizer, f"{name}_token_id", None)
|
| 110 |
-
if token_id is None:
|
| 111 |
-
token_id = getattr(model.config, f"{name}_token_id", None)
|
| 112 |
-
if token_id is None:
|
| 113 |
-
token_id = tokenizer.convert_tokens_to_ids(fallback_token)
|
| 114 |
-
if token_id is None or int(token_id) < 0:
|
| 115 |
-
raise ValueError(f"Could not resolve {name} token ID for {model.name_or_path!r}")
|
| 116 |
-
return int(token_id)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def _select_contextual_representation(outputs: Any, representation: str) -> torch.Tensor:
|
| 120 |
-
"""Select a Model2Vec-style or layer-wise one-token representation."""
|
| 121 |
-
|
| 122 |
-
if representation == "swe_mean":
|
| 123 |
-
return outputs.last_hidden_state.mean(dim=1)
|
| 124 |
-
if representation == "hidden_last_token":
|
| 125 |
-
return outputs.last_hidden_state[:, 1, :]
|
| 126 |
-
|
| 127 |
-
hidden_states = outputs.hidden_states
|
| 128 |
-
if hidden_states is None:
|
| 129 |
-
raise ValueError(f"{representation!r} requires output_hidden_states=True")
|
| 130 |
-
if representation == "hidden_middle_token":
|
| 131 |
-
# hidden_states[0] is the embedding output; all later entries are
|
| 132 |
-
# completed Transformer layers. Select the central completed layer.
|
| 133 |
-
middle_index = max(1, len(hidden_states) // 2)
|
| 134 |
-
return hidden_states[middle_index][:, 1, :]
|
| 135 |
-
if representation == "hidden_last4_mean_token":
|
| 136 |
-
return torch.stack(hidden_states[-4:], dim=0).mean(dim=0)[:, 1, :]
|
| 137 |
-
raise ValueError(f"Unsupported contextual representation: {representation!r}")
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
def extract_representation_tables(
|
| 141 |
-
*,
|
| 142 |
-
model: Any,
|
| 143 |
-
tokenizer: Any,
|
| 144 |
-
representations: Sequence[str],
|
| 145 |
-
device: str,
|
| 146 |
-
batch_size: int,
|
| 147 |
-
) -> dict[str, np.ndarray]:
|
| 148 |
-
"""Extract aligned input, SWE/output, or hidden-state token tables.
|
| 149 |
-
|
| 150 |
-
``swe_mean`` matches Model2Vec output distillation before PCA/SIF: each
|
| 151 |
-
vocabulary item is forwarded as ``[BOS, token, EOS]`` and the final hidden
|
| 152 |
-
states are mean pooled. Hidden variants retain only the contextualized
|
| 153 |
-
vocabulary-token position.
|
| 154 |
-
"""
|
| 155 |
-
|
| 156 |
-
requested = tuple(dict.fromkeys(representations))
|
| 157 |
-
unknown = sorted(set(requested) - set(REPRESENTATION_CHOICES))
|
| 158 |
-
if unknown:
|
| 159 |
-
raise ValueError(f"Unknown representations: {unknown}")
|
| 160 |
-
if batch_size <= 0:
|
| 161 |
-
raise ValueError("contextual batch size must be positive")
|
| 162 |
-
|
| 163 |
-
input_table = model.get_input_embeddings().weight.detach().to(dtype=torch.float32, device="cpu").numpy()
|
| 164 |
-
vocab_size, hidden_dim = input_table.shape
|
| 165 |
-
tables: dict[str, np.ndarray] = {}
|
| 166 |
-
if "input" in requested:
|
| 167 |
-
tables["input"] = np.ascontiguousarray(input_table)
|
| 168 |
-
|
| 169 |
-
contextual = [name for name in requested if name != "input"]
|
| 170 |
-
if not contextual:
|
| 171 |
-
return tables
|
| 172 |
-
|
| 173 |
-
bos_id = _special_token_id(tokenizer, model, "bos", "<s>")
|
| 174 |
-
eos_id = _special_token_id(tokenizer, model, "eos", "</s>")
|
| 175 |
-
arrays = {name: np.empty((vocab_size, hidden_dim), dtype=np.float32) for name in contextual}
|
| 176 |
-
needs_hidden_states = any(
|
| 177 |
-
name in {"hidden_middle_token", "hidden_last4_mean_token"} for name in contextual
|
| 178 |
-
)
|
| 179 |
-
|
| 180 |
-
model = model.eval().to(device=device, dtype=torch.float32)
|
| 181 |
-
with torch.inference_mode():
|
| 182 |
-
for start in range(0, vocab_size, batch_size):
|
| 183 |
-
stop = min(vocab_size, start + batch_size)
|
| 184 |
-
token_ids = torch.arange(start, stop, dtype=torch.long, device=device)
|
| 185 |
-
input_ids = torch.stack(
|
| 186 |
-
(
|
| 187 |
-
torch.full_like(token_ids, bos_id),
|
| 188 |
-
token_ids,
|
| 189 |
-
torch.full_like(token_ids, eos_id),
|
| 190 |
-
),
|
| 191 |
-
dim=1,
|
| 192 |
-
)
|
| 193 |
-
outputs = model(
|
| 194 |
-
input_ids=input_ids,
|
| 195 |
-
attention_mask=torch.ones_like(input_ids),
|
| 196 |
-
output_hidden_states=needs_hidden_states,
|
| 197 |
-
return_dict=True,
|
| 198 |
-
)
|
| 199 |
-
for name in contextual:
|
| 200 |
-
selected = _select_contextual_representation(outputs, name)
|
| 201 |
-
arrays[name][start:stop] = selected.detach().to(dtype=torch.float32, device="cpu").numpy()
|
| 202 |
-
|
| 203 |
-
tables.update({name: np.ascontiguousarray(array) for name, array in arrays.items()})
|
| 204 |
-
model.to("cpu")
|
| 205 |
-
return tables
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
def initialize_model(
|
| 209 |
-
*,
|
| 210 |
-
base_models: Sequence[str],
|
| 211 |
-
base_revisions: Sequence[str | None] | None,
|
| 212 |
-
output_dir: Path,
|
| 213 |
-
target_dim: int,
|
| 214 |
-
sif_a: float,
|
| 215 |
-
zipf_exponent: float,
|
| 216 |
-
trust_remote_code: bool,
|
| 217 |
-
representations: Sequence[str] = ("input",),
|
| 218 |
-
device: str = "cpu",
|
| 219 |
-
contextual_batch_size: int = 2048,
|
| 220 |
-
) -> None:
|
| 221 |
-
from sentence_transformers import SentenceTransformer
|
| 222 |
-
from sentence_transformers.sentence_transformer.modules import StaticEmbedding
|
| 223 |
-
|
| 224 |
-
if not base_models:
|
| 225 |
-
raise ValueError("At least one base model is required")
|
| 226 |
-
|
| 227 |
-
revisions = list(base_revisions or [None] * len(base_models))
|
| 228 |
-
if len(revisions) != len(base_models):
|
| 229 |
-
raise ValueError("--base-revision must be repeated once per --base-model")
|
| 230 |
-
tokenizer = load_static_tokenizer(
|
| 231 |
-
base_models[0],
|
| 232 |
-
revision=revisions[0],
|
| 233 |
-
trust_remote_code=trust_remote_code,
|
| 234 |
-
)
|
| 235 |
-
reference_vocab = tokenizer.get_vocab()
|
| 236 |
-
reference_special_ids = tuple(tokenizer.all_special_ids)
|
| 237 |
-
source_tables: list[np.ndarray] = []
|
| 238 |
-
source_components: list[dict[str, Any]] = []
|
| 239 |
-
for model_name, revision in zip(base_models, revisions, strict=True):
|
| 240 |
-
candidate_tokenizer = load_static_tokenizer(
|
| 241 |
-
model_name,
|
| 242 |
-
revision=revision,
|
| 243 |
-
trust_remote_code=trust_remote_code,
|
| 244 |
-
)
|
| 245 |
-
if candidate_tokenizer.get_vocab() != reference_vocab:
|
| 246 |
-
raise ValueError(f"Tokenizer vocabulary or token IDs differ for {model_name!r}")
|
| 247 |
-
if tuple(candidate_tokenizer.all_special_ids) != reference_special_ids:
|
| 248 |
-
raise ValueError(f"Special token IDs differ for {model_name!r}")
|
| 249 |
-
|
| 250 |
-
model = AutoModel.from_pretrained(
|
| 251 |
-
model_name,
|
| 252 |
-
revision=revision,
|
| 253 |
-
trust_remote_code=trust_remote_code,
|
| 254 |
-
)
|
| 255 |
-
tables = extract_representation_tables(
|
| 256 |
-
model=model,
|
| 257 |
-
tokenizer=candidate_tokenizer,
|
| 258 |
-
representations=representations,
|
| 259 |
-
device=device,
|
| 260 |
-
batch_size=contextual_batch_size,
|
| 261 |
-
)
|
| 262 |
-
for representation in representations:
|
| 263 |
-
table = tables[representation]
|
| 264 |
-
if table.shape[0] != len(reference_vocab):
|
| 265 |
-
raise ValueError(
|
| 266 |
-
f"Embedding rows for {model_name!r}/{representation} ({table.shape[0]}) "
|
| 267 |
-
f"do not match tokenizer size ({len(reference_vocab)})"
|
| 268 |
-
)
|
| 269 |
-
source_tables.append(table)
|
| 270 |
-
source_components.append(
|
| 271 |
-
{
|
| 272 |
-
"model": model_name,
|
| 273 |
-
"revision": revision,
|
| 274 |
-
"representation": representation,
|
| 275 |
-
"shape": [int(value) for value in table.shape],
|
| 276 |
-
}
|
| 277 |
-
)
|
| 278 |
-
del model
|
| 279 |
-
|
| 280 |
-
source = np.ascontiguousarray(np.concatenate(source_tables, axis=1))
|
| 281 |
-
matrix, metadata = build_initial_matrix(
|
| 282 |
-
source,
|
| 283 |
-
target_dim=target_dim,
|
| 284 |
-
sif_a=sif_a,
|
| 285 |
-
zipf_exponent=zipf_exponent,
|
| 286 |
-
)
|
| 287 |
-
concatenated = len(source_tables) > 1
|
| 288 |
-
del source_tables, source
|
| 289 |
-
|
| 290 |
-
static_model = SentenceTransformer(modules=[StaticEmbedding(tokenizer=tokenizer, embedding_weights=matrix)])
|
| 291 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 292 |
-
static_model.save_pretrained(str(output_dir), create_model_card=False)
|
| 293 |
-
metadata.update(
|
| 294 |
-
{
|
| 295 |
-
"base_models": list(base_models),
|
| 296 |
-
"base_revisions": revisions,
|
| 297 |
-
"representations": list(representations),
|
| 298 |
-
"source_components": source_components,
|
| 299 |
-
"source": "aligned token representations concatenated before PCA",
|
| 300 |
-
"concatenated": concatenated,
|
| 301 |
-
"target_dim": target_dim,
|
| 302 |
-
"contextual_inference_dtype": (
|
| 303 |
-
"float32" if any(representation != "input" for representation in representations) else None
|
| 304 |
-
),
|
| 305 |
-
}
|
| 306 |
-
)
|
| 307 |
-
(output_dir / "initialization.json").write_text(
|
| 308 |
-
json.dumps(metadata, ensure_ascii=False, indent=2) + "\n",
|
| 309 |
-
encoding="utf-8",
|
| 310 |
-
)
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
| 314 |
-
parser = argparse.ArgumentParser(description=__doc__)
|
| 315 |
-
parser.add_argument(
|
| 316 |
-
"--base-model",
|
| 317 |
-
dest="base_models",
|
| 318 |
-
action="append",
|
| 319 |
-
help="Encoder supplying tokenizer and input embeddings; repeat to concatenate aligned token tables",
|
| 320 |
-
)
|
| 321 |
-
parser.add_argument(
|
| 322 |
-
"--base-revision",
|
| 323 |
-
dest="base_revisions",
|
| 324 |
-
action="append",
|
| 325 |
-
help="Pinned Hub commit for the matching --base-model; repeat in the same order",
|
| 326 |
-
)
|
| 327 |
-
parser.add_argument("--output-dir", type=Path, required=True)
|
| 328 |
-
parser.add_argument("--target-dim", type=int, default=512)
|
| 329 |
-
parser.add_argument("--sif-a", type=float, default=5e-5)
|
| 330 |
-
parser.add_argument("--zipf-exponent", type=float, default=1.0)
|
| 331 |
-
parser.add_argument(
|
| 332 |
-
"--representation",
|
| 333 |
-
dest="representations",
|
| 334 |
-
action="append",
|
| 335 |
-
choices=REPRESENTATION_CHOICES,
|
| 336 |
-
help="Aligned table to extract from every base model; repeat to fuse representations (default: input)",
|
| 337 |
-
)
|
| 338 |
-
parser.add_argument("--device", default="cpu", help="Device used only while extracting contextual tables")
|
| 339 |
-
parser.add_argument("--contextual-batch-size", type=int, default=2048)
|
| 340 |
-
parser.add_argument("--trust-remote-code", action=argparse.BooleanOptionalAction, default=False)
|
| 341 |
-
return parser.parse_args(argv)
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
def main(argv: list[str] | None = None) -> int:
|
| 345 |
-
args = parse_args(argv)
|
| 346 |
-
initialize_model(
|
| 347 |
-
base_models=args.base_models or ["ai-forever/ruBert-base"],
|
| 348 |
-
base_revisions=args.base_revisions,
|
| 349 |
-
output_dir=args.output_dir.resolve(),
|
| 350 |
-
target_dim=args.target_dim,
|
| 351 |
-
sif_a=args.sif_a,
|
| 352 |
-
zipf_exponent=args.zipf_exponent,
|
| 353 |
-
trust_remote_code=args.trust_remote_code,
|
| 354 |
-
representations=args.representations or ["input"],
|
| 355 |
-
device=args.device,
|
| 356 |
-
contextual_batch_size=args.contextual_batch_size,
|
| 357 |
-
)
|
| 358 |
-
return 0
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
if __name__ == "__main__":
|
| 362 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/language_conditioned.py
DELETED
|
@@ -1,165 +0,0 @@
|
|
| 1 |
-
"""Language-conditioned static embedding.
|
| 2 |
-
|
| 3 |
-
The language is carried as a marker token prepended to each text, so the two
|
| 4 |
-
sides of a parallel pair can carry different language identities without
|
| 5 |
-
changing the trainer's two-column dataset schema.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
from torch import nn
|
| 12 |
-
|
| 13 |
-
MODES = (
|
| 14 |
-
"none", "marker", "centroid", "diag", "lowrank", "bilinear", "senses",
|
| 15 |
-
"hard", "gate", "gate_senses", "split", "ngram", "ngram3", "wordpool",
|
| 16 |
-
"ngram_gate", "fuse", "vocabmoe", "collapse", "collapse_shared",
|
| 17 |
-
"collapse_dynamic", "collapse_reclaimed", "collapse_online",
|
| 18 |
-
"collapse_online_compiled", "collapse_recursive", "idfpool",
|
| 19 |
-
)
|
| 20 |
-
|
| 21 |
-
# Language-specific pooling changes the relative numerator composition. One
|
| 22 |
-
# shared vocabulary row cannot represent a different weight in every language;
|
| 23 |
-
# the positive weighted-mean denominator itself cancels under cosine scoring.
|
| 24 |
-
GATING_MODES = ("gate", "gate_senses")
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
class LanguageConditioner(nn.Module):
|
| 28 |
-
"""The conditioning head. Owns every parameter the base table does not."""
|
| 29 |
-
|
| 30 |
-
def __init__(
|
| 31 |
-
self,
|
| 32 |
-
*,
|
| 33 |
-
mode: str,
|
| 34 |
-
dim: int,
|
| 35 |
-
vocab_size: int,
|
| 36 |
-
n_languages: int,
|
| 37 |
-
code_dim: int = 16,
|
| 38 |
-
n_senses: int = 64,
|
| 39 |
-
rank: int = 8,
|
| 40 |
-
) -> None:
|
| 41 |
-
super().__init__()
|
| 42 |
-
if mode not in MODES:
|
| 43 |
-
raise ValueError(f"unknown mode {mode!r}, expected one of {MODES}")
|
| 44 |
-
self.mode = mode
|
| 45 |
-
self.dim = dim
|
| 46 |
-
self.n_languages = n_languages
|
| 47 |
-
self.code_dim = code_dim
|
| 48 |
-
self.n_senses = n_senses
|
| 49 |
-
self.rank = rank
|
| 50 |
-
|
| 51 |
-
if mode == "centroid":
|
| 52 |
-
self.centroid = nn.Parameter(torch.zeros(n_languages, dim))
|
| 53 |
-
elif mode == "diag":
|
| 54 |
-
self.scale = nn.Parameter(torch.ones(n_languages, dim))
|
| 55 |
-
elif mode == "lowrank":
|
| 56 |
-
self.left = nn.Parameter(torch.randn(n_languages, dim, rank) * 0.02)
|
| 57 |
-
self.right = nn.Parameter(torch.zeros(n_languages, rank, dim))
|
| 58 |
-
elif mode == "bilinear":
|
| 59 |
-
self.code = nn.EmbeddingBag(vocab_size, code_dim, mode="mean")
|
| 60 |
-
nn.init.normal_(self.code.weight, std=0.02)
|
| 61 |
-
self.readout = nn.Parameter(torch.zeros(n_languages, code_dim, dim))
|
| 62 |
-
if mode in ("gate", "gate_senses"):
|
| 63 |
-
self.gate_code = nn.Embedding(vocab_size, code_dim)
|
| 64 |
-
nn.init.normal_(self.gate_code.weight, std=0.02)
|
| 65 |
-
self.gate_language = nn.Parameter(torch.zeros(n_languages, code_dim))
|
| 66 |
-
if mode in ("senses", "hard", "gate_senses"):
|
| 67 |
-
self.code = nn.Embedding(vocab_size, code_dim)
|
| 68 |
-
nn.init.normal_(self.code.weight, std=0.02)
|
| 69 |
-
self.language = nn.Parameter(torch.ones(n_languages, code_dim))
|
| 70 |
-
self.probe = nn.Parameter(torch.randn(n_senses, code_dim) * 0.02)
|
| 71 |
-
self.senses = nn.Parameter(torch.zeros(n_senses, dim))
|
| 72 |
-
|
| 73 |
-
def extra_repr(self) -> str:
|
| 74 |
-
return (f"mode={self.mode}, languages={self.n_languages}, "
|
| 75 |
-
f"code_dim={self.code_dim}, senses={self.n_senses}, rank={self.rank}")
|
| 76 |
-
|
| 77 |
-
def token_gate(self, token_ids: torch.Tensor, lang_per_token: torch.Tensor) -> torch.Tensor:
|
| 78 |
-
"""Multiplicative weight per token, centred on 1 at initialisation."""
|
| 79 |
-
score = (self.gate_code(token_ids) * self.gate_language[lang_per_token]).sum(-1)
|
| 80 |
-
return 2.0 * torch.sigmoid(score)
|
| 81 |
-
|
| 82 |
-
def sense_weights(self, token_ids: torch.Tensor, lang_per_token: torch.Tensor) -> torch.Tensor:
|
| 83 |
-
"""Alpha over the sense dictionary, per token, given its language."""
|
| 84 |
-
a = self.code(token_ids)
|
| 85 |
-
v = self.language[lang_per_token]
|
| 86 |
-
logits = (a * v) @ self.probe.T
|
| 87 |
-
alpha = torch.softmax(logits, dim=-1)
|
| 88 |
-
if self.mode == "hard":
|
| 89 |
-
index = alpha.argmax(dim=-1, keepdim=True)
|
| 90 |
-
onehot = torch.zeros_like(alpha).scatter_(-1, index, 1.0)
|
| 91 |
-
alpha = onehot + alpha - alpha.detach()
|
| 92 |
-
return alpha
|
| 93 |
-
|
| 94 |
-
def forward(
|
| 95 |
-
self,
|
| 96 |
-
pooled: torch.Tensor,
|
| 97 |
-
language: torch.Tensor,
|
| 98 |
-
token_ids: torch.Tensor,
|
| 99 |
-
segment: torch.Tensor,
|
| 100 |
-
lengths: torch.Tensor,
|
| 101 |
-
) -> torch.Tensor:
|
| 102 |
-
mode = self.mode
|
| 103 |
-
if mode in ("none", "marker"):
|
| 104 |
-
return pooled
|
| 105 |
-
if mode == "centroid":
|
| 106 |
-
return pooled - self.centroid[language]
|
| 107 |
-
if mode == "diag":
|
| 108 |
-
return pooled * self.scale[language]
|
| 109 |
-
if mode == "lowrank":
|
| 110 |
-
left = self.left[language]
|
| 111 |
-
right = self.right[language]
|
| 112 |
-
latent = torch.einsum("bd,bdr->br", pooled, left)
|
| 113 |
-
return pooled + torch.einsum("br,brd->bd", latent, right)
|
| 114 |
-
if mode == "bilinear":
|
| 115 |
-
offsets = torch.cat([
|
| 116 |
-
torch.zeros(1, dtype=torch.long, device=token_ids.device),
|
| 117 |
-
lengths.cumsum(0)[:-1],
|
| 118 |
-
])
|
| 119 |
-
mean_code = self.code(token_ids, offsets)
|
| 120 |
-
return pooled + torch.einsum("bk,bkd->bd", mean_code, self.readout[language])
|
| 121 |
-
if mode == "gate":
|
| 122 |
-
return pooled
|
| 123 |
-
|
| 124 |
-
alpha = self.sense_weights(token_ids, language[segment])
|
| 125 |
-
summed = torch.zeros(pooled.shape[0], self.n_senses,
|
| 126 |
-
dtype=alpha.dtype, device=alpha.device)
|
| 127 |
-
summed.index_add_(0, segment, alpha)
|
| 128 |
-
mean_alpha = summed / lengths.clamp(min=1).unsqueeze(1).to(summed.dtype)
|
| 129 |
-
return pooled + mean_alpha @ self.senses
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def split_markers(
|
| 133 |
-
input_ids: torch.Tensor,
|
| 134 |
-
offsets: torch.Tensor,
|
| 135 |
-
marker_lookup: torch.Tensor,
|
| 136 |
-
keep_marker: bool,
|
| 137 |
-
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 138 |
-
"""Peel the leading language marker off every sequence.
|
| 139 |
-
|
| 140 |
-
Returns content ids, sentence indices, content lengths, per-sentence
|
| 141 |
-
languages, and offsets for the content-only token stream.
|
| 142 |
-
"""
|
| 143 |
-
total = input_ids.numel()
|
| 144 |
-
batch = offsets.numel()
|
| 145 |
-
marker_ids = input_ids[offsets]
|
| 146 |
-
language = marker_lookup[marker_ids]
|
| 147 |
-
|
| 148 |
-
ends = torch.cat([offsets[1:], torch.tensor([total], device=offsets.device)])
|
| 149 |
-
full_lengths = ends - offsets
|
| 150 |
-
if keep_marker:
|
| 151 |
-
segment = torch.repeat_interleave(
|
| 152 |
-
torch.arange(batch, device=offsets.device), full_lengths)
|
| 153 |
-
return input_ids, segment, full_lengths, language, offsets
|
| 154 |
-
|
| 155 |
-
keep = torch.ones(total, dtype=torch.bool, device=input_ids.device)
|
| 156 |
-
keep[offsets] = False
|
| 157 |
-
content = input_ids[keep]
|
| 158 |
-
lengths = (full_lengths - 1).clamp(min=0)
|
| 159 |
-
segment = torch.repeat_interleave(
|
| 160 |
-
torch.arange(batch, device=offsets.device), lengths)
|
| 161 |
-
new_offsets = torch.cat([
|
| 162 |
-
torch.zeros(1, dtype=torch.long, device=offsets.device),
|
| 163 |
-
lengths.cumsum(0)[:-1],
|
| 164 |
-
])
|
| 165 |
-
return content, segment, lengths, language, new_offsets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/latent_compilation.py
DELETED
|
@@ -1,1095 +0,0 @@
|
|
| 1 |
-
"""Deterministic MAP compilation for trained latent lexicons."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
import hashlib
|
| 7 |
-
import json
|
| 8 |
-
import math
|
| 9 |
-
import os
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
import shutil
|
| 12 |
-
import tempfile
|
| 13 |
-
from typing import Mapping, Sequence
|
| 14 |
-
|
| 15 |
-
import numpy as np
|
| 16 |
-
from safetensors.torch import load_file, save_file
|
| 17 |
-
import torch
|
| 18 |
-
import torch.nn.functional as F
|
| 19 |
-
|
| 20 |
-
from starse.latent_lexicon import (
|
| 21 |
-
LexiconEmission,
|
| 22 |
-
bounded_potential,
|
| 23 |
-
map_matching,
|
| 24 |
-
map_split_frontier,
|
| 25 |
-
)
|
| 26 |
-
from starse.latent_training import LatentLexiconModel
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
_HASH_CHUNK_BYTES = 8 * 1024 * 1024
|
| 30 |
-
_FINITE_CHUNK_ELEMENTS = 1024 * 1024
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def _array_identity(array: np.ndarray) -> dict[str, object]:
|
| 34 |
-
if not array.flags.c_contiguous:
|
| 35 |
-
raise ValueError("compiled identity array must be C-contiguous")
|
| 36 |
-
digest = hashlib.sha256()
|
| 37 |
-
byte_view = memoryview(array).cast("B")
|
| 38 |
-
for start in range(0, len(byte_view), _HASH_CHUNK_BYTES):
|
| 39 |
-
digest.update(byte_view[start:start + _HASH_CHUNK_BYTES])
|
| 40 |
-
return {
|
| 41 |
-
"shape": list(array.shape),
|
| 42 |
-
"dtype": str(array.dtype),
|
| 43 |
-
"sha256": digest.hexdigest(),
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def _tensor_array(tensor: torch.Tensor) -> np.ndarray:
|
| 48 |
-
if tensor.device.type != "cpu" or not tensor.is_contiguous():
|
| 49 |
-
raise ValueError("compiled identity tensor must be contiguous CPU storage")
|
| 50 |
-
return tensor.detach().numpy()
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def _tensor_is_finite(tensor: torch.Tensor) -> bool:
|
| 54 |
-
flat = tensor.view(-1)
|
| 55 |
-
for start in range(0, flat.numel(), _FINITE_CHUNK_ELEMENTS):
|
| 56 |
-
if not bool(torch.isfinite(
|
| 57 |
-
flat[start:start + _FINITE_CHUNK_ELEMENTS]
|
| 58 |
-
).all().item()):
|
| 59 |
-
return False
|
| 60 |
-
return True
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
@dataclass(frozen=True)
|
| 64 |
-
class CompiledSentence:
|
| 65 |
-
emissions: tuple[LexiconEmission, ...]
|
| 66 |
-
numerator: torch.Tensor
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
@dataclass
|
| 70 |
-
class CompiledLatentLexicon:
|
| 71 |
-
embedding: torch.Tensor
|
| 72 |
-
model_vocab_size: int
|
| 73 |
-
split_active: torch.Tensor
|
| 74 |
-
parent_left: torch.Tensor
|
| 75 |
-
parent_right: torch.Tensor
|
| 76 |
-
pair_keys: np.ndarray
|
| 77 |
-
pair_scores: torch.Tensor
|
| 78 |
-
|
| 79 |
-
def identity(self) -> dict[str, object]:
|
| 80 |
-
arrays = {
|
| 81 |
-
"embedding": _tensor_array(self.embedding),
|
| 82 |
-
"split_active": _tensor_array(self.split_active),
|
| 83 |
-
"parent_left": _tensor_array(self.parent_left),
|
| 84 |
-
"parent_right": _tensor_array(self.parent_right),
|
| 85 |
-
"pair_keys": self.pair_keys,
|
| 86 |
-
"pair_scores": _tensor_array(self.pair_scores),
|
| 87 |
-
}
|
| 88 |
-
return {
|
| 89 |
-
"model_vocab_size": self.model_vocab_size,
|
| 90 |
-
"arrays": {
|
| 91 |
-
name: _array_identity(array)
|
| 92 |
-
for name, array in arrays.items()
|
| 93 |
-
},
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
def pair_row_id(self, left: int, right: int) -> int:
|
| 97 |
-
if not 0 <= int(left) < self.model_vocab_size:
|
| 98 |
-
raise ValueError("left pair token is outside the compiled vocabulary")
|
| 99 |
-
if not 0 <= int(right) < self.model_vocab_size:
|
| 100 |
-
raise ValueError("right pair token is outside the compiled vocabulary")
|
| 101 |
-
key = np.uint64((int(left) << 32) | int(right))
|
| 102 |
-
position = int(np.searchsorted(self.pair_keys, key))
|
| 103 |
-
if position >= self.pair_keys.size or self.pair_keys[position] != key:
|
| 104 |
-
raise ValueError("pair is not active in the compiled vocabulary")
|
| 105 |
-
return self.model_vocab_size + position
|
| 106 |
-
|
| 107 |
-
def _frontier(self, root: int) -> tuple[int, ...]:
|
| 108 |
-
frontier: list[int] = []
|
| 109 |
-
|
| 110 |
-
def visit(token: int, ancestors: frozenset[int]) -> None:
|
| 111 |
-
if token in ancestors:
|
| 112 |
-
raise ValueError(f"cycle in compiled split tree at token {token}")
|
| 113 |
-
left = int(self.parent_left[token])
|
| 114 |
-
right = int(self.parent_right[token])
|
| 115 |
-
if not bool(self.split_active[token]) or left < 0:
|
| 116 |
-
frontier.append(token)
|
| 117 |
-
return
|
| 118 |
-
nested = ancestors | {token}
|
| 119 |
-
visit(left, nested)
|
| 120 |
-
visit(right, nested)
|
| 121 |
-
|
| 122 |
-
visit(root, frozenset())
|
| 123 |
-
return tuple(frontier)
|
| 124 |
-
|
| 125 |
-
def encode_roots(self, roots: Sequence[int]) -> CompiledSentence:
|
| 126 |
-
root_ids = tuple(int(token) for token in roots)
|
| 127 |
-
if not root_ids:
|
| 128 |
-
raise ValueError("compiled sentence needs at least one root token")
|
| 129 |
-
if min(root_ids) < 0 or max(root_ids) >= self.model_vocab_size:
|
| 130 |
-
raise ValueError("root token id is outside the compiled vocabulary")
|
| 131 |
-
|
| 132 |
-
edge_count = max(0, len(root_ids) - 1)
|
| 133 |
-
edge_scores = self.pair_scores.new_zeros((edge_count,))
|
| 134 |
-
edge_mask = torch.zeros(edge_count, dtype=torch.bool)
|
| 135 |
-
edge_rows = np.full(edge_count, -1, dtype=np.int64)
|
| 136 |
-
if edge_count:
|
| 137 |
-
packed = np.asarray(
|
| 138 |
-
[
|
| 139 |
-
(root_ids[index] << 32) | root_ids[index + 1]
|
| 140 |
-
for index in range(edge_count)
|
| 141 |
-
],
|
| 142 |
-
dtype=np.uint64,
|
| 143 |
-
)
|
| 144 |
-
positions = np.searchsorted(self.pair_keys, packed)
|
| 145 |
-
in_bounds = positions < self.pair_keys.size
|
| 146 |
-
bounded = positions[in_bounds]
|
| 147 |
-
exact = self.pair_keys[bounded] == packed[in_bounds]
|
| 148 |
-
exact_edges = np.flatnonzero(in_bounds)[exact]
|
| 149 |
-
exact_rows = bounded[exact].astype(np.int64)
|
| 150 |
-
edge_rows[exact_edges] = exact_rows
|
| 151 |
-
if exact_edges.size:
|
| 152 |
-
edge_index = torch.from_numpy(exact_edges)
|
| 153 |
-
pair_index = torch.from_numpy(exact_rows)
|
| 154 |
-
edge_mask[edge_index] = True
|
| 155 |
-
edge_scores[edge_index] = self.pair_scores[pair_index]
|
| 156 |
-
selected = map_matching(edge_scores, edge_mask).tolist()
|
| 157 |
-
|
| 158 |
-
emissions: list[LexiconEmission] = []
|
| 159 |
-
vectors: list[torch.Tensor] = []
|
| 160 |
-
position = 0
|
| 161 |
-
while position < len(root_ids):
|
| 162 |
-
if position < len(selected) and selected[position]:
|
| 163 |
-
pair = (root_ids[position], root_ids[position + 1])
|
| 164 |
-
emissions.append(LexiconEmission("pair", pair))
|
| 165 |
-
vectors.append(
|
| 166 |
-
self.embedding[self.model_vocab_size + int(edge_rows[position])]
|
| 167 |
-
)
|
| 168 |
-
position += 2
|
| 169 |
-
continue
|
| 170 |
-
for token in self._frontier(root_ids[position]):
|
| 171 |
-
emissions.append(LexiconEmission("original", (token,)))
|
| 172 |
-
vectors.append(self.embedding[token])
|
| 173 |
-
position += 1
|
| 174 |
-
return CompiledSentence(
|
| 175 |
-
emissions=tuple(emissions),
|
| 176 |
-
numerator=torch.stack(vectors).sum(dim=0),
|
| 177 |
-
)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
def _pair_vectors(
|
| 181 |
-
model: LatentLexiconModel,
|
| 182 |
-
left_ids: torch.Tensor,
|
| 183 |
-
right_ids: torch.Tensor,
|
| 184 |
-
) -> torch.Tensor:
|
| 185 |
-
buckets, signs = model.hasher.codes(
|
| 186 |
-
left_ids,
|
| 187 |
-
right_ids,
|
| 188 |
-
dtype=model.embedding.weight.dtype,
|
| 189 |
-
device=model.embedding.weight.device,
|
| 190 |
-
)
|
| 191 |
-
return model.residual(
|
| 192 |
-
model.embedding.weight.index_select(0, left_ids),
|
| 193 |
-
model.embedding.weight.index_select(0, right_ids),
|
| 194 |
-
buckets,
|
| 195 |
-
signs,
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
def _raw_origin(value: float, label: str) -> float:
|
| 200 |
-
if isinstance(value, bool):
|
| 201 |
-
raise ValueError(f"{label} raw origin must be finite")
|
| 202 |
-
try:
|
| 203 |
-
origin = float(value)
|
| 204 |
-
except (TypeError, ValueError) as error:
|
| 205 |
-
raise ValueError(f"{label} raw origin must be finite") from error
|
| 206 |
-
if not math.isfinite(origin):
|
| 207 |
-
raise ValueError(f"{label} raw origin must be finite")
|
| 208 |
-
return origin
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
def _validated_pair_support(
|
| 212 |
-
source_keys: np.ndarray,
|
| 213 |
-
pair_support: np.ndarray | None,
|
| 214 |
-
minimum_pair_support: int,
|
| 215 |
-
) -> np.ndarray | None:
|
| 216 |
-
if (
|
| 217 |
-
isinstance(minimum_pair_support, bool)
|
| 218 |
-
or not isinstance(minimum_pair_support, int)
|
| 219 |
-
or minimum_pair_support < 1
|
| 220 |
-
):
|
| 221 |
-
raise ValueError("minimum pair support must be a positive integer")
|
| 222 |
-
if pair_support is None:
|
| 223 |
-
if minimum_pair_support != 1:
|
| 224 |
-
raise ValueError("minimum pair support above one requires pair support")
|
| 225 |
-
return None
|
| 226 |
-
support = np.asarray(pair_support)
|
| 227 |
-
if (
|
| 228 |
-
support.ndim != 1
|
| 229 |
-
or support.dtype != np.uint64
|
| 230 |
-
or bool((support < 1).any())
|
| 231 |
-
):
|
| 232 |
-
raise ValueError("pair support must be positive uint64")
|
| 233 |
-
if support.shape != source_keys.shape:
|
| 234 |
-
raise ValueError("pair support must be aligned to pair keys")
|
| 235 |
-
return support
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
def decision_raw_origins(policy: Mapping[str, object]) -> tuple[float, float]:
|
| 239 |
-
"""Validate the registered optimizer-centred policy and return its origins."""
|
| 240 |
-
expected = {
|
| 241 |
-
"protocol", "name", "ties", "explorer_component_used",
|
| 242 |
-
"selection_uses_heldout_or_benchmark_data", "pair_raw_origin",
|
| 243 |
-
"split_raw_origin", "pair_reference", "split_reference",
|
| 244 |
-
}
|
| 245 |
-
if (
|
| 246 |
-
not isinstance(policy, Mapping)
|
| 247 |
-
or set(policy) != expected
|
| 248 |
-
or policy.get("protocol") != "latent-compilation-decision-v1"
|
| 249 |
-
or policy.get("name") not in {
|
| 250 |
-
"optimizer-centered-advantage",
|
| 251 |
-
"optimizer-centered-support8-v1",
|
| 252 |
-
}
|
| 253 |
-
or policy.get("ties") != "inactive"
|
| 254 |
-
or policy.get("explorer_component_used") is not False
|
| 255 |
-
or policy.get("selection_uses_heldout_or_benchmark_data") is not False
|
| 256 |
-
):
|
| 257 |
-
raise ValueError("compiled decision policy schema is invalid")
|
| 258 |
-
pair_origin = _raw_origin(policy.get("pair_raw_origin"), "pair")
|
| 259 |
-
split_origin = _raw_origin(policy.get("split_raw_origin"), "split")
|
| 260 |
-
pair = policy.get("pair_reference")
|
| 261 |
-
split = policy.get("split_reference")
|
| 262 |
-
if (
|
| 263 |
-
not isinstance(pair, Mapping)
|
| 264 |
-
or set(pair) != {
|
| 265 |
-
"initial_potential", "initial_raw", "optimizer", "weight_decay"
|
| 266 |
-
}
|
| 267 |
-
or pair.get("initial_potential") != -2.0
|
| 268 |
-
or pair.get("initial_raw") != pair_origin
|
| 269 |
-
or pair.get("optimizer") != "sparse-adagrad"
|
| 270 |
-
or pair.get("weight_decay") != 0.0
|
| 271 |
-
or not isinstance(split, Mapping)
|
| 272 |
-
or set(split) != {
|
| 273 |
-
"initial_potential", "initial_raw", "optimizer", "dense_lr",
|
| 274 |
-
"warmup_steps", "weight_decay", "completed_steps",
|
| 275 |
-
"learning_rate_schedule", "adamw_decay_multiplier",
|
| 276 |
-
}
|
| 277 |
-
or split.get("initial_potential") != -2.0
|
| 278 |
-
or split.get("initial_raw") != pair_origin
|
| 279 |
-
or split.get("optimizer") != "adamw"
|
| 280 |
-
or split.get("learning_rate_schedule")
|
| 281 |
-
!= "constant-with-linear-warmup-using-prior-completed-steps"
|
| 282 |
-
):
|
| 283 |
-
raise ValueError("compiled decision policy reference is invalid")
|
| 284 |
-
for name in ("dense_lr", "weight_decay", "adamw_decay_multiplier"):
|
| 285 |
-
value = split.get(name)
|
| 286 |
-
if (
|
| 287 |
-
isinstance(value, bool)
|
| 288 |
-
or not isinstance(value, (int, float))
|
| 289 |
-
or not math.isfinite(float(value))
|
| 290 |
-
or float(value) < 0.0
|
| 291 |
-
):
|
| 292 |
-
raise ValueError("compiled split optimizer reference is invalid")
|
| 293 |
-
for name, allow_zero in (("warmup_steps", True), ("completed_steps", False)):
|
| 294 |
-
value = split.get(name)
|
| 295 |
-
if (
|
| 296 |
-
isinstance(value, bool)
|
| 297 |
-
or not isinstance(value, int)
|
| 298 |
-
or value < (0 if allow_zero else 1)
|
| 299 |
-
):
|
| 300 |
-
raise ValueError("compiled split optimizer reference is invalid")
|
| 301 |
-
if float(split["dense_lr"]) <= 0.0:
|
| 302 |
-
raise ValueError("compiled split optimizer learning rate is invalid")
|
| 303 |
-
return pair_origin, split_origin
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
def validate_decision_support_coupling(
|
| 307 |
-
policy: Mapping[str, object] | None,
|
| 308 |
-
pair_support_filter: Mapping[str, object] | None,
|
| 309 |
-
*,
|
| 310 |
-
source_checkpoint: Mapping[str, object] | None = None,
|
| 311 |
-
) -> None:
|
| 312 |
-
"""Require the preregistered support marker and its evidence together."""
|
| 313 |
-
policy_name = None if policy is None else policy.get("name")
|
| 314 |
-
support8 = policy_name == "optimizer-centered-support8-v1"
|
| 315 |
-
if support8 != (pair_support_filter is not None):
|
| 316 |
-
raise ValueError("compiled support8 policy and filter must be coupled")
|
| 317 |
-
if pair_support_filter is None:
|
| 318 |
-
return
|
| 319 |
-
inputs = None if source_checkpoint is None else source_checkpoint.get("inputs")
|
| 320 |
-
inventory = pair_support_filter.get("inventory")
|
| 321 |
-
pair_keys = pair_support_filter.get("pair_keys")
|
| 322 |
-
if (
|
| 323 |
-
not isinstance(inputs, Mapping)
|
| 324 |
-
or not isinstance(inventory, Mapping)
|
| 325 |
-
or not isinstance(pair_keys, Mapping)
|
| 326 |
-
or inventory.get("sha256") != inputs.get("inventory_manifest_sha256")
|
| 327 |
-
or pair_keys.get("sha256") != inputs.get("pair_keys_sha256")
|
| 328 |
-
):
|
| 329 |
-
raise ValueError("compiled support8 evidence differs from checkpoint inputs")
|
| 330 |
-
inventory_path = Path(str(inventory.get("path", "")))
|
| 331 |
-
try:
|
| 332 |
-
if (
|
| 333 |
-
not inventory_path.is_file()
|
| 334 |
-
or _sha256_file(inventory_path) != inventory.get("sha256")
|
| 335 |
-
or inventory_path.stat().st_size != inventory.get("bytes")
|
| 336 |
-
):
|
| 337 |
-
raise ValueError
|
| 338 |
-
with inventory_path.open(encoding="utf-8") as handle:
|
| 339 |
-
manifest = json.load(handle)
|
| 340 |
-
except (OSError, json.JSONDecodeError, ValueError) as error:
|
| 341 |
-
raise ValueError(
|
| 342 |
-
"compiled support8 evidence is not declared by inventory"
|
| 343 |
-
) from error
|
| 344 |
-
outputs = manifest.get("outputs") if isinstance(manifest, dict) else None
|
| 345 |
-
expected_outputs = {
|
| 346 |
-
"pair_keys.npy": pair_keys,
|
| 347 |
-
"pair_support.npy": pair_support_filter.get("source"),
|
| 348 |
-
}
|
| 349 |
-
if not isinstance(outputs, list):
|
| 350 |
-
raise ValueError("compiled support8 evidence is not declared by inventory")
|
| 351 |
-
for basename, expected in expected_outputs.items():
|
| 352 |
-
matches = [
|
| 353 |
-
record
|
| 354 |
-
for record in outputs
|
| 355 |
-
if isinstance(record, Mapping)
|
| 356 |
-
and Path(str(record.get("path", ""))).name == basename
|
| 357 |
-
]
|
| 358 |
-
if (
|
| 359 |
-
len(matches) != 1
|
| 360 |
-
or not isinstance(expected, Mapping)
|
| 361 |
-
or Path(str(matches[0].get("path", ""))).resolve()
|
| 362 |
-
!= Path(str(expected.get("path", ""))).resolve()
|
| 363 |
-
or matches[0].get("sha256") != expected.get("sha256")
|
| 364 |
-
):
|
| 365 |
-
raise ValueError(
|
| 366 |
-
"compiled support8 evidence is not declared by inventory"
|
| 367 |
-
)
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
def _map_pair_edge_scores(
|
| 371 |
-
model: LatentLexiconModel,
|
| 372 |
-
root_ids: Sequence[int],
|
| 373 |
-
*,
|
| 374 |
-
raw_origin: float,
|
| 375 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 376 |
-
origin = _raw_origin(raw_origin, "pair")
|
| 377 |
-
rows_cpu = model.pair_gate.index.lookup_sequence(root_ids)
|
| 378 |
-
device = model.embedding.weight.device
|
| 379 |
-
dtype = model.embedding.weight.dtype
|
| 380 |
-
rows = rows_cpu.to(device=device)
|
| 381 |
-
mask = rows >= 0
|
| 382 |
-
scores = torch.zeros(rows.shape, dtype=dtype, device=device)
|
| 383 |
-
positions = torch.nonzero(mask, as_tuple=False).flatten()
|
| 384 |
-
if positions.numel():
|
| 385 |
-
exact_rows = rows.index_select(0, positions)
|
| 386 |
-
raw = (
|
| 387 |
-
model.pair_gate.base_raw.to(device=device, dtype=dtype)
|
| 388 |
-
+ model.pair_gate.gates(exact_rows).squeeze(1)
|
| 389 |
-
- origin
|
| 390 |
-
)
|
| 391 |
-
scores.index_copy_(
|
| 392 |
-
0,
|
| 393 |
-
positions,
|
| 394 |
-
bounded_potential(raw, model.pair_gate.potential_bound),
|
| 395 |
-
)
|
| 396 |
-
return scores, mask
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
@torch.no_grad()
|
| 400 |
-
def map_latent_sentence(
|
| 401 |
-
model: LatentLexiconModel,
|
| 402 |
-
roots: Sequence[int],
|
| 403 |
-
*,
|
| 404 |
-
pair_raw_origin: float = 0.0,
|
| 405 |
-
split_raw_origin: float = 0.0,
|
| 406 |
-
pair_support: np.ndarray | None = None,
|
| 407 |
-
minimum_pair_support: int = 1,
|
| 408 |
-
) -> CompiledSentence:
|
| 409 |
-
root_ids = tuple(int(token) for token in roots)
|
| 410 |
-
model_vocab_size = model.embedding.num_embeddings
|
| 411 |
-
if not root_ids:
|
| 412 |
-
raise ValueError("latent MAP sentence needs at least one root token")
|
| 413 |
-
if min(root_ids) < 0 or max(root_ids) >= model_vocab_size:
|
| 414 |
-
raise ValueError("root token id is outside the model vocabulary")
|
| 415 |
-
|
| 416 |
-
source_keys = model.pair_gate.index.keys
|
| 417 |
-
support = _validated_pair_support(
|
| 418 |
-
source_keys, pair_support, minimum_pair_support
|
| 419 |
-
)
|
| 420 |
-
|
| 421 |
-
edge_scores, edge_mask = _map_pair_edge_scores(
|
| 422 |
-
model, root_ids, raw_origin=pair_raw_origin
|
| 423 |
-
)
|
| 424 |
-
if support is not None:
|
| 425 |
-
rows = model.pair_gate.index.lookup_sequence(root_ids).numpy()
|
| 426 |
-
eligible = np.zeros(rows.shape, dtype=np.bool_)
|
| 427 |
-
present = rows >= 0
|
| 428 |
-
eligible[present] = support[rows[present]] >= minimum_pair_support
|
| 429 |
-
edge_mask &= torch.as_tensor(eligible, device=edge_mask.device)
|
| 430 |
-
selected = map_matching(edge_scores, edge_mask).tolist()
|
| 431 |
-
split_scores = bounded_potential(
|
| 432 |
-
model.split_raw_scores - _raw_origin(split_raw_origin, "split"),
|
| 433 |
-
model.potential_bound,
|
| 434 |
-
)
|
| 435 |
-
pair_edges = [edge for edge, chosen in enumerate(selected) if chosen]
|
| 436 |
-
pair_vectors: dict[int, torch.Tensor] = {}
|
| 437 |
-
if pair_edges:
|
| 438 |
-
device = model.embedding.weight.device
|
| 439 |
-
left_ids = torch.tensor(
|
| 440 |
-
[root_ids[edge] for edge in pair_edges],
|
| 441 |
-
dtype=torch.long,
|
| 442 |
-
device=device,
|
| 443 |
-
)
|
| 444 |
-
right_ids = torch.tensor(
|
| 445 |
-
[root_ids[edge + 1] for edge in pair_edges],
|
| 446 |
-
dtype=torch.long,
|
| 447 |
-
device=device,
|
| 448 |
-
)
|
| 449 |
-
materialized = _pair_vectors(model, left_ids, right_ids)
|
| 450 |
-
pair_vectors = {
|
| 451 |
-
edge: materialized[index] for index, edge in enumerate(pair_edges)
|
| 452 |
-
}
|
| 453 |
-
|
| 454 |
-
emissions: list[LexiconEmission] = []
|
| 455 |
-
vectors: list[torch.Tensor] = []
|
| 456 |
-
position = 0
|
| 457 |
-
while position < len(root_ids):
|
| 458 |
-
if position < len(selected) and selected[position]:
|
| 459 |
-
pair = (root_ids[position], root_ids[position + 1])
|
| 460 |
-
emissions.append(LexiconEmission("pair", pair))
|
| 461 |
-
vectors.append(pair_vectors[position])
|
| 462 |
-
position += 2
|
| 463 |
-
continue
|
| 464 |
-
split = map_split_frontier(
|
| 465 |
-
root_ids[position],
|
| 466 |
-
model.parent_left,
|
| 467 |
-
model.parent_right,
|
| 468 |
-
split_scores,
|
| 469 |
-
)
|
| 470 |
-
for token in split.frontier:
|
| 471 |
-
emissions.append(LexiconEmission("original", (token,)))
|
| 472 |
-
vectors.append(model.embedding.weight[token])
|
| 473 |
-
position += 1
|
| 474 |
-
return CompiledSentence(
|
| 475 |
-
emissions=tuple(emissions),
|
| 476 |
-
numerator=torch.stack(vectors).sum(dim=0),
|
| 477 |
-
)
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
def _compile_split_bits(
|
| 481 |
-
model: LatentLexiconModel, *, split_raw_origin: float = 0.0
|
| 482 |
-
) -> torch.Tensor:
|
| 483 |
-
scores = bounded_potential(
|
| 484 |
-
model.split_raw_scores.detach()
|
| 485 |
-
- _raw_origin(split_raw_origin, "split"),
|
| 486 |
-
model.potential_bound,
|
| 487 |
-
)
|
| 488 |
-
left = model.parent_left.detach().to(device="cpu", dtype=torch.long)
|
| 489 |
-
right = model.parent_right.detach().to(device="cpu", dtype=torch.long)
|
| 490 |
-
vocabulary_size = scores.shape[0]
|
| 491 |
-
state = [0] * vocabulary_size
|
| 492 |
-
depth = [0] * vocabulary_size
|
| 493 |
-
|
| 494 |
-
def visit(token: int) -> int:
|
| 495 |
-
if state[token] == 2:
|
| 496 |
-
return depth[token]
|
| 497 |
-
if state[token] == 1:
|
| 498 |
-
raise ValueError(f"cycle in split tree at token {token}")
|
| 499 |
-
state[token] = 1
|
| 500 |
-
left_id = int(left[token])
|
| 501 |
-
right_id = int(right[token])
|
| 502 |
-
if (left_id < 0) != (right_id < 0):
|
| 503 |
-
raise ValueError(f"token {token} must have both children or neither")
|
| 504 |
-
if left_id < 0:
|
| 505 |
-
token_depth = 0
|
| 506 |
-
else:
|
| 507 |
-
if not 0 <= left_id < vocabulary_size or not 0 <= right_id < vocabulary_size:
|
| 508 |
-
raise ValueError(f"token {token} has a child outside child arrays")
|
| 509 |
-
token_depth = max(visit(left_id), visit(right_id)) + 1
|
| 510 |
-
depth[token] = token_depth
|
| 511 |
-
state[token] = 2
|
| 512 |
-
return token_depth
|
| 513 |
-
|
| 514 |
-
for token in range(vocabulary_size):
|
| 515 |
-
visit(token)
|
| 516 |
-
|
| 517 |
-
levels: dict[int, list[int]] = {}
|
| 518 |
-
for token, token_depth in enumerate(depth):
|
| 519 |
-
if token_depth:
|
| 520 |
-
levels.setdefault(token_depth, []).append(token)
|
| 521 |
-
|
| 522 |
-
device = scores.device
|
| 523 |
-
active = torch.zeros(vocabulary_size, dtype=torch.bool, device=device)
|
| 524 |
-
best = torch.zeros_like(scores)
|
| 525 |
-
for token_depth in sorted(levels):
|
| 526 |
-
tokens = torch.tensor(levels[token_depth], dtype=torch.long, device=device)
|
| 527 |
-
left_ids = left.index_select(0, tokens.cpu()).to(device=device)
|
| 528 |
-
right_ids = right.index_select(0, tokens.cpu()).to(device=device)
|
| 529 |
-
node_scores = scores.index_select(0, tokens)
|
| 530 |
-
stop_scores = F.logsigmoid(-node_scores)
|
| 531 |
-
split_scores = F.logsigmoid(node_scores) + best.index_select(0, left_ids)
|
| 532 |
-
split_scores = split_scores + best.index_select(0, right_ids)
|
| 533 |
-
choose_split = split_scores > stop_scores
|
| 534 |
-
active.index_copy_(0, tokens, choose_split)
|
| 535 |
-
best.index_copy_(
|
| 536 |
-
0, tokens, torch.where(choose_split, split_scores, stop_scores)
|
| 537 |
-
)
|
| 538 |
-
return active.cpu()
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
@torch.no_grad()
|
| 542 |
-
def compile_latent_model(
|
| 543 |
-
model: LatentLexiconModel,
|
| 544 |
-
*,
|
| 545 |
-
pair_chunk_size: int = 65536,
|
| 546 |
-
pair_raw_origin: float = 0.0,
|
| 547 |
-
split_raw_origin: float = 0.0,
|
| 548 |
-
pair_support: np.ndarray | None = None,
|
| 549 |
-
minimum_pair_support: int = 1,
|
| 550 |
-
) -> CompiledLatentLexicon:
|
| 551 |
-
if (
|
| 552 |
-
isinstance(pair_chunk_size, bool)
|
| 553 |
-
or not isinstance(pair_chunk_size, int)
|
| 554 |
-
or pair_chunk_size <= 0
|
| 555 |
-
):
|
| 556 |
-
raise ValueError("pair chunk size must be a positive integer")
|
| 557 |
-
device = model.embedding.weight.device
|
| 558 |
-
dtype = model.embedding.weight.dtype
|
| 559 |
-
pair_origin = _raw_origin(pair_raw_origin, "pair")
|
| 560 |
-
split_origin = _raw_origin(split_raw_origin, "split")
|
| 561 |
-
source_keys = model.pair_gate.index.keys
|
| 562 |
-
support = _validated_pair_support(
|
| 563 |
-
source_keys, pair_support, minimum_pair_support
|
| 564 |
-
)
|
| 565 |
-
base_raw = model.pair_gate.base_raw.to(device=device, dtype=dtype)
|
| 566 |
-
active_pair_count = 0
|
| 567 |
-
for start in range(0, len(source_keys), pair_chunk_size):
|
| 568 |
-
stop = min(start + pair_chunk_size, len(source_keys))
|
| 569 |
-
raw = (
|
| 570 |
-
base_raw + model.pair_gate.gates.weight[start:stop, 0] - pair_origin
|
| 571 |
-
)
|
| 572 |
-
scores = bounded_potential(raw, model.pair_gate.potential_bound)
|
| 573 |
-
active = scores > 0
|
| 574 |
-
if support is not None:
|
| 575 |
-
active &= torch.as_tensor(
|
| 576 |
-
support[start:stop] >= minimum_pair_support,
|
| 577 |
-
dtype=torch.bool,
|
| 578 |
-
device=device,
|
| 579 |
-
)
|
| 580 |
-
active_pair_count += int(active.sum().item())
|
| 581 |
-
|
| 582 |
-
model_vocab_size = model.embedding.num_embeddings
|
| 583 |
-
dimension = model.embedding.embedding_dim
|
| 584 |
-
embedding = torch.empty(
|
| 585 |
-
(model_vocab_size + active_pair_count, dimension), dtype=dtype
|
| 586 |
-
)
|
| 587 |
-
embedding[:model_vocab_size].copy_(model.embedding.weight.detach().cpu())
|
| 588 |
-
pair_keys = np.empty((active_pair_count,), dtype=np.uint64)
|
| 589 |
-
pair_scores = torch.empty((active_pair_count,), dtype=dtype)
|
| 590 |
-
output_offset = 0
|
| 591 |
-
for start in range(0, len(source_keys), pair_chunk_size):
|
| 592 |
-
stop = min(start + pair_chunk_size, len(source_keys))
|
| 593 |
-
raw = (
|
| 594 |
-
base_raw + model.pair_gate.gates.weight[start:stop, 0] - pair_origin
|
| 595 |
-
)
|
| 596 |
-
scores = bounded_potential(raw, model.pair_gate.potential_bound)
|
| 597 |
-
active = scores > 0
|
| 598 |
-
if support is not None:
|
| 599 |
-
active &= torch.as_tensor(
|
| 600 |
-
support[start:stop] >= minimum_pair_support,
|
| 601 |
-
dtype=torch.bool,
|
| 602 |
-
device=device,
|
| 603 |
-
)
|
| 604 |
-
rows = torch.nonzero(active, as_tuple=False).flatten()
|
| 605 |
-
count = rows.numel()
|
| 606 |
-
if count == 0:
|
| 607 |
-
continue
|
| 608 |
-
keys = np.asarray(source_keys[start:stop])[rows.cpu().numpy()].copy()
|
| 609 |
-
left = torch.as_tensor(
|
| 610 |
-
(keys >> np.uint64(32)).astype(np.int64),
|
| 611 |
-
dtype=torch.long,
|
| 612 |
-
device=device,
|
| 613 |
-
)
|
| 614 |
-
right = torch.as_tensor(
|
| 615 |
-
(keys & np.uint64(0xFFFFFFFF)).astype(np.int64),
|
| 616 |
-
dtype=torch.long,
|
| 617 |
-
device=device,
|
| 618 |
-
)
|
| 619 |
-
output_stop = output_offset + count
|
| 620 |
-
pair_keys[output_offset:output_stop] = keys
|
| 621 |
-
pair_scores[output_offset:output_stop].copy_(
|
| 622 |
-
scores.index_select(0, rows).detach().cpu()
|
| 623 |
-
)
|
| 624 |
-
embedding[
|
| 625 |
-
model_vocab_size + output_offset:model_vocab_size + output_stop
|
| 626 |
-
].copy_(_pair_vectors(model, left, right).detach().cpu())
|
| 627 |
-
output_offset = output_stop
|
| 628 |
-
if output_offset != active_pair_count:
|
| 629 |
-
raise RuntimeError("active pair count changed while compiling the model")
|
| 630 |
-
|
| 631 |
-
return CompiledLatentLexicon(
|
| 632 |
-
embedding=embedding,
|
| 633 |
-
model_vocab_size=model_vocab_size,
|
| 634 |
-
split_active=_compile_split_bits(model, split_raw_origin=split_origin),
|
| 635 |
-
parent_left=model.parent_left.detach().cpu().clone(),
|
| 636 |
-
parent_right=model.parent_right.detach().cpu().clone(),
|
| 637 |
-
pair_keys=pair_keys,
|
| 638 |
-
pair_scores=pair_scores,
|
| 639 |
-
)
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
def _sha256_file(path: Path) -> str:
|
| 643 |
-
digest = hashlib.sha256()
|
| 644 |
-
with path.open("rb") as handle:
|
| 645 |
-
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
| 646 |
-
digest.update(chunk)
|
| 647 |
-
return digest.hexdigest()
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
_ARTIFACT_PROTOCOL = "compiled-latent-lexicon-v1"
|
| 651 |
-
_ARTIFACT_FILES = {"manifest.json", "model.safetensors", "routing.npz", "tokenizer.json"}
|
| 652 |
-
_ROUTING_ARRAYS = {
|
| 653 |
-
"split_active", "parent_left", "parent_right", "pair_keys", "pair_scores"
|
| 654 |
-
}
|
| 655 |
-
_SOURCE_KEYS = {
|
| 656 |
-
"protocol", "completed_steps", "stream_identity_sha256", "manifest",
|
| 657 |
-
"model_checkpoint", "run_sha256", "inputs",
|
| 658 |
-
}
|
| 659 |
-
_SOURCE_INPUT_KEYS = {
|
| 660 |
-
"initializer_sha256", "inventory_manifest_sha256", "pair_keys_sha256",
|
| 661 |
-
"tokenizer_sha256",
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
def _require_sha256(value: object, label: str) -> str:
|
| 666 |
-
if (
|
| 667 |
-
not isinstance(value, str)
|
| 668 |
-
or len(value) != 64
|
| 669 |
-
or any(character not in "0123456789abcdef" for character in value)
|
| 670 |
-
):
|
| 671 |
-
raise ValueError(f"compiled source {label} hash is invalid")
|
| 672 |
-
return value
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
def _validate_source_checkpoint(value: object) -> dict[str, object]:
|
| 676 |
-
copied = _json_copy(value)
|
| 677 |
-
if (
|
| 678 |
-
not isinstance(copied, dict)
|
| 679 |
-
or set(copied) != _SOURCE_KEYS
|
| 680 |
-
or copied.get("protocol") != "compiled-latent-streaming-source-v1"
|
| 681 |
-
):
|
| 682 |
-
raise ValueError("compiled source checkpoint schema is invalid")
|
| 683 |
-
completed = copied.get("completed_steps")
|
| 684 |
-
if isinstance(completed, bool) or not isinstance(completed, int) or completed <= 0:
|
| 685 |
-
raise ValueError("compiled source checkpoint step is invalid")
|
| 686 |
-
_require_sha256(copied.get("stream_identity_sha256"), "stream identity")
|
| 687 |
-
_require_sha256(copied.get("run_sha256"), "run")
|
| 688 |
-
manifest = copied.get("manifest")
|
| 689 |
-
if not isinstance(manifest, dict) or set(manifest) != {"file", "sha256"}:
|
| 690 |
-
raise ValueError("compiled source manifest identity is invalid")
|
| 691 |
-
manifest_file = manifest.get("file")
|
| 692 |
-
if not isinstance(manifest_file, str) or Path(manifest_file).name != manifest_file:
|
| 693 |
-
raise ValueError("compiled source manifest filename is invalid")
|
| 694 |
-
_require_sha256(manifest.get("sha256"), "manifest")
|
| 695 |
-
model = copied.get("model_checkpoint")
|
| 696 |
-
if (
|
| 697 |
-
not isinstance(model, dict)
|
| 698 |
-
or set(model) != {"file", "sha256", "bytes"}
|
| 699 |
-
):
|
| 700 |
-
raise ValueError("compiled source model identity is invalid")
|
| 701 |
-
model_file = model.get("file")
|
| 702 |
-
model_bytes = model.get("bytes")
|
| 703 |
-
if (
|
| 704 |
-
not isinstance(model_file, str)
|
| 705 |
-
or Path(model_file).name != model_file
|
| 706 |
-
or isinstance(model_bytes, bool)
|
| 707 |
-
or not isinstance(model_bytes, int)
|
| 708 |
-
or model_bytes <= 0
|
| 709 |
-
):
|
| 710 |
-
raise ValueError("compiled source model record is invalid")
|
| 711 |
-
_require_sha256(model.get("sha256"), "model")
|
| 712 |
-
inputs = copied.get("inputs")
|
| 713 |
-
if not isinstance(inputs, dict) or set(inputs) != _SOURCE_INPUT_KEYS:
|
| 714 |
-
raise ValueError("compiled source input identities are invalid")
|
| 715 |
-
for name, digest in inputs.items():
|
| 716 |
-
_require_sha256(digest, name)
|
| 717 |
-
return copied
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
def _validate_compiled(compiled: CompiledLatentLexicon) -> None:
|
| 721 |
-
embedding = compiled.embedding
|
| 722 |
-
if (
|
| 723 |
-
embedding.ndim != 2
|
| 724 |
-
or embedding.dtype not in (torch.float32, torch.float64)
|
| 725 |
-
or embedding.device.type != "cpu"
|
| 726 |
-
or not embedding.is_contiguous()
|
| 727 |
-
or not _tensor_is_finite(embedding)
|
| 728 |
-
):
|
| 729 |
-
raise ValueError("compiled embedding table is invalid")
|
| 730 |
-
vocabulary_size = compiled.model_vocab_size
|
| 731 |
-
if (
|
| 732 |
-
isinstance(vocabulary_size, bool)
|
| 733 |
-
or not isinstance(vocabulary_size, int)
|
| 734 |
-
or vocabulary_size <= 0
|
| 735 |
-
or vocabulary_size >= (1 << 32)
|
| 736 |
-
):
|
| 737 |
-
raise ValueError("compiled model vocabulary size is invalid")
|
| 738 |
-
expected_token_shape = (vocabulary_size,)
|
| 739 |
-
if (
|
| 740 |
-
compiled.split_active.shape != expected_token_shape
|
| 741 |
-
or compiled.split_active.dtype != torch.bool
|
| 742 |
-
or compiled.split_active.device.type != "cpu"
|
| 743 |
-
):
|
| 744 |
-
raise ValueError("compiled split routing is invalid")
|
| 745 |
-
for name, parent in (
|
| 746 |
-
("left", compiled.parent_left), ("right", compiled.parent_right)
|
| 747 |
-
):
|
| 748 |
-
if (
|
| 749 |
-
parent.shape != expected_token_shape
|
| 750 |
-
or parent.dtype != torch.long
|
| 751 |
-
or parent.device.type != "cpu"
|
| 752 |
-
):
|
| 753 |
-
raise ValueError(f"compiled {name} parent routing is invalid")
|
| 754 |
-
left = compiled.parent_left.numpy()
|
| 755 |
-
right = compiled.parent_right.numpy()
|
| 756 |
-
if bool(((left < 0) != (right < 0)).any()):
|
| 757 |
-
raise ValueError("compiled token must have both children or neither")
|
| 758 |
-
if bool(compiled.split_active.numpy()[left < 0].any()):
|
| 759 |
-
raise ValueError("compiled atomic token cannot have an active split")
|
| 760 |
-
children = np.concatenate((left[left >= 0], right[right >= 0]))
|
| 761 |
-
if children.size and (
|
| 762 |
-
int(children.min()) < 0 or int(children.max()) >= vocabulary_size
|
| 763 |
-
):
|
| 764 |
-
raise ValueError("compiled child is outside the model vocabulary")
|
| 765 |
-
state = [0] * vocabulary_size
|
| 766 |
-
|
| 767 |
-
def visit(token: int) -> None:
|
| 768 |
-
if state[token] == 2:
|
| 769 |
-
return
|
| 770 |
-
if state[token] == 1:
|
| 771 |
-
raise ValueError(f"cycle in compiled split tree at token {token}")
|
| 772 |
-
state[token] = 1
|
| 773 |
-
if left[token] >= 0:
|
| 774 |
-
visit(int(left[token]))
|
| 775 |
-
visit(int(right[token]))
|
| 776 |
-
state[token] = 2
|
| 777 |
-
|
| 778 |
-
for token in range(vocabulary_size):
|
| 779 |
-
visit(token)
|
| 780 |
-
if not isinstance(compiled.pair_keys, np.ndarray):
|
| 781 |
-
raise ValueError("compiled pair keys must be a NumPy array")
|
| 782 |
-
if (
|
| 783 |
-
compiled.pair_keys.ndim != 1
|
| 784 |
-
or compiled.pair_keys.dtype != np.uint64
|
| 785 |
-
or not compiled.pair_keys.flags.c_contiguous
|
| 786 |
-
):
|
| 787 |
-
raise ValueError("compiled pair keys are invalid")
|
| 788 |
-
if compiled.pair_keys.size > 1 and not bool(
|
| 789 |
-
(compiled.pair_keys[1:] > compiled.pair_keys[:-1]).all()
|
| 790 |
-
):
|
| 791 |
-
raise ValueError("compiled pair keys must be strictly increasing")
|
| 792 |
-
if compiled.pair_keys.size:
|
| 793 |
-
pair_left = compiled.pair_keys >> np.uint64(32)
|
| 794 |
-
pair_right = compiled.pair_keys & np.uint64(0xFFFFFFFF)
|
| 795 |
-
if (
|
| 796 |
-
int(pair_left.max()) >= vocabulary_size
|
| 797 |
-
or int(pair_right.max()) >= vocabulary_size
|
| 798 |
-
):
|
| 799 |
-
raise ValueError("compiled pair key is outside the model vocabulary")
|
| 800 |
-
pair_count = int(compiled.pair_keys.size)
|
| 801 |
-
if (
|
| 802 |
-
compiled.pair_scores.shape != (pair_count,)
|
| 803 |
-
or compiled.pair_scores.dtype != embedding.dtype
|
| 804 |
-
or compiled.pair_scores.device.type != "cpu"
|
| 805 |
-
or not compiled.pair_scores.is_contiguous()
|
| 806 |
-
or not _tensor_is_finite(compiled.pair_scores)
|
| 807 |
-
or (pair_count and not bool((compiled.pair_scores > 0).all().item()))
|
| 808 |
-
):
|
| 809 |
-
raise ValueError("compiled pair scores are invalid")
|
| 810 |
-
if embedding.shape[0] != vocabulary_size + pair_count:
|
| 811 |
-
raise ValueError("compiled table and routing cardinalities differ")
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
def _json_copy(value: object) -> object:
|
| 815 |
-
try:
|
| 816 |
-
return json.loads(json.dumps(value, sort_keys=True, allow_nan=False))
|
| 817 |
-
except (TypeError, ValueError) as error:
|
| 818 |
-
raise ValueError("compiled source checkpoint identity is not JSON") from error
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
def _validated_support_source(
|
| 822 |
-
value: object, *, basename: str | None, label: str
|
| 823 |
-
) -> dict[str, object]:
|
| 824 |
-
if (
|
| 825 |
-
not isinstance(value, dict)
|
| 826 |
-
or set(value) != {"path", "sha256", "bytes"}
|
| 827 |
-
or not isinstance(value.get("path"), str)
|
| 828 |
-
or not Path(value["path"]).is_absolute()
|
| 829 |
-
or (basename is not None and Path(value["path"]).name != basename)
|
| 830 |
-
or isinstance(value.get("bytes"), bool)
|
| 831 |
-
or not isinstance(value.get("bytes"), int)
|
| 832 |
-
or value["bytes"] <= 0
|
| 833 |
-
):
|
| 834 |
-
raise ValueError(f"compiled {label} source is invalid")
|
| 835 |
-
_require_sha256(value.get("sha256"), label)
|
| 836 |
-
return value
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
def _validated_pair_support_filter(value: object) -> dict[str, object]:
|
| 840 |
-
copied = _json_copy(value)
|
| 841 |
-
if not isinstance(copied, dict) or set(copied) != {
|
| 842 |
-
"protocol", "minimum_pair_support", "inventory", "pair_keys", "source",
|
| 843 |
-
}:
|
| 844 |
-
raise ValueError("compiled pair support filter is invalid")
|
| 845 |
-
if (
|
| 846 |
-
copied.get("protocol") != "latent-pair-support-filter-v1"
|
| 847 |
-
or copied.get("minimum_pair_support") != 8
|
| 848 |
-
):
|
| 849 |
-
raise ValueError("compiled pair support filter is invalid")
|
| 850 |
-
_validated_support_source(
|
| 851 |
-
copied.get("inventory"), basename=None, label="pair inventory"
|
| 852 |
-
)
|
| 853 |
-
inventory = copied["inventory"]
|
| 854 |
-
assert isinstance(inventory, dict)
|
| 855 |
-
if Path(inventory["path"]).suffix != ".json":
|
| 856 |
-
raise ValueError("compiled pair inventory source is invalid")
|
| 857 |
-
_validated_support_source(
|
| 858 |
-
copied.get("pair_keys"), basename="pair_keys.npy", label="pair keys"
|
| 859 |
-
)
|
| 860 |
-
_validated_support_source(
|
| 861 |
-
copied.get("source"), basename="pair_support.npy", label="pair support"
|
| 862 |
-
)
|
| 863 |
-
return copied
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
def _file_record(path: Path) -> dict[str, object]:
|
| 867 |
-
return {"sha256": _sha256_file(path), "bytes": path.stat().st_size}
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
def _fsync_file(path: Path) -> None:
|
| 871 |
-
with path.open("rb") as handle:
|
| 872 |
-
os.fsync(handle.fileno())
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
def _fsync_directory(path: Path) -> None:
|
| 876 |
-
descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
|
| 877 |
-
try:
|
| 878 |
-
os.fsync(descriptor)
|
| 879 |
-
finally:
|
| 880 |
-
os.close(descriptor)
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
def _load_json(path: Path) -> dict[str, object]:
|
| 884 |
-
try:
|
| 885 |
-
with path.open(encoding="utf-8") as handle:
|
| 886 |
-
value = json.load(handle)
|
| 887 |
-
except (OSError, json.JSONDecodeError) as error:
|
| 888 |
-
raise ValueError(f"compiled artifact JSON is invalid: {path.name}") from error
|
| 889 |
-
if not isinstance(value, dict):
|
| 890 |
-
raise ValueError(f"compiled artifact JSON is invalid: {path.name}")
|
| 891 |
-
return value
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
def _manifest_integer(
|
| 895 |
-
manifest: Mapping[str, object], name: str, *, positive: bool
|
| 896 |
-
) -> int:
|
| 897 |
-
value = manifest.get(name)
|
| 898 |
-
if (
|
| 899 |
-
isinstance(value, bool)
|
| 900 |
-
or not isinstance(value, int)
|
| 901 |
-
or (value <= 0 if positive else value < 0)
|
| 902 |
-
):
|
| 903 |
-
raise ValueError(f"compiled artifact manifest {name} is invalid")
|
| 904 |
-
return value
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
def save_compiled_lexicon(
|
| 908 |
-
output: str | Path,
|
| 909 |
-
compiled: CompiledLatentLexicon,
|
| 910 |
-
tokenizer_path: str | Path,
|
| 911 |
-
*,
|
| 912 |
-
source_checkpoint: Mapping[str, object],
|
| 913 |
-
decision_policy: Mapping[str, object] | None = None,
|
| 914 |
-
pair_support_filter: Mapping[str, object] | None = None,
|
| 915 |
-
) -> dict[str, object]:
|
| 916 |
-
output_path = Path(output)
|
| 917 |
-
if output_path.exists() and (
|
| 918 |
-
not output_path.is_dir() or any(output_path.iterdir())
|
| 919 |
-
):
|
| 920 |
-
raise ValueError(f"refusing nonempty compiled output: {output_path}")
|
| 921 |
-
_validate_compiled(compiled)
|
| 922 |
-
tokenizer_source = Path(tokenizer_path)
|
| 923 |
-
if not tokenizer_source.is_file():
|
| 924 |
-
raise ValueError("compiled tokenizer source does not exist")
|
| 925 |
-
copied_source = _validate_source_checkpoint(dict(source_checkpoint))
|
| 926 |
-
copied_policy = None
|
| 927 |
-
if decision_policy is not None:
|
| 928 |
-
decision_raw_origins(decision_policy)
|
| 929 |
-
copied_policy = _json_copy(dict(decision_policy))
|
| 930 |
-
copied_support_filter = (
|
| 931 |
-
None
|
| 932 |
-
if pair_support_filter is None
|
| 933 |
-
else _validated_pair_support_filter(pair_support_filter)
|
| 934 |
-
)
|
| 935 |
-
validate_decision_support_coupling(
|
| 936 |
-
copied_policy,
|
| 937 |
-
copied_support_filter,
|
| 938 |
-
source_checkpoint=copied_source,
|
| 939 |
-
)
|
| 940 |
-
|
| 941 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 942 |
-
staging = Path(tempfile.mkdtemp(
|
| 943 |
-
prefix=f".{output_path.name}.tmp-", dir=output_path.parent
|
| 944 |
-
))
|
| 945 |
-
published = False
|
| 946 |
-
try:
|
| 947 |
-
model_path = staging / "model.safetensors"
|
| 948 |
-
routing_path = staging / "routing.npz"
|
| 949 |
-
copied_tokenizer = staging / "tokenizer.json"
|
| 950 |
-
save_file({"embedding.weight": compiled.embedding.contiguous()}, model_path)
|
| 951 |
-
np.savez(
|
| 952 |
-
routing_path,
|
| 953 |
-
split_active=compiled.split_active.numpy(),
|
| 954 |
-
parent_left=compiled.parent_left.numpy(),
|
| 955 |
-
parent_right=compiled.parent_right.numpy(),
|
| 956 |
-
pair_keys=compiled.pair_keys,
|
| 957 |
-
pair_scores=compiled.pair_scores.numpy(),
|
| 958 |
-
)
|
| 959 |
-
shutil.copyfile(tokenizer_source, copied_tokenizer)
|
| 960 |
-
for payload_path in (model_path, routing_path, copied_tokenizer):
|
| 961 |
-
_fsync_file(payload_path)
|
| 962 |
-
files = {
|
| 963 |
-
path.name: _file_record(path)
|
| 964 |
-
for path in (model_path, routing_path, copied_tokenizer)
|
| 965 |
-
}
|
| 966 |
-
pair_count = int(compiled.pair_keys.size)
|
| 967 |
-
table_rows = int(compiled.embedding.shape[0])
|
| 968 |
-
manifest = {
|
| 969 |
-
"protocol": _ARTIFACT_PROTOCOL,
|
| 970 |
-
"model_vocab_size": compiled.model_vocab_size,
|
| 971 |
-
"active_pair_count": pair_count,
|
| 972 |
-
"active_row_count": table_rows,
|
| 973 |
-
"table_rows": table_rows,
|
| 974 |
-
"compiled_identity": compiled.identity(),
|
| 975 |
-
"source_checkpoint": copied_source,
|
| 976 |
-
"files": files,
|
| 977 |
-
}
|
| 978 |
-
if copied_policy is not None:
|
| 979 |
-
manifest["decision_policy"] = copied_policy
|
| 980 |
-
if copied_support_filter is not None:
|
| 981 |
-
manifest["pair_support_filter"] = copied_support_filter
|
| 982 |
-
manifest_path = staging / "manifest.json"
|
| 983 |
-
with manifest_path.open("w", encoding="utf-8") as handle:
|
| 984 |
-
json.dump(manifest, handle, indent=2, sort_keys=True)
|
| 985 |
-
handle.write("\n")
|
| 986 |
-
handle.flush()
|
| 987 |
-
os.fsync(handle.fileno())
|
| 988 |
-
_fsync_directory(staging)
|
| 989 |
-
if output_path.exists():
|
| 990 |
-
output_path.rmdir()
|
| 991 |
-
os.replace(staging, output_path)
|
| 992 |
-
_fsync_directory(output_path.parent)
|
| 993 |
-
published = True
|
| 994 |
-
return manifest
|
| 995 |
-
finally:
|
| 996 |
-
if not published:
|
| 997 |
-
shutil.rmtree(staging, ignore_errors=True)
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
def load_compiled_lexicon(output: str | Path) -> CompiledLatentLexicon:
|
| 1001 |
-
output_path = Path(output)
|
| 1002 |
-
if not output_path.is_dir() or {
|
| 1003 |
-
path.name for path in output_path.iterdir()
|
| 1004 |
-
} != _ARTIFACT_FILES:
|
| 1005 |
-
raise ValueError("compiled artifact file inventory is invalid")
|
| 1006 |
-
manifest = _load_json(output_path / "manifest.json")
|
| 1007 |
-
expected_manifest_keys = {
|
| 1008 |
-
"protocol", "model_vocab_size", "active_pair_count", "active_row_count",
|
| 1009 |
-
"table_rows", "compiled_identity", "source_checkpoint", "files",
|
| 1010 |
-
}
|
| 1011 |
-
optional_manifest_keys = {"decision_policy", "pair_support_filter"}
|
| 1012 |
-
if (
|
| 1013 |
-
not expected_manifest_keys <= set(manifest) <= expected_manifest_keys | optional_manifest_keys
|
| 1014 |
-
or manifest.get("protocol") != _ARTIFACT_PROTOCOL
|
| 1015 |
-
):
|
| 1016 |
-
raise ValueError("compiled artifact manifest is invalid")
|
| 1017 |
-
policy = None
|
| 1018 |
-
if "decision_policy" in manifest:
|
| 1019 |
-
policy = manifest["decision_policy"]
|
| 1020 |
-
if not isinstance(policy, Mapping):
|
| 1021 |
-
raise ValueError("compiled artifact decision policy is invalid")
|
| 1022 |
-
decision_raw_origins(policy)
|
| 1023 |
-
support_filter = None
|
| 1024 |
-
if "pair_support_filter" in manifest:
|
| 1025 |
-
support_filter = _validated_pair_support_filter(
|
| 1026 |
-
manifest["pair_support_filter"]
|
| 1027 |
-
)
|
| 1028 |
-
source_checkpoint = manifest.get("source_checkpoint")
|
| 1029 |
-
validate_decision_support_coupling(
|
| 1030 |
-
policy,
|
| 1031 |
-
support_filter,
|
| 1032 |
-
source_checkpoint=(
|
| 1033 |
-
source_checkpoint if isinstance(source_checkpoint, Mapping) else None
|
| 1034 |
-
),
|
| 1035 |
-
)
|
| 1036 |
-
model_vocab_size = _manifest_integer(
|
| 1037 |
-
manifest, "model_vocab_size", positive=True
|
| 1038 |
-
)
|
| 1039 |
-
manifest_pair_count = _manifest_integer(
|
| 1040 |
-
manifest, "active_pair_count", positive=False
|
| 1041 |
-
)
|
| 1042 |
-
manifest_active_rows = _manifest_integer(
|
| 1043 |
-
manifest, "active_row_count", positive=False
|
| 1044 |
-
)
|
| 1045 |
-
manifest_table_rows = _manifest_integer(
|
| 1046 |
-
manifest, "table_rows", positive=True
|
| 1047 |
-
)
|
| 1048 |
-
if not isinstance(manifest.get("compiled_identity"), dict):
|
| 1049 |
-
raise ValueError("compiled artifact manifest identity is invalid")
|
| 1050 |
-
_validate_source_checkpoint(manifest.get("source_checkpoint"))
|
| 1051 |
-
files = manifest.get("files")
|
| 1052 |
-
payload_names = _ARTIFACT_FILES - {"manifest.json"}
|
| 1053 |
-
if not isinstance(files, dict) or set(files) != payload_names:
|
| 1054 |
-
raise ValueError("compiled artifact file records are invalid")
|
| 1055 |
-
for name in sorted(payload_names):
|
| 1056 |
-
record = files[name]
|
| 1057 |
-
path = output_path / name
|
| 1058 |
-
if (
|
| 1059 |
-
not isinstance(record, dict)
|
| 1060 |
-
or set(record) != {"sha256", "bytes"}
|
| 1061 |
-
or not isinstance(record.get("sha256"), str)
|
| 1062 |
-
or len(record["sha256"]) != 64
|
| 1063 |
-
or any(character not in "0123456789abcdef" for character in record["sha256"])
|
| 1064 |
-
or isinstance(record.get("bytes"), bool)
|
| 1065 |
-
or not isinstance(record.get("bytes"), int)
|
| 1066 |
-
or record["bytes"] < 0
|
| 1067 |
-
or record.get("bytes") != path.stat().st_size
|
| 1068 |
-
or record.get("sha256") != _sha256_file(path)
|
| 1069 |
-
):
|
| 1070 |
-
raise ValueError(f"compiled artifact hash mismatch: {name}")
|
| 1071 |
-
tensors = load_file(output_path / "model.safetensors", device="cpu")
|
| 1072 |
-
if set(tensors) != {"embedding.weight"}:
|
| 1073 |
-
raise ValueError("compiled model tensor inventory is invalid")
|
| 1074 |
-
with np.load(output_path / "routing.npz", allow_pickle=False) as routing:
|
| 1075 |
-
if set(routing.files) != _ROUTING_ARRAYS:
|
| 1076 |
-
raise ValueError("compiled routing array inventory is invalid")
|
| 1077 |
-
compiled = CompiledLatentLexicon(
|
| 1078 |
-
embedding=tensors["embedding.weight"],
|
| 1079 |
-
model_vocab_size=model_vocab_size,
|
| 1080 |
-
split_active=torch.from_numpy(routing["split_active"].copy()),
|
| 1081 |
-
parent_left=torch.from_numpy(routing["parent_left"].copy()),
|
| 1082 |
-
parent_right=torch.from_numpy(routing["parent_right"].copy()),
|
| 1083 |
-
pair_keys=routing["pair_keys"].copy(),
|
| 1084 |
-
pair_scores=torch.from_numpy(routing["pair_scores"].copy()),
|
| 1085 |
-
)
|
| 1086 |
-
_validate_compiled(compiled)
|
| 1087 |
-
pair_count = int(compiled.pair_keys.size)
|
| 1088 |
-
if (
|
| 1089 |
-
manifest_pair_count != pair_count
|
| 1090 |
-
or manifest_active_rows != compiled.embedding.shape[0]
|
| 1091 |
-
or manifest_table_rows != compiled.embedding.shape[0]
|
| 1092 |
-
or manifest.get("compiled_identity") != compiled.identity()
|
| 1093 |
-
):
|
| 1094 |
-
raise ValueError("compiled artifact semantic identity differs")
|
| 1095 |
-
return compiled
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/latent_lexicon.py
DELETED
|
@@ -1,1097 +0,0 @@
|
|
| 1 |
-
"""Exact hard-segmentation distributions for the contrastive latent lexicon."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
import math
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Sequence
|
| 9 |
-
|
| 10 |
-
import numpy as np
|
| 11 |
-
import torch
|
| 12 |
-
import torch.nn.functional as F
|
| 13 |
-
from torch import nn
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
@dataclass(frozen=True)
|
| 17 |
-
class MatchingSample:
|
| 18 |
-
selected: torch.Tensor
|
| 19 |
-
log_probability: torch.Tensor
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
@dataclass(frozen=True)
|
| 23 |
-
class SplitSample:
|
| 24 |
-
frontier: tuple[int, ...]
|
| 25 |
-
split_paths: tuple[tuple[int, ...], ...]
|
| 26 |
-
stopped_paths: tuple[tuple[int, ...], ...]
|
| 27 |
-
log_probability: torch.Tensor
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
@dataclass(frozen=True)
|
| 31 |
-
class CounterfactualInfoNCE:
|
| 32 |
-
primary_loss: torch.Tensor
|
| 33 |
-
alternate_anchor_losses: torch.Tensor
|
| 34 |
-
alternate_positive_losses: torch.Tensor
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
@dataclass(frozen=True)
|
| 38 |
-
class LexiconEmission:
|
| 39 |
-
kind: str
|
| 40 |
-
token_ids: tuple[int, ...]
|
| 41 |
-
|
| 42 |
-
def __post_init__(self) -> None:
|
| 43 |
-
if self.kind not in ("original", "pair"):
|
| 44 |
-
raise ValueError("lexicon emission kind must be original or pair")
|
| 45 |
-
expected = 1 if self.kind == "original" else 2
|
| 46 |
-
if len(self.token_ids) != expected:
|
| 47 |
-
raise ValueError(f"{self.kind} emission must contain {expected} token ids")
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
@dataclass(frozen=True)
|
| 51 |
-
class LatentSentenceSample:
|
| 52 |
-
matching: MatchingSample
|
| 53 |
-
splits: tuple[SplitSample | None, ...]
|
| 54 |
-
emissions: tuple[LexiconEmission, ...]
|
| 55 |
-
numerator: torch.Tensor
|
| 56 |
-
learned_log_probability: torch.Tensor
|
| 57 |
-
explorer_log_probability: torch.Tensor
|
| 58 |
-
mixture_log_probability: torch.Tensor
|
| 59 |
-
sampled_from_explorer: bool
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
class CompositionalPairResidual(nn.Module):
|
| 63 |
-
"""Zero-neutral signed multi-table residual for exact adjacent pairs."""
|
| 64 |
-
|
| 65 |
-
def __init__(
|
| 66 |
-
self, rows_per_component: int, dimension: int, components: int = 4
|
| 67 |
-
) -> None:
|
| 68 |
-
super().__init__()
|
| 69 |
-
if rows_per_component <= 0 or dimension <= 0 or components <= 0:
|
| 70 |
-
raise ValueError("residual table sizes and component count must be positive")
|
| 71 |
-
self.rows_per_component = int(rows_per_component)
|
| 72 |
-
self.dimension = int(dimension)
|
| 73 |
-
self.components = int(components)
|
| 74 |
-
self.tables = nn.ModuleList([
|
| 75 |
-
nn.Embedding(self.rows_per_component, self.dimension)
|
| 76 |
-
for _ in range(self.components)
|
| 77 |
-
])
|
| 78 |
-
for table in self.tables:
|
| 79 |
-
nn.init.zeros_(table.weight)
|
| 80 |
-
|
| 81 |
-
def forward(
|
| 82 |
-
self,
|
| 83 |
-
left: torch.Tensor,
|
| 84 |
-
right: torch.Tensor,
|
| 85 |
-
buckets: torch.Tensor,
|
| 86 |
-
signs: torch.Tensor,
|
| 87 |
-
) -> torch.Tensor:
|
| 88 |
-
if left.ndim != 2 or right.shape != left.shape:
|
| 89 |
-
raise ValueError("left and right pair vectors must have the same shape")
|
| 90 |
-
if left.shape[1] != self.dimension:
|
| 91 |
-
raise ValueError("pair vector dimension differs from residual tables")
|
| 92 |
-
expected_code_shape = (left.shape[0], self.components)
|
| 93 |
-
if buckets.shape != expected_code_shape:
|
| 94 |
-
raise ValueError("residual bucket shape differs from pair/component shape")
|
| 95 |
-
if signs.shape != expected_code_shape:
|
| 96 |
-
raise ValueError("residual sign shape differs from pair/component shape")
|
| 97 |
-
if buckets.dtype != torch.long:
|
| 98 |
-
raise ValueError("residual buckets must be int64")
|
| 99 |
-
if buckets.device != left.device or signs.device != left.device:
|
| 100 |
-
raise ValueError("residual codes and pair vectors must share a device")
|
| 101 |
-
if signs.dtype != left.dtype:
|
| 102 |
-
raise ValueError("residual signs and pair vectors must share a dtype")
|
| 103 |
-
if left.dtype != self.tables[0].weight.dtype or left.device != self.tables[0].weight.device:
|
| 104 |
-
raise ValueError("pair vectors and residual tables must share dtype and device")
|
| 105 |
-
if buckets.numel() and (
|
| 106 |
-
int(buckets.min().item()) < 0
|
| 107 |
-
or int(buckets.max().item()) >= self.rows_per_component
|
| 108 |
-
):
|
| 109 |
-
raise ValueError("residual bucket is outside its component table")
|
| 110 |
-
if signs.numel() and not bool(((signs == 1) | (signs == -1)).all().item()):
|
| 111 |
-
raise ValueError("residual signs must be exactly -1 or +1")
|
| 112 |
-
components = [
|
| 113 |
-
table(buckets[:, component]) * signs[:, component].unsqueeze(1)
|
| 114 |
-
for component, table in enumerate(self.tables)
|
| 115 |
-
]
|
| 116 |
-
if components:
|
| 117 |
-
residual = torch.stack(components).sum(dim=0) / math.sqrt(self.components)
|
| 118 |
-
else:
|
| 119 |
-
residual = torch.zeros_like(left)
|
| 120 |
-
return left + right + residual
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
@dataclass(frozen=True)
|
| 124 |
-
class PairGateIndex:
|
| 125 |
-
keys: np.memmap
|
| 126 |
-
path: Path
|
| 127 |
-
|
| 128 |
-
@classmethod
|
| 129 |
-
def from_npy(cls, path: str | Path) -> "PairGateIndex":
|
| 130 |
-
resolved = Path(path).resolve()
|
| 131 |
-
keys = np.load(resolved, mmap_mode="r", allow_pickle=False)
|
| 132 |
-
if not isinstance(keys, np.memmap):
|
| 133 |
-
raise ValueError("pair keys must be a memory-mapped NumPy array")
|
| 134 |
-
if keys.ndim != 1 or keys.size == 0:
|
| 135 |
-
raise ValueError("pair keys must be a positive-length rank-1 array")
|
| 136 |
-
if keys.dtype != np.uint64:
|
| 137 |
-
raise ValueError("pair keys must have uint64 dtype")
|
| 138 |
-
if not bool((keys[1:] > keys[:-1]).all()):
|
| 139 |
-
raise ValueError("pair keys must be strictly increasing")
|
| 140 |
-
return cls(keys=keys, path=resolved)
|
| 141 |
-
|
| 142 |
-
def lookup(
|
| 143 |
-
self, left_ids: torch.Tensor, right_ids: torch.Tensor
|
| 144 |
-
) -> torch.Tensor:
|
| 145 |
-
if left_ids.ndim != 1 or right_ids.shape != left_ids.shape:
|
| 146 |
-
raise ValueError("pair lookup ids must be matching rank-1 tensors")
|
| 147 |
-
left = left_ids.detach().to(device="cpu", dtype=torch.long).numpy()
|
| 148 |
-
right = right_ids.detach().to(device="cpu", dtype=torch.long).numpy()
|
| 149 |
-
if left.size and (
|
| 150 |
-
int(left.min()) < 0 or int(right.min()) < 0
|
| 151 |
-
or int(left.max()) >= (1 << 32) or int(right.max()) >= (1 << 32)
|
| 152 |
-
):
|
| 153 |
-
raise ValueError("pair lookup ids must fit unsigned 32-bit fields")
|
| 154 |
-
packed = (
|
| 155 |
-
left.astype(np.uint64) << np.uint64(32)
|
| 156 |
-
) | right.astype(np.uint64)
|
| 157 |
-
positions = np.searchsorted(self.keys, packed)
|
| 158 |
-
rows = np.full(packed.shape, -1, dtype=np.int64)
|
| 159 |
-
in_bounds = positions < self.keys.size
|
| 160 |
-
bounded_positions = positions[in_bounds]
|
| 161 |
-
exact = self.keys[bounded_positions] == packed[in_bounds]
|
| 162 |
-
rows[np.flatnonzero(in_bounds)[exact]] = bounded_positions[exact].astype(np.int64)
|
| 163 |
-
return torch.from_numpy(rows)
|
| 164 |
-
|
| 165 |
-
def lookup_sequence(self, root_ids: Sequence[int]) -> torch.Tensor:
|
| 166 |
-
roots = torch.as_tensor(tuple(root_ids), dtype=torch.long)
|
| 167 |
-
if roots.numel() < 2:
|
| 168 |
-
return torch.empty(0, dtype=torch.long)
|
| 169 |
-
return self.lookup(roots[:-1], roots[1:])
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
def bounded_potential(raw: torch.Tensor, bound: float) -> torch.Tensor:
|
| 173 |
-
if not math.isfinite(bound) or bound <= 0:
|
| 174 |
-
raise ValueError("potential bound must be finite and positive")
|
| 175 |
-
return float(bound) * torch.tanh(raw / float(bound))
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
class SparsePairGate(nn.Module):
|
| 179 |
-
def __init__(
|
| 180 |
-
self,
|
| 181 |
-
index: PairGateIndex,
|
| 182 |
-
initial_potential: float = -2.0,
|
| 183 |
-
potential_bound: float = 4.0,
|
| 184 |
-
) -> None:
|
| 185 |
-
super().__init__()
|
| 186 |
-
if not math.isfinite(initial_potential) or abs(initial_potential) >= potential_bound:
|
| 187 |
-
raise ValueError("initial potential must lie strictly inside its bound")
|
| 188 |
-
self.index = index
|
| 189 |
-
self.potential_bound = float(potential_bound)
|
| 190 |
-
self.gates = nn.Embedding(len(index.keys), 1, sparse=True)
|
| 191 |
-
nn.init.zeros_(self.gates.weight)
|
| 192 |
-
initial_raw = potential_bound * math.atanh(initial_potential / potential_bound)
|
| 193 |
-
self.register_buffer("base_raw", torch.tensor(float(initial_raw)))
|
| 194 |
-
|
| 195 |
-
def edge_scores(
|
| 196 |
-
self,
|
| 197 |
-
root_ids: Sequence[int],
|
| 198 |
-
extra_raw: torch.Tensor | None = None,
|
| 199 |
-
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 200 |
-
roots = tuple(int(token) for token in root_ids)
|
| 201 |
-
if not roots:
|
| 202 |
-
raise ValueError("pair gate needs at least one root token")
|
| 203 |
-
rows_cpu = self.index.lookup_sequence(roots)
|
| 204 |
-
device = self.gates.weight.device
|
| 205 |
-
dtype = self.gates.weight.dtype
|
| 206 |
-
rows = rows_cpu.to(device=device)
|
| 207 |
-
mask = rows >= 0
|
| 208 |
-
base_score = bounded_potential(self.base_raw.to(dtype=dtype), self.potential_bound)
|
| 209 |
-
scores = base_score.expand(rows.shape).clone()
|
| 210 |
-
if extra_raw is not None:
|
| 211 |
-
if extra_raw.shape != rows.shape:
|
| 212 |
-
raise ValueError("extra pair raw scores differ from edge shape")
|
| 213 |
-
if extra_raw.device != device or extra_raw.dtype != dtype:
|
| 214 |
-
raise ValueError("extra pair raw scores differ from gate dtype or device")
|
| 215 |
-
positions = torch.nonzero(mask, as_tuple=False).flatten()
|
| 216 |
-
if positions.numel():
|
| 217 |
-
exact_rows = rows.index_select(0, positions)
|
| 218 |
-
raw = self.base_raw.to(dtype=dtype) + self.gates(exact_rows).squeeze(1)
|
| 219 |
-
if extra_raw is not None:
|
| 220 |
-
raw = raw + extra_raw.index_select(0, positions)
|
| 221 |
-
exact_scores = bounded_potential(raw, self.potential_bound)
|
| 222 |
-
scores = scores.index_copy(0, positions, exact_scores)
|
| 223 |
-
return scores, mask, rows
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
class SignedPairHasher:
|
| 227 |
-
_MASK = (1 << 64) - 1
|
| 228 |
-
_GAMMA = 0x9E3779B97F4A7C15
|
| 229 |
-
_MIX_1 = 0xBF58476D1CE4E5B9
|
| 230 |
-
_MIX_2 = 0x94D049BB133111EB
|
| 231 |
-
|
| 232 |
-
def __init__(
|
| 233 |
-
self,
|
| 234 |
-
model_vocab_size: int,
|
| 235 |
-
rows_per_component: int,
|
| 236 |
-
components: int = 4,
|
| 237 |
-
training_seed: int = 0,
|
| 238 |
-
) -> None:
|
| 239 |
-
if not 0 < model_vocab_size < (1 << 32):
|
| 240 |
-
raise ValueError("model vocabulary size must fit unsigned 32-bit ids")
|
| 241 |
-
if rows_per_component <= 0 or rows_per_component & (rows_per_component - 1):
|
| 242 |
-
raise ValueError("rows per hash component must be a power of two")
|
| 243 |
-
if components <= 0:
|
| 244 |
-
raise ValueError("hash component count must be positive")
|
| 245 |
-
self.model_vocab_size = int(model_vocab_size)
|
| 246 |
-
self.rows_per_component = int(rows_per_component)
|
| 247 |
-
self.components = int(components)
|
| 248 |
-
self.training_seed = int(training_seed)
|
| 249 |
-
self.bucket_bits = self.rows_per_component.bit_length() - 1
|
| 250 |
-
self.component_seeds = tuple(
|
| 251 |
-
self.splitmix64(self.training_seed + component)
|
| 252 |
-
for component in range(self.components)
|
| 253 |
-
)
|
| 254 |
-
|
| 255 |
-
@classmethod
|
| 256 |
-
def splitmix64(cls, value: int) -> int:
|
| 257 |
-
z = (int(value) + cls._GAMMA) & cls._MASK
|
| 258 |
-
z = ((z ^ (z >> 30)) * cls._MIX_1) & cls._MASK
|
| 259 |
-
z = ((z ^ (z >> 27)) * cls._MIX_2) & cls._MASK
|
| 260 |
-
return (z ^ (z >> 31)) & cls._MASK
|
| 261 |
-
|
| 262 |
-
@classmethod
|
| 263 |
-
def _splitmix64_array(cls, values: np.ndarray) -> np.ndarray:
|
| 264 |
-
with np.errstate(over="ignore"):
|
| 265 |
-
z = values + np.uint64(cls._GAMMA)
|
| 266 |
-
z = (z ^ (z >> np.uint64(30))) * np.uint64(cls._MIX_1)
|
| 267 |
-
z = (z ^ (z >> np.uint64(27))) * np.uint64(cls._MIX_2)
|
| 268 |
-
return z ^ (z >> np.uint64(31))
|
| 269 |
-
|
| 270 |
-
def codes(
|
| 271 |
-
self,
|
| 272 |
-
left_ids: torch.Tensor,
|
| 273 |
-
right_ids: torch.Tensor,
|
| 274 |
-
*,
|
| 275 |
-
dtype: torch.dtype,
|
| 276 |
-
device: torch.device | str | None = None,
|
| 277 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 278 |
-
if left_ids.ndim != 1 or right_ids.shape != left_ids.shape:
|
| 279 |
-
raise ValueError("hash ids must be matching rank-1 tensors")
|
| 280 |
-
left = left_ids.detach().to(device="cpu", dtype=torch.long).numpy()
|
| 281 |
-
right = right_ids.detach().to(device="cpu", dtype=torch.long).numpy()
|
| 282 |
-
if left.size and (
|
| 283 |
-
int(left.min()) < 0 or int(right.min()) < 0
|
| 284 |
-
or int(left.max()) >= self.model_vocab_size
|
| 285 |
-
or int(right.max()) >= self.model_vocab_size
|
| 286 |
-
):
|
| 287 |
-
raise ValueError("hash token id is outside the model vocabulary")
|
| 288 |
-
q = (
|
| 289 |
-
left.astype(np.uint64) * np.uint64(self.model_vocab_size)
|
| 290 |
-
+ right.astype(np.uint64)
|
| 291 |
-
)
|
| 292 |
-
buckets = []
|
| 293 |
-
signs = []
|
| 294 |
-
bucket_mask = np.uint64(self.rows_per_component - 1)
|
| 295 |
-
for seed in self.component_seeds:
|
| 296 |
-
z = self._splitmix64_array(q ^ np.uint64(seed))
|
| 297 |
-
buckets.append((z & bucket_mask).astype(np.int64))
|
| 298 |
-
sign_bit = (z >> np.uint64(self.bucket_bits)) & np.uint64(1)
|
| 299 |
-
signs.append(np.where(sign_bit == 0, 1.0, -1.0))
|
| 300 |
-
target_device = left_ids.device if device is None else torch.device(device)
|
| 301 |
-
bucket_array = np.stack(buckets, axis=1) if buckets else np.empty((len(q), 0))
|
| 302 |
-
sign_array = np.stack(signs, axis=1) if signs else np.empty((len(q), 0))
|
| 303 |
-
return (
|
| 304 |
-
torch.as_tensor(bucket_array, dtype=torch.long, device=target_device),
|
| 305 |
-
torch.as_tensor(sign_array, dtype=dtype, device=target_device),
|
| 306 |
-
)
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
def _validate_edges(
|
| 310 |
-
edge_scores: torch.Tensor, edge_mask: torch.Tensor | None
|
| 311 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 312 |
-
if edge_scores.ndim != 1:
|
| 313 |
-
raise ValueError("edge score shape must be one-dimensional")
|
| 314 |
-
if not edge_scores.is_floating_point():
|
| 315 |
-
raise ValueError("edge scores must be floating point")
|
| 316 |
-
if edge_mask is None:
|
| 317 |
-
mask = torch.ones_like(edge_scores, dtype=torch.bool)
|
| 318 |
-
else:
|
| 319 |
-
if edge_mask.shape != edge_scores.shape:
|
| 320 |
-
raise ValueError("edge mask shape differs from edge scores")
|
| 321 |
-
if edge_mask.dtype != torch.bool:
|
| 322 |
-
raise ValueError("edge mask must be boolean")
|
| 323 |
-
mask = edge_mask.to(device=edge_scores.device)
|
| 324 |
-
return edge_scores, mask
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
def _matching_prefix(
|
| 328 |
-
edge_scores: torch.Tensor, edge_mask: torch.Tensor
|
| 329 |
-
) -> list[torch.Tensor]:
|
| 330 |
-
prefix = [edge_scores.new_zeros(())]
|
| 331 |
-
if edge_scores.numel() == 0:
|
| 332 |
-
return prefix
|
| 333 |
-
prefix.append(edge_scores.new_zeros(()))
|
| 334 |
-
for vertices in range(2, edge_scores.numel() + 2):
|
| 335 |
-
edge = vertices - 2
|
| 336 |
-
separate = prefix[vertices - 1]
|
| 337 |
-
if bool(edge_mask[edge].item()):
|
| 338 |
-
merged = prefix[vertices - 2] + edge_scores[edge]
|
| 339 |
-
prefix.append(torch.logaddexp(separate, merged))
|
| 340 |
-
else:
|
| 341 |
-
prefix.append(separate)
|
| 342 |
-
return prefix
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
def matching_log_partition(
|
| 346 |
-
edge_scores: torch.Tensor, edge_mask: torch.Tensor | None = None
|
| 347 |
-
) -> torch.Tensor:
|
| 348 |
-
scores, mask = _validate_edges(edge_scores, edge_mask)
|
| 349 |
-
return _matching_prefix(scores, mask)[-1]
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
def matching_edge_marginals(
|
| 353 |
-
edge_scores: torch.Tensor, edge_mask: torch.Tensor | None = None
|
| 354 |
-
) -> torch.Tensor:
|
| 355 |
-
scores, mask = _validate_edges(edge_scores, edge_mask)
|
| 356 |
-
count = scores.numel()
|
| 357 |
-
if count == 0:
|
| 358 |
-
return torch.empty_like(scores)
|
| 359 |
-
prefix = _matching_prefix(scores, mask)
|
| 360 |
-
suffix: list[torch.Tensor] = [scores.new_zeros(()) for _ in range(count + 2)]
|
| 361 |
-
for vertex in range(count - 1, -1, -1):
|
| 362 |
-
separate = suffix[vertex + 1]
|
| 363 |
-
if bool(mask[vertex].item()):
|
| 364 |
-
merged = scores[vertex] + suffix[vertex + 2]
|
| 365 |
-
suffix[vertex] = torch.logaddexp(separate, merged)
|
| 366 |
-
else:
|
| 367 |
-
suffix[vertex] = separate
|
| 368 |
-
log_z = prefix[-1]
|
| 369 |
-
values = torch.stack([
|
| 370 |
-
torch.exp(prefix[edge] + scores[edge] + suffix[edge + 2] - log_z)
|
| 371 |
-
if bool(mask[edge].item()) else scores.new_zeros(())
|
| 372 |
-
for edge in range(count)
|
| 373 |
-
])
|
| 374 |
-
return values
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
def _validate_selection(
|
| 378 |
-
selected: torch.Tensor, edge_scores: torch.Tensor, edge_mask: torch.Tensor
|
| 379 |
-
) -> torch.Tensor:
|
| 380 |
-
if selected.shape != edge_scores.shape:
|
| 381 |
-
raise ValueError("matching selection shape differs from edge scores")
|
| 382 |
-
chosen = selected.to(device=edge_scores.device, dtype=torch.bool)
|
| 383 |
-
if bool((chosen & ~edge_mask).any().item()):
|
| 384 |
-
raise ValueError("matching selected a masked edge")
|
| 385 |
-
if chosen.numel() > 1 and bool((chosen[:-1] & chosen[1:]).any().item()):
|
| 386 |
-
raise ValueError("matching edges overlap")
|
| 387 |
-
return chosen
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
def matching_log_probability(
|
| 391 |
-
edge_scores: torch.Tensor,
|
| 392 |
-
selected: torch.Tensor,
|
| 393 |
-
edge_mask: torch.Tensor | None = None,
|
| 394 |
-
) -> torch.Tensor:
|
| 395 |
-
scores, mask = _validate_edges(edge_scores, edge_mask)
|
| 396 |
-
chosen = _validate_selection(selected, scores, mask)
|
| 397 |
-
return scores[chosen].sum() - _matching_prefix(scores, mask)[-1]
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
def sample_matching(
|
| 401 |
-
edge_scores: torch.Tensor,
|
| 402 |
-
edge_mask: torch.Tensor | None = None,
|
| 403 |
-
*,
|
| 404 |
-
generator: torch.Generator | None = None,
|
| 405 |
-
) -> MatchingSample:
|
| 406 |
-
scores, mask = _validate_edges(edge_scores, edge_mask)
|
| 407 |
-
prefix = _matching_prefix(scores, mask)
|
| 408 |
-
selected = torch.zeros_like(scores, dtype=torch.bool)
|
| 409 |
-
vertices = scores.numel() + 1
|
| 410 |
-
while vertices >= 2:
|
| 411 |
-
edge = vertices - 2
|
| 412 |
-
if bool(mask[edge].item()):
|
| 413 |
-
probability = torch.exp(
|
| 414 |
-
prefix[vertices - 2] + scores[edge] - prefix[vertices]
|
| 415 |
-
).detach()
|
| 416 |
-
draw = torch.rand((), generator=generator, device=scores.device)
|
| 417 |
-
if bool((draw < probability).item()):
|
| 418 |
-
selected[edge] = True
|
| 419 |
-
vertices -= 2
|
| 420 |
-
continue
|
| 421 |
-
vertices -= 1
|
| 422 |
-
log_probability = scores[selected].sum() - prefix[-1]
|
| 423 |
-
return MatchingSample(selected=selected, log_probability=log_probability)
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
def map_matching(
|
| 427 |
-
edge_scores: torch.Tensor, edge_mask: torch.Tensor | None = None
|
| 428 |
-
) -> torch.Tensor:
|
| 429 |
-
scores, mask = _validate_edges(edge_scores, edge_mask)
|
| 430 |
-
count = scores.numel()
|
| 431 |
-
selected = torch.zeros_like(scores, dtype=torch.bool)
|
| 432 |
-
if count == 0:
|
| 433 |
-
return selected
|
| 434 |
-
best = [scores.new_zeros(()), scores.new_zeros(())]
|
| 435 |
-
take_edge = [False, False]
|
| 436 |
-
for vertices in range(2, count + 2):
|
| 437 |
-
edge = vertices - 2
|
| 438 |
-
separate = best[vertices - 1]
|
| 439 |
-
merged = best[vertices - 2] + scores[edge]
|
| 440 |
-
take = bool(mask[edge].item()) and bool((merged > separate).item())
|
| 441 |
-
best.append(merged if take else separate)
|
| 442 |
-
take_edge.append(take)
|
| 443 |
-
vertices = count + 1
|
| 444 |
-
while vertices >= 2:
|
| 445 |
-
if take_edge[vertices]:
|
| 446 |
-
selected[vertices - 2] = True
|
| 447 |
-
vertices -= 2
|
| 448 |
-
else:
|
| 449 |
-
vertices -= 1
|
| 450 |
-
return selected
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
def _tree_size(left: Sequence[int] | torch.Tensor, right: Sequence[int] | torch.Tensor) -> int:
|
| 454 |
-
if len(left) != len(right):
|
| 455 |
-
raise ValueError("left and right child arrays differ in length")
|
| 456 |
-
return len(left)
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
def _child_pair(
|
| 460 |
-
token: int,
|
| 461 |
-
left: Sequence[int] | torch.Tensor,
|
| 462 |
-
right: Sequence[int] | torch.Tensor,
|
| 463 |
-
) -> tuple[int, int] | None:
|
| 464 |
-
size = _tree_size(left, right)
|
| 465 |
-
if not 0 <= token < size:
|
| 466 |
-
raise ValueError(f"token {token} is outside child arrays")
|
| 467 |
-
left_id, right_id = int(left[token]), int(right[token])
|
| 468 |
-
if (left_id < 0) != (right_id < 0):
|
| 469 |
-
raise ValueError(f"token {token} must have both children or neither")
|
| 470 |
-
if left_id < 0:
|
| 471 |
-
return None
|
| 472 |
-
if not 0 <= left_id < size or not 0 <= right_id < size:
|
| 473 |
-
raise ValueError(f"token {token} has a child outside child arrays")
|
| 474 |
-
return left_id, right_id
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
def _validate_split_scores(
|
| 478 |
-
split_scores: torch.Tensor,
|
| 479 |
-
left: Sequence[int] | torch.Tensor,
|
| 480 |
-
right: Sequence[int] | torch.Tensor,
|
| 481 |
-
) -> None:
|
| 482 |
-
if split_scores.ndim != 1 or split_scores.shape[0] != _tree_size(left, right):
|
| 483 |
-
raise ValueError("split score shape differs from child arrays")
|
| 484 |
-
if not split_scores.is_floating_point():
|
| 485 |
-
raise ValueError("split scores must be floating point")
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
def decomposable_occurrence_paths(
|
| 489 |
-
root: int,
|
| 490 |
-
left: Sequence[int] | torch.Tensor,
|
| 491 |
-
right: Sequence[int] | torch.Tensor,
|
| 492 |
-
) -> tuple[tuple[int, ...], ...]:
|
| 493 |
-
paths: list[tuple[int, ...]] = []
|
| 494 |
-
|
| 495 |
-
def visit(token: int, path: tuple[int, ...], ancestors: frozenset[int]) -> None:
|
| 496 |
-
if token in ancestors:
|
| 497 |
-
raise ValueError(f"cycle in split tree at token {token}")
|
| 498 |
-
children = _child_pair(token, left, right)
|
| 499 |
-
if children is None:
|
| 500 |
-
return
|
| 501 |
-
paths.append(path)
|
| 502 |
-
nested = ancestors | {token}
|
| 503 |
-
visit(children[0], path + (0,), nested)
|
| 504 |
-
visit(children[1], path + (1,), nested)
|
| 505 |
-
|
| 506 |
-
visit(int(root), (), frozenset())
|
| 507 |
-
return tuple(paths)
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
def sample_split_frontier(
|
| 511 |
-
root: int,
|
| 512 |
-
left: Sequence[int] | torch.Tensor,
|
| 513 |
-
right: Sequence[int] | torch.Tensor,
|
| 514 |
-
split_scores: torch.Tensor,
|
| 515 |
-
*,
|
| 516 |
-
generator: torch.Generator | None = None,
|
| 517 |
-
) -> SplitSample:
|
| 518 |
-
_validate_split_scores(split_scores, left, right)
|
| 519 |
-
frontier: list[int] = []
|
| 520 |
-
split_paths: list[tuple[int, ...]] = []
|
| 521 |
-
stopped_paths: list[tuple[int, ...]] = []
|
| 522 |
-
terms: list[torch.Tensor] = []
|
| 523 |
-
|
| 524 |
-
def visit(token: int, path: tuple[int, ...], ancestors: frozenset[int]) -> None:
|
| 525 |
-
if token in ancestors:
|
| 526 |
-
raise ValueError(f"cycle in split tree at token {token}")
|
| 527 |
-
children = _child_pair(token, left, right)
|
| 528 |
-
if children is None:
|
| 529 |
-
frontier.append(token)
|
| 530 |
-
return
|
| 531 |
-
score = split_scores[token]
|
| 532 |
-
draw = torch.rand((), generator=generator, device=split_scores.device)
|
| 533 |
-
if bool((draw < score.sigmoid().detach()).item()):
|
| 534 |
-
split_paths.append(path)
|
| 535 |
-
terms.append(F.logsigmoid(score))
|
| 536 |
-
nested = ancestors | {token}
|
| 537 |
-
visit(children[0], path + (0,), nested)
|
| 538 |
-
visit(children[1], path + (1,), nested)
|
| 539 |
-
else:
|
| 540 |
-
stopped_paths.append(path)
|
| 541 |
-
terms.append(F.logsigmoid(-score))
|
| 542 |
-
frontier.append(token)
|
| 543 |
-
|
| 544 |
-
visit(int(root), (), frozenset())
|
| 545 |
-
log_probability = sum(terms, split_scores.new_zeros(()))
|
| 546 |
-
return SplitSample(
|
| 547 |
-
tuple(frontier), tuple(split_paths), tuple(stopped_paths), log_probability
|
| 548 |
-
)
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
def split_log_probability(
|
| 552 |
-
root: int,
|
| 553 |
-
left: Sequence[int] | torch.Tensor,
|
| 554 |
-
right: Sequence[int] | torch.Tensor,
|
| 555 |
-
split_scores: torch.Tensor,
|
| 556 |
-
sample: SplitSample,
|
| 557 |
-
) -> torch.Tensor:
|
| 558 |
-
_validate_split_scores(split_scores, left, right)
|
| 559 |
-
if len(set(sample.split_paths)) != len(sample.split_paths):
|
| 560 |
-
raise ValueError("split sample repeats a split path")
|
| 561 |
-
if len(set(sample.stopped_paths)) != len(sample.stopped_paths):
|
| 562 |
-
raise ValueError("split sample repeats a stopped path")
|
| 563 |
-
split_paths, stopped_paths = set(sample.split_paths), set(sample.stopped_paths)
|
| 564 |
-
if split_paths & stopped_paths:
|
| 565 |
-
raise ValueError("split sample both splits and stops one path")
|
| 566 |
-
used_split: set[tuple[int, ...]] = set()
|
| 567 |
-
used_stopped: set[tuple[int, ...]] = set()
|
| 568 |
-
frontier: list[int] = []
|
| 569 |
-
terms: list[torch.Tensor] = []
|
| 570 |
-
|
| 571 |
-
def visit(token: int, path: tuple[int, ...], ancestors: frozenset[int]) -> None:
|
| 572 |
-
if token in ancestors:
|
| 573 |
-
raise ValueError(f"cycle in split tree at token {token}")
|
| 574 |
-
children = _child_pair(token, left, right)
|
| 575 |
-
if children is None:
|
| 576 |
-
frontier.append(token)
|
| 577 |
-
return
|
| 578 |
-
if path in split_paths:
|
| 579 |
-
used_split.add(path)
|
| 580 |
-
terms.append(F.logsigmoid(split_scores[token]))
|
| 581 |
-
nested = ancestors | {token}
|
| 582 |
-
visit(children[0], path + (0,), nested)
|
| 583 |
-
visit(children[1], path + (1,), nested)
|
| 584 |
-
elif path in stopped_paths:
|
| 585 |
-
used_stopped.add(path)
|
| 586 |
-
terms.append(F.logsigmoid(-split_scores[token]))
|
| 587 |
-
frontier.append(token)
|
| 588 |
-
else:
|
| 589 |
-
raise ValueError(f"split sample has no decision for path {path}")
|
| 590 |
-
|
| 591 |
-
visit(int(root), (), frozenset())
|
| 592 |
-
if used_split != split_paths or used_stopped != stopped_paths:
|
| 593 |
-
raise ValueError("split sample contains unreachable decisions")
|
| 594 |
-
if tuple(frontier) != sample.frontier:
|
| 595 |
-
raise ValueError("split sample frontier does not match its decisions")
|
| 596 |
-
return sum(terms, split_scores.new_zeros(()))
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
def map_split_frontier(
|
| 600 |
-
root: int,
|
| 601 |
-
left: Sequence[int] | torch.Tensor,
|
| 602 |
-
right: Sequence[int] | torch.Tensor,
|
| 603 |
-
split_scores: torch.Tensor,
|
| 604 |
-
) -> SplitSample:
|
| 605 |
-
_validate_split_scores(split_scores, left, right)
|
| 606 |
-
|
| 607 |
-
def visit(
|
| 608 |
-
token: int, path: tuple[int, ...], ancestors: frozenset[int]
|
| 609 |
-
) -> tuple[list[int], list[tuple[int, ...]], list[tuple[int, ...]], torch.Tensor]:
|
| 610 |
-
if token in ancestors:
|
| 611 |
-
raise ValueError(f"cycle in split tree at token {token}")
|
| 612 |
-
children = _child_pair(token, left, right)
|
| 613 |
-
if children is None:
|
| 614 |
-
return [token], [], [], split_scores.new_zeros(())
|
| 615 |
-
nested = ancestors | {token}
|
| 616 |
-
left_result = visit(children[0], path + (0,), nested)
|
| 617 |
-
right_result = visit(children[1], path + (1,), nested)
|
| 618 |
-
score = split_scores[token]
|
| 619 |
-
stop_score = F.logsigmoid(-score)
|
| 620 |
-
split_score = F.logsigmoid(score) + left_result[3] + right_result[3]
|
| 621 |
-
if bool((split_score > stop_score).item()):
|
| 622 |
-
return (
|
| 623 |
-
left_result[0] + right_result[0],
|
| 624 |
-
[path] + left_result[1] + right_result[1],
|
| 625 |
-
left_result[2] + right_result[2],
|
| 626 |
-
split_score,
|
| 627 |
-
)
|
| 628 |
-
return [token], [], [path], stop_score
|
| 629 |
-
|
| 630 |
-
frontier, split_paths, stopped_paths, log_probability = visit(
|
| 631 |
-
int(root), (), frozenset()
|
| 632 |
-
)
|
| 633 |
-
return SplitSample(
|
| 634 |
-
tuple(frontier), tuple(split_paths), tuple(stopped_paths), log_probability
|
| 635 |
-
)
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
def single_chain_split(
|
| 639 |
-
root: int,
|
| 640 |
-
left: Sequence[int] | torch.Tensor,
|
| 641 |
-
right: Sequence[int] | torch.Tensor,
|
| 642 |
-
target_path: tuple[int, ...],
|
| 643 |
-
) -> SplitSample:
|
| 644 |
-
valid_targets = decomposable_occurrence_paths(root, left, right)
|
| 645 |
-
if target_path not in valid_targets:
|
| 646 |
-
raise ValueError(f"target path {target_path} is not decomposable")
|
| 647 |
-
frontier: list[int] = []
|
| 648 |
-
split_paths: list[tuple[int, ...]] = []
|
| 649 |
-
stopped_paths: list[tuple[int, ...]] = []
|
| 650 |
-
|
| 651 |
-
def emit_stopped(token: int, path: tuple[int, ...]) -> None:
|
| 652 |
-
if _child_pair(token, left, right) is not None:
|
| 653 |
-
stopped_paths.append(path)
|
| 654 |
-
frontier.append(token)
|
| 655 |
-
|
| 656 |
-
def visit(token: int, path: tuple[int, ...], ancestors: frozenset[int]) -> None:
|
| 657 |
-
if token in ancestors:
|
| 658 |
-
raise ValueError(f"cycle in split tree at token {token}")
|
| 659 |
-
children = _child_pair(token, left, right)
|
| 660 |
-
if children is None or target_path[:len(path)] != path:
|
| 661 |
-
raise ValueError(f"target path {target_path} is not reachable")
|
| 662 |
-
split_paths.append(path)
|
| 663 |
-
if path == target_path:
|
| 664 |
-
emit_stopped(children[0], path + (0,))
|
| 665 |
-
emit_stopped(children[1], path + (1,))
|
| 666 |
-
return
|
| 667 |
-
direction = target_path[len(path)]
|
| 668 |
-
nested = ancestors | {token}
|
| 669 |
-
for child_index, child in enumerate(children):
|
| 670 |
-
child_path = path + (child_index,)
|
| 671 |
-
if child_index == direction:
|
| 672 |
-
visit(child, child_path, nested)
|
| 673 |
-
else:
|
| 674 |
-
emit_stopped(child, child_path)
|
| 675 |
-
|
| 676 |
-
visit(int(root), (), frozenset())
|
| 677 |
-
return SplitSample(
|
| 678 |
-
tuple(frontier), tuple(split_paths), tuple(stopped_paths), torch.tensor(0.0)
|
| 679 |
-
)
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
def _split_signature(sample: SplitSample) -> tuple[object, ...]:
|
| 683 |
-
return sample.frontier, sample.split_paths, sample.stopped_paths
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
def sample_single_chain_explorer(
|
| 687 |
-
root: int,
|
| 688 |
-
left: Sequence[int] | torch.Tensor,
|
| 689 |
-
right: Sequence[int] | torch.Tensor,
|
| 690 |
-
*,
|
| 691 |
-
generator: torch.Generator | None = None,
|
| 692 |
-
) -> SplitSample:
|
| 693 |
-
targets = decomposable_occurrence_paths(root, left, right)
|
| 694 |
-
if not targets:
|
| 695 |
-
return SplitSample((int(root),), (), (), torch.tensor(0.0, dtype=torch.float64))
|
| 696 |
-
random_device = torch.device(generator.device) if generator is not None else torch.device("cpu")
|
| 697 |
-
if bool((torch.rand((), generator=generator, device=random_device) < 0.5).item()):
|
| 698 |
-
return SplitSample(
|
| 699 |
-
(int(root),), (), ((),), torch.tensor(math.log(0.5), dtype=torch.float64)
|
| 700 |
-
)
|
| 701 |
-
index = int(torch.randint(
|
| 702 |
-
len(targets), (), generator=generator, device=random_device
|
| 703 |
-
).item())
|
| 704 |
-
sample = single_chain_split(root, left, right, targets[index])
|
| 705 |
-
return SplitSample(
|
| 706 |
-
sample.frontier,
|
| 707 |
-
sample.split_paths,
|
| 708 |
-
sample.stopped_paths,
|
| 709 |
-
torch.tensor(math.log(0.5 / len(targets)), dtype=torch.float64),
|
| 710 |
-
)
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
def single_chain_explorer_log_probability(
|
| 714 |
-
root: int,
|
| 715 |
-
left: Sequence[int] | torch.Tensor,
|
| 716 |
-
right: Sequence[int] | torch.Tensor,
|
| 717 |
-
sample: SplitSample,
|
| 718 |
-
) -> torch.Tensor:
|
| 719 |
-
targets = decomposable_occurrence_paths(root, left, right)
|
| 720 |
-
signature = _split_signature(sample)
|
| 721 |
-
if not targets:
|
| 722 |
-
value = 0.0 if signature == ((int(root),), (), ()) else -torch.inf
|
| 723 |
-
return torch.tensor(value, dtype=torch.float64)
|
| 724 |
-
untouched = ((int(root),), (), ((),))
|
| 725 |
-
if signature == untouched:
|
| 726 |
-
return torch.tensor(math.log(0.5), dtype=torch.float64)
|
| 727 |
-
target_log_probability = math.log(0.5 / len(targets))
|
| 728 |
-
for target in targets:
|
| 729 |
-
if signature == _split_signature(single_chain_split(root, left, right, target)):
|
| 730 |
-
return torch.tensor(target_log_probability, dtype=torch.float64)
|
| 731 |
-
return torch.tensor(-torch.inf, dtype=torch.float64)
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
def mixture_log_probability(
|
| 735 |
-
learned_log_probability: torch.Tensor,
|
| 736 |
-
explorer_log_probability: torch.Tensor,
|
| 737 |
-
epsilon: float,
|
| 738 |
-
) -> torch.Tensor:
|
| 739 |
-
if not 0.0 <= float(epsilon) <= 1.0:
|
| 740 |
-
raise ValueError("mixture epsilon must be between zero and one")
|
| 741 |
-
if learned_log_probability.ndim != 0 or explorer_log_probability.ndim != 0:
|
| 742 |
-
raise ValueError("mixture log probabilities must be scalars")
|
| 743 |
-
if epsilon == 0.0:
|
| 744 |
-
return learned_log_probability
|
| 745 |
-
if epsilon == 1.0:
|
| 746 |
-
return explorer_log_probability
|
| 747 |
-
explorer = explorer_log_probability.to(
|
| 748 |
-
device=learned_log_probability.device, dtype=learned_log_probability.dtype
|
| 749 |
-
)
|
| 750 |
-
learned_weight = learned_log_probability.new_tensor(math.log1p(-epsilon))
|
| 751 |
-
explorer_weight = learned_log_probability.new_tensor(math.log(epsilon))
|
| 752 |
-
return torch.logaddexp(
|
| 753 |
-
learned_weight + learned_log_probability,
|
| 754 |
-
explorer_weight + explorer,
|
| 755 |
-
)
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
def paired_rloo_surrogate(
|
| 759 |
-
primary_log_probabilities: torch.Tensor,
|
| 760 |
-
alternate_log_probabilities: torch.Tensor,
|
| 761 |
-
primary_loss: torch.Tensor,
|
| 762 |
-
alternate_losses: torch.Tensor,
|
| 763 |
-
) -> torch.Tensor:
|
| 764 |
-
if primary_log_probabilities.ndim != 1:
|
| 765 |
-
raise ValueError("paired log probabilities must be one-dimensional")
|
| 766 |
-
if primary_log_probabilities.shape != alternate_log_probabilities.shape:
|
| 767 |
-
raise ValueError("paired log-probability shapes differ")
|
| 768 |
-
if primary_loss.ndim != 0:
|
| 769 |
-
raise ValueError("primary loss must be scalar")
|
| 770 |
-
if alternate_losses.shape != primary_log_probabilities.shape:
|
| 771 |
-
raise ValueError("counterfactual loss shape differs from text paths")
|
| 772 |
-
delta = (primary_loss - alternate_losses).detach()
|
| 773 |
-
return 0.5 * (
|
| 774 |
-
delta * (primary_log_probabilities - alternate_log_probabilities)
|
| 775 |
-
).sum()
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
def exclusive_logsumexp(scores: torch.Tensor, dim: int) -> torch.Tensor:
|
| 779 |
-
if scores.ndim != 2:
|
| 780 |
-
raise ValueError("exclusive logsumexp expects a rank-2 tensor")
|
| 781 |
-
if dim not in (0, 1):
|
| 782 |
-
raise ValueError("exclusive logsumexp dim must be zero or one")
|
| 783 |
-
if not scores.is_floating_point():
|
| 784 |
-
raise ValueError("exclusive logsumexp expects floating scores")
|
| 785 |
-
if dim == 1:
|
| 786 |
-
return exclusive_logsumexp(scores.t(), dim=0).t()
|
| 787 |
-
rows, columns = scores.shape
|
| 788 |
-
negative_infinity = torch.full(
|
| 789 |
-
(columns,), -torch.inf, dtype=scores.dtype, device=scores.device
|
| 790 |
-
)
|
| 791 |
-
suffix: list[torch.Tensor] = [negative_infinity] * rows
|
| 792 |
-
running = negative_infinity
|
| 793 |
-
for row in range(rows - 1, -1, -1):
|
| 794 |
-
suffix[row] = running
|
| 795 |
-
running = torch.logaddexp(running, scores[row])
|
| 796 |
-
result: list[torch.Tensor] = []
|
| 797 |
-
running = negative_infinity
|
| 798 |
-
for row in range(rows):
|
| 799 |
-
result.append(torch.logaddexp(running, suffix[row]))
|
| 800 |
-
running = torch.logaddexp(running, scores[row])
|
| 801 |
-
if not result:
|
| 802 |
-
return torch.empty_like(scores)
|
| 803 |
-
return torch.stack(result)
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
def _validate_replacement_inputs(
|
| 807 |
-
primary_anchor: torch.Tensor,
|
| 808 |
-
primary_positive: torch.Tensor,
|
| 809 |
-
alternate_anchor: torch.Tensor,
|
| 810 |
-
alternate_positive: torch.Tensor,
|
| 811 |
-
) -> None:
|
| 812 |
-
tensors = (
|
| 813 |
-
primary_anchor, primary_positive, alternate_anchor, alternate_positive
|
| 814 |
-
)
|
| 815 |
-
if any(tensor.ndim != 2 for tensor in tensors):
|
| 816 |
-
raise ValueError("InfoNCE replacement inputs must be rank-2")
|
| 817 |
-
if any(tensor.shape != primary_anchor.shape for tensor in tensors[1:]):
|
| 818 |
-
raise ValueError("InfoNCE replacement inputs must have the same shape")
|
| 819 |
-
if primary_anchor.shape[0] <= 0 or primary_anchor.shape[1] <= 0:
|
| 820 |
-
raise ValueError("InfoNCE replacement needs a positive batch and dimension")
|
| 821 |
-
if any(not tensor.is_floating_point() for tensor in tensors):
|
| 822 |
-
raise ValueError("InfoNCE replacement inputs must be floating point")
|
| 823 |
-
if any(tensor.dtype != primary_anchor.dtype for tensor in tensors[1:]):
|
| 824 |
-
raise ValueError("InfoNCE replacement inputs must have the same dtype")
|
| 825 |
-
if any(tensor.device != primary_anchor.device for tensor in tensors[1:]):
|
| 826 |
-
raise ValueError("InfoNCE replacement inputs must be on the same device")
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
def symmetric_info_nce_replacements(
|
| 830 |
-
primary_anchor: torch.Tensor,
|
| 831 |
-
primary_positive: torch.Tensor,
|
| 832 |
-
alternate_anchor: torch.Tensor,
|
| 833 |
-
alternate_positive: torch.Tensor,
|
| 834 |
-
scale: float,
|
| 835 |
-
) -> CounterfactualInfoNCE:
|
| 836 |
-
_validate_replacement_inputs(
|
| 837 |
-
primary_anchor, primary_positive, alternate_anchor, alternate_positive
|
| 838 |
-
)
|
| 839 |
-
anchor = F.normalize(primary_anchor, dim=1)
|
| 840 |
-
positive = F.normalize(primary_positive, dim=1)
|
| 841 |
-
alternative_anchor = F.normalize(alternate_anchor, dim=1)
|
| 842 |
-
alternative_positive = F.normalize(alternate_positive, dim=1)
|
| 843 |
-
factor = float(scale)
|
| 844 |
-
scores = anchor @ positive.t() * factor
|
| 845 |
-
anchor_scores = alternative_anchor @ positive.t() * factor
|
| 846 |
-
positive_scores = anchor @ alternative_positive.t() * factor
|
| 847 |
-
batch = scores.shape[0]
|
| 848 |
-
diagonal = scores.diagonal()
|
| 849 |
-
anchor_diagonal = anchor_scores.diagonal()
|
| 850 |
-
positive_diagonal = positive_scores.diagonal()
|
| 851 |
-
row_normalizers = torch.logsumexp(scores, dim=1)
|
| 852 |
-
column_normalizers = torch.logsumexp(scores, dim=0)
|
| 853 |
-
primary_loss = 0.5 * (
|
| 854 |
-
(-diagonal + row_normalizers).mean()
|
| 855 |
-
+ (-diagonal + column_normalizers).mean()
|
| 856 |
-
)
|
| 857 |
-
|
| 858 |
-
anchor_row_change = (
|
| 859 |
-
-anchor_diagonal + torch.logsumexp(anchor_scores, dim=1)
|
| 860 |
-
+ diagonal - row_normalizers
|
| 861 |
-
)
|
| 862 |
-
replaced_column_normalizers = torch.logaddexp(
|
| 863 |
-
exclusive_logsumexp(scores, dim=0), anchor_scores
|
| 864 |
-
)
|
| 865 |
-
anchor_column_change = (
|
| 866 |
-
(replaced_column_normalizers - column_normalizers.unsqueeze(0)).sum(dim=1)
|
| 867 |
-
- (anchor_diagonal - diagonal)
|
| 868 |
-
)
|
| 869 |
-
alternate_anchor_losses = primary_loss + 0.5 / batch * (
|
| 870 |
-
anchor_row_change + anchor_column_change
|
| 871 |
-
)
|
| 872 |
-
|
| 873 |
-
positive_column_change = (
|
| 874 |
-
-positive_diagonal + torch.logsumexp(positive_scores, dim=0)
|
| 875 |
-
+ diagonal - column_normalizers
|
| 876 |
-
)
|
| 877 |
-
replaced_row_normalizers = torch.logaddexp(
|
| 878 |
-
exclusive_logsumexp(scores, dim=1), positive_scores
|
| 879 |
-
)
|
| 880 |
-
positive_row_change = (
|
| 881 |
-
(replaced_row_normalizers - row_normalizers.unsqueeze(1)).sum(dim=0)
|
| 882 |
-
- (positive_diagonal - diagonal)
|
| 883 |
-
)
|
| 884 |
-
alternate_positive_losses = primary_loss + 0.5 / batch * (
|
| 885 |
-
positive_column_change + positive_row_change
|
| 886 |
-
)
|
| 887 |
-
return CounterfactualInfoNCE(
|
| 888 |
-
primary_loss, alternate_anchor_losses, alternate_positive_losses
|
| 889 |
-
)
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
def _generator_matches_device(
|
| 893 |
-
generator: torch.Generator,
|
| 894 |
-
tensor_device: torch.device,
|
| 895 |
-
) -> bool:
|
| 896 |
-
generator_device = torch.device(generator.device)
|
| 897 |
-
if generator_device.type != tensor_device.type:
|
| 898 |
-
return False
|
| 899 |
-
if generator_device.type != "cuda":
|
| 900 |
-
return generator_device == tensor_device
|
| 901 |
-
generator_index = (
|
| 902 |
-
torch.cuda.current_device()
|
| 903 |
-
if generator_device.index is None
|
| 904 |
-
else generator_device.index
|
| 905 |
-
)
|
| 906 |
-
tensor_index = (
|
| 907 |
-
torch.cuda.current_device()
|
| 908 |
-
if tensor_device.index is None
|
| 909 |
-
else tensor_device.index
|
| 910 |
-
)
|
| 911 |
-
return generator_index == tensor_index
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
def sample_latent_sentence(
|
| 915 |
-
root_ids: Sequence[int],
|
| 916 |
-
embedding_weight: torch.Tensor,
|
| 917 |
-
parent_left: Sequence[int] | torch.Tensor,
|
| 918 |
-
parent_right: Sequence[int] | torch.Tensor,
|
| 919 |
-
split_raw_scores: torch.Tensor,
|
| 920 |
-
pair_gate: SparsePairGate,
|
| 921 |
-
residual: CompositionalPairResidual,
|
| 922 |
-
hasher: SignedPairHasher,
|
| 923 |
-
*,
|
| 924 |
-
explorer_epsilon: float = 0.1,
|
| 925 |
-
explorer_merge_potential: float = -2.0,
|
| 926 |
-
potential_bound: float = 4.0,
|
| 927 |
-
generator: torch.Generator | None = None,
|
| 928 |
-
materialize_numerator: bool = True,
|
| 929 |
-
) -> LatentSentenceSample:
|
| 930 |
-
"""Sample one exact path, optionally deferring its vector materialization.
|
| 931 |
-
|
| 932 |
-
When ``materialize_numerator`` is false, routing and log probabilities are
|
| 933 |
-
complete but ``numerator`` is an empty placeholder that must be replaced
|
| 934 |
-
before constructing a contrastive loss.
|
| 935 |
-
"""
|
| 936 |
-
roots = tuple(int(token) for token in root_ids)
|
| 937 |
-
if not roots:
|
| 938 |
-
raise ValueError("latent sentence needs at least one root token")
|
| 939 |
-
if embedding_weight.ndim != 2 or not embedding_weight.is_floating_point():
|
| 940 |
-
raise ValueError("embedding weight must be a floating rank-2 tensor")
|
| 941 |
-
model_vocab_size = hasher.model_vocab_size
|
| 942 |
-
if len(parent_left) != model_vocab_size or len(parent_right) != model_vocab_size:
|
| 943 |
-
raise ValueError("canonical child arrays differ from the model vocabulary")
|
| 944 |
-
if split_raw_scores.shape != (model_vocab_size,):
|
| 945 |
-
raise ValueError("split raw score shape differs from the model vocabulary")
|
| 946 |
-
if split_raw_scores.dtype != embedding_weight.dtype or split_raw_scores.device != embedding_weight.device:
|
| 947 |
-
raise ValueError("split scores and embedding weight must share dtype and device")
|
| 948 |
-
if embedding_weight.shape[0] < model_vocab_size:
|
| 949 |
-
raise ValueError("embedding table is smaller than the model vocabulary")
|
| 950 |
-
if min(roots) < 0 or max(roots) >= model_vocab_size:
|
| 951 |
-
raise ValueError("root token id is outside the model vocabulary")
|
| 952 |
-
if residual.dimension != embedding_weight.shape[1]:
|
| 953 |
-
raise ValueError("residual and embedding dimensions differ")
|
| 954 |
-
if residual.components != hasher.components:
|
| 955 |
-
raise ValueError("hasher and residual component counts differ")
|
| 956 |
-
if residual.rows_per_component != hasher.rows_per_component:
|
| 957 |
-
raise ValueError("hasher and residual row counts differ")
|
| 958 |
-
if residual.tables[0].weight.dtype != embedding_weight.dtype or residual.tables[0].weight.device != embedding_weight.device:
|
| 959 |
-
raise ValueError("residual and embedding weights must share dtype and device")
|
| 960 |
-
if pair_gate.gates.weight.dtype != embedding_weight.dtype or pair_gate.gates.weight.device != embedding_weight.device:
|
| 961 |
-
raise ValueError("pair gate and embedding weight must share dtype and device")
|
| 962 |
-
if pair_gate.potential_bound != float(potential_bound):
|
| 963 |
-
raise ValueError("pair gate and sentence potential bounds differ")
|
| 964 |
-
if not 0.0 <= explorer_epsilon <= 1.0:
|
| 965 |
-
raise ValueError("explorer epsilon must be between zero and one")
|
| 966 |
-
if not math.isfinite(explorer_merge_potential):
|
| 967 |
-
raise ValueError("explorer merge potential must be finite")
|
| 968 |
-
if generator is not None and not _generator_matches_device(
|
| 969 |
-
generator, embedding_weight.device
|
| 970 |
-
):
|
| 971 |
-
raise ValueError("generator device differs from sentence tensors")
|
| 972 |
-
|
| 973 |
-
learned_edge_scores, edge_mask, _ = pair_gate.edge_scores(roots)
|
| 974 |
-
explorer_edge_scores = torch.full_like(
|
| 975 |
-
learned_edge_scores, float(explorer_merge_potential)
|
| 976 |
-
)
|
| 977 |
-
learned_split_scores = bounded_potential(split_raw_scores, potential_bound)
|
| 978 |
-
if explorer_epsilon == 0.0:
|
| 979 |
-
sampled_from_explorer = False
|
| 980 |
-
elif explorer_epsilon == 1.0:
|
| 981 |
-
sampled_from_explorer = True
|
| 982 |
-
else:
|
| 983 |
-
draw = torch.rand(
|
| 984 |
-
(), generator=generator, device=embedding_weight.device
|
| 985 |
-
)
|
| 986 |
-
sampled_from_explorer = bool((draw < explorer_epsilon).item())
|
| 987 |
-
matching = sample_matching(
|
| 988 |
-
explorer_edge_scores if sampled_from_explorer else learned_edge_scores,
|
| 989 |
-
edge_mask,
|
| 990 |
-
generator=generator,
|
| 991 |
-
)
|
| 992 |
-
selected = matching.selected.detach().to(device="cpu").tolist()
|
| 993 |
-
splits: list[SplitSample | None] = [None] * len(roots)
|
| 994 |
-
position = 0
|
| 995 |
-
while position < len(roots):
|
| 996 |
-
if position < len(selected) and selected[position]:
|
| 997 |
-
position += 2
|
| 998 |
-
continue
|
| 999 |
-
root = roots[position]
|
| 1000 |
-
if sampled_from_explorer:
|
| 1001 |
-
split = sample_single_chain_explorer(
|
| 1002 |
-
root, parent_left, parent_right, generator=generator
|
| 1003 |
-
)
|
| 1004 |
-
else:
|
| 1005 |
-
split = sample_split_frontier(
|
| 1006 |
-
root,
|
| 1007 |
-
parent_left,
|
| 1008 |
-
parent_right,
|
| 1009 |
-
learned_split_scores,
|
| 1010 |
-
generator=generator,
|
| 1011 |
-
)
|
| 1012 |
-
splits[position] = split
|
| 1013 |
-
position += 1
|
| 1014 |
-
|
| 1015 |
-
learned_log_probability = matching_log_probability(
|
| 1016 |
-
learned_edge_scores, matching.selected, edge_mask
|
| 1017 |
-
)
|
| 1018 |
-
explorer_log_probability = matching_log_probability(
|
| 1019 |
-
explorer_edge_scores, matching.selected, edge_mask
|
| 1020 |
-
)
|
| 1021 |
-
for position, split in enumerate(splits):
|
| 1022 |
-
if split is None:
|
| 1023 |
-
continue
|
| 1024 |
-
root = roots[position]
|
| 1025 |
-
learned_log_probability = learned_log_probability + split_log_probability(
|
| 1026 |
-
root, parent_left, parent_right, learned_split_scores, split
|
| 1027 |
-
)
|
| 1028 |
-
explorer_split_log = single_chain_explorer_log_probability(
|
| 1029 |
-
root, parent_left, parent_right, split
|
| 1030 |
-
).to(device=embedding_weight.device, dtype=embedding_weight.dtype)
|
| 1031 |
-
explorer_log_probability = explorer_log_probability + explorer_split_log
|
| 1032 |
-
mixture_probability = mixture_log_probability(
|
| 1033 |
-
learned_log_probability, explorer_log_probability, explorer_epsilon
|
| 1034 |
-
)
|
| 1035 |
-
|
| 1036 |
-
matched_edges = [edge for edge, chosen in enumerate(selected) if chosen]
|
| 1037 |
-
pair_vectors: dict[int, torch.Tensor] = {}
|
| 1038 |
-
if matched_edges and materialize_numerator:
|
| 1039 |
-
left_ids = torch.tensor(
|
| 1040 |
-
[roots[edge] for edge in matched_edges],
|
| 1041 |
-
dtype=torch.long,
|
| 1042 |
-
device=embedding_weight.device,
|
| 1043 |
-
)
|
| 1044 |
-
right_ids = torch.tensor(
|
| 1045 |
-
[roots[edge + 1] for edge in matched_edges],
|
| 1046 |
-
dtype=torch.long,
|
| 1047 |
-
device=embedding_weight.device,
|
| 1048 |
-
)
|
| 1049 |
-
buckets, signs = hasher.codes(
|
| 1050 |
-
left_ids,
|
| 1051 |
-
right_ids,
|
| 1052 |
-
dtype=embedding_weight.dtype,
|
| 1053 |
-
device=embedding_weight.device,
|
| 1054 |
-
)
|
| 1055 |
-
vectors = residual(
|
| 1056 |
-
embedding_weight.index_select(0, left_ids),
|
| 1057 |
-
embedding_weight.index_select(0, right_ids),
|
| 1058 |
-
buckets,
|
| 1059 |
-
signs,
|
| 1060 |
-
)
|
| 1061 |
-
pair_vectors = {
|
| 1062 |
-
edge: vectors[index] for index, edge in enumerate(matched_edges)
|
| 1063 |
-
}
|
| 1064 |
-
|
| 1065 |
-
emitted_vectors: list[torch.Tensor] = []
|
| 1066 |
-
emissions: list[LexiconEmission] = []
|
| 1067 |
-
position = 0
|
| 1068 |
-
while position < len(roots):
|
| 1069 |
-
if position < len(selected) and selected[position]:
|
| 1070 |
-
emissions.append(LexiconEmission("pair", (roots[position], roots[position + 1])))
|
| 1071 |
-
if materialize_numerator:
|
| 1072 |
-
emitted_vectors.append(pair_vectors[position])
|
| 1073 |
-
position += 2
|
| 1074 |
-
continue
|
| 1075 |
-
split = splits[position]
|
| 1076 |
-
if split is None:
|
| 1077 |
-
raise RuntimeError("unmatched root has no sampled split frontier")
|
| 1078 |
-
for token in split.frontier:
|
| 1079 |
-
emissions.append(LexiconEmission("original", (token,)))
|
| 1080 |
-
if materialize_numerator:
|
| 1081 |
-
emitted_vectors.append(embedding_weight[token])
|
| 1082 |
-
position += 1
|
| 1083 |
-
numerator = (
|
| 1084 |
-
torch.stack(emitted_vectors).sum(dim=0)
|
| 1085 |
-
if materialize_numerator
|
| 1086 |
-
else embedding_weight.new_empty((0,))
|
| 1087 |
-
)
|
| 1088 |
-
return LatentSentenceSample(
|
| 1089 |
-
matching=matching,
|
| 1090 |
-
splits=tuple(splits),
|
| 1091 |
-
emissions=tuple(emissions),
|
| 1092 |
-
numerator=numerator,
|
| 1093 |
-
learned_log_probability=learned_log_probability,
|
| 1094 |
-
explorer_log_probability=explorer_log_probability,
|
| 1095 |
-
mixture_log_probability=mixture_probability,
|
| 1096 |
-
sampled_from_explorer=sampled_from_explorer,
|
| 1097 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/latent_profile.py
DELETED
|
@@ -1,267 +0,0 @@
|
|
| 1 |
-
"""Validated access to frozen latent-lexicon profiling batches."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import argparse
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
import hashlib
|
| 8 |
-
import json
|
| 9 |
-
from numbers import Integral
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
from typing import Mapping, Sequence
|
| 12 |
-
|
| 13 |
-
import numpy as np
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
PROFILE_BATCH_PROTOCOL = "latent-lexicon-profile-batches-v1"
|
| 17 |
-
PROFILE_AUDIT_PROTOCOL = "latent-lexicon-profile-bank-audit-v1"
|
| 18 |
-
_ARRAY_DTYPES = {
|
| 19 |
-
"anchor_ids": np.dtype(np.int32),
|
| 20 |
-
"anchor_offsets": np.dtype(np.int64),
|
| 21 |
-
"positive_ids": np.dtype(np.int32),
|
| 22 |
-
"positive_offsets": np.dtype(np.int64),
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def sha256_file(path: Path) -> str:
|
| 27 |
-
digest = hashlib.sha256()
|
| 28 |
-
with path.open("rb") as handle:
|
| 29 |
-
for block in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
| 30 |
-
digest.update(block)
|
| 31 |
-
return digest.hexdigest()
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def _mapping(value: object, name: str) -> Mapping[str, object]:
|
| 35 |
-
if not isinstance(value, Mapping):
|
| 36 |
-
raise ValueError(f"latent profile {name} must be a mapping")
|
| 37 |
-
return value
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def validate_offsets(offsets: np.ndarray, ids_length: int) -> None:
|
| 41 |
-
if (
|
| 42 |
-
offsets.ndim != 1
|
| 43 |
-
or offsets.size == 0
|
| 44 |
-
or int(offsets[0]) != 0
|
| 45 |
-
or int(offsets[-1]) != ids_length
|
| 46 |
-
or bool((offsets[1:] < offsets[:-1]).any())
|
| 47 |
-
):
|
| 48 |
-
raise ValueError("latent profile offsets are invalid")
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def decode_rows(
|
| 52 |
-
ids: np.ndarray,
|
| 53 |
-
offsets: np.ndarray,
|
| 54 |
-
start: int,
|
| 55 |
-
stop: int,
|
| 56 |
-
) -> tuple[tuple[int, ...], ...]:
|
| 57 |
-
return tuple(
|
| 58 |
-
tuple(int(token) for token in ids[
|
| 59 |
-
int(offsets[row]):int(offsets[row + 1])
|
| 60 |
-
])
|
| 61 |
-
for row in range(start, stop)
|
| 62 |
-
)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
@dataclass(frozen=True)
|
| 66 |
-
class LatentProfileBank:
|
| 67 |
-
root: Path
|
| 68 |
-
manifest: Mapping[str, object]
|
| 69 |
-
anchor_ids: np.ndarray
|
| 70 |
-
anchor_offsets: np.ndarray
|
| 71 |
-
positive_ids: np.ndarray
|
| 72 |
-
positive_offsets: np.ndarray
|
| 73 |
-
|
| 74 |
-
@classmethod
|
| 75 |
-
def from_directory(cls, path: str | Path) -> "LatentProfileBank":
|
| 76 |
-
root = Path(path).resolve()
|
| 77 |
-
manifest_value = json.loads(
|
| 78 |
-
(root / "manifest.json").read_text(encoding="utf-8")
|
| 79 |
-
)
|
| 80 |
-
if not isinstance(manifest_value, dict):
|
| 81 |
-
raise ValueError("latent profile manifest must be a JSON object")
|
| 82 |
-
manifest: Mapping[str, object] = manifest_value
|
| 83 |
-
if manifest.get("protocol") != PROFILE_BATCH_PROTOCOL:
|
| 84 |
-
raise ValueError("latent profile batch protocol mismatch")
|
| 85 |
-
if manifest.get("selection_uses_heldout_or_benchmark_data") is not False:
|
| 86 |
-
raise ValueError("latent profile batch bank is not training-only")
|
| 87 |
-
outputs = _mapping(manifest.get("outputs"), "outputs")
|
| 88 |
-
if set(outputs) != {f"{name}.npy" for name in _ARRAY_DTYPES}:
|
| 89 |
-
raise ValueError("latent profile output inventory differs")
|
| 90 |
-
|
| 91 |
-
arrays: dict[str, np.ndarray] = {}
|
| 92 |
-
for name, dtype in _ARRAY_DTYPES.items():
|
| 93 |
-
filename = f"{name}.npy"
|
| 94 |
-
array_path = root / filename
|
| 95 |
-
expected = _mapping(outputs[filename], f"output {filename}")
|
| 96 |
-
if sha256_file(array_path) != expected.get("sha256"):
|
| 97 |
-
raise ValueError(f"latent profile batch hash mismatch: {name}")
|
| 98 |
-
array = np.load(array_path, mmap_mode="r", allow_pickle=False)
|
| 99 |
-
if not isinstance(array, np.memmap):
|
| 100 |
-
raise ValueError(f"latent profile array is not memory-mapped: {name}")
|
| 101 |
-
if (
|
| 102 |
-
array.ndim != 1
|
| 103 |
-
or array.dtype != dtype
|
| 104 |
-
or expected.get("dtype") != array.dtype.str
|
| 105 |
-
or expected.get("shape") != list(array.shape)
|
| 106 |
-
):
|
| 107 |
-
raise ValueError(f"latent profile batch metadata mismatch: {name}")
|
| 108 |
-
arrays[name] = array
|
| 109 |
-
|
| 110 |
-
validate_offsets(arrays["anchor_offsets"], len(arrays["anchor_ids"]))
|
| 111 |
-
validate_offsets(arrays["positive_offsets"], len(arrays["positive_ids"]))
|
| 112 |
-
pair_count = int(manifest.get("pair_count", -1))
|
| 113 |
-
if (
|
| 114 |
-
len(arrays["anchor_offsets"]) != pair_count + 1
|
| 115 |
-
or len(arrays["positive_offsets"]) != pair_count + 1
|
| 116 |
-
):
|
| 117 |
-
raise ValueError("latent profile pair count differs from offsets")
|
| 118 |
-
tokenizer = _mapping(
|
| 119 |
-
_mapping(manifest.get("inputs"), "inputs").get("tokenizer"),
|
| 120 |
-
"tokenizer input",
|
| 121 |
-
)
|
| 122 |
-
model_vocab_size = int(tokenizer.get("model_vocab_size", 0))
|
| 123 |
-
for name in ("anchor_ids", "positive_ids"):
|
| 124 |
-
ids = arrays[name]
|
| 125 |
-
if ids.size and (
|
| 126 |
-
int(ids.min()) < 0 or int(ids.max()) >= model_vocab_size
|
| 127 |
-
):
|
| 128 |
-
raise ValueError("latent profile content id is outside model vocabulary")
|
| 129 |
-
|
| 130 |
-
records = manifest.get("records")
|
| 131 |
-
if not isinstance(records, list) or not records:
|
| 132 |
-
raise ValueError("latent profile manifest has no records")
|
| 133 |
-
expected_start = 0
|
| 134 |
-
names: set[str] = set()
|
| 135 |
-
for record in records:
|
| 136 |
-
row = _mapping(record, "record")
|
| 137 |
-
name = row.get("name")
|
| 138 |
-
start = int(row.get("pair_start", -1))
|
| 139 |
-
end = int(row.get("pair_end", -1))
|
| 140 |
-
if (
|
| 141 |
-
not isinstance(name, str)
|
| 142 |
-
or name in names
|
| 143 |
-
or start != expected_start
|
| 144 |
-
or end <= start
|
| 145 |
-
or end > pair_count
|
| 146 |
-
):
|
| 147 |
-
raise ValueError("latent profile record ranges are invalid")
|
| 148 |
-
names.add(name)
|
| 149 |
-
expected_start = end
|
| 150 |
-
if expected_start != pair_count:
|
| 151 |
-
raise ValueError("latent profile record ranges do not cover the bank")
|
| 152 |
-
return cls(root=root, manifest=manifest, **arrays)
|
| 153 |
-
|
| 154 |
-
@property
|
| 155 |
-
def record_names(self) -> tuple[str, ...]:
|
| 156 |
-
records = self.manifest["records"]
|
| 157 |
-
assert isinstance(records, list)
|
| 158 |
-
return tuple(str(record["name"]) for record in records)
|
| 159 |
-
|
| 160 |
-
def batch(
|
| 161 |
-
self,
|
| 162 |
-
record_name: str,
|
| 163 |
-
batch_size: int,
|
| 164 |
-
offset: int = 0,
|
| 165 |
-
) -> tuple[Sequence[Sequence[int]], Sequence[Sequence[int]]]:
|
| 166 |
-
records = self.manifest["records"]
|
| 167 |
-
assert isinstance(records, list)
|
| 168 |
-
record = next(
|
| 169 |
-
(row for row in records if row["name"] == record_name),
|
| 170 |
-
None,
|
| 171 |
-
)
|
| 172 |
-
if record is None:
|
| 173 |
-
raise ValueError(f"unknown latent profile record: {record_name}")
|
| 174 |
-
if (
|
| 175 |
-
not isinstance(batch_size, Integral)
|
| 176 |
-
or isinstance(batch_size, bool)
|
| 177 |
-
or not isinstance(offset, Integral)
|
| 178 |
-
or isinstance(offset, bool)
|
| 179 |
-
or batch_size <= 0
|
| 180 |
-
or offset < 0
|
| 181 |
-
):
|
| 182 |
-
raise ValueError("latent profile batch slice is outside its record")
|
| 183 |
-
start = int(record["pair_start"]) + int(offset)
|
| 184 |
-
stop = start + int(batch_size)
|
| 185 |
-
if stop > int(record["pair_end"]):
|
| 186 |
-
raise ValueError("latent profile batch slice is outside its record")
|
| 187 |
-
return (
|
| 188 |
-
decode_rows(self.anchor_ids, self.anchor_offsets, start, stop),
|
| 189 |
-
decode_rows(self.positive_ids, self.positive_offsets, start, stop),
|
| 190 |
-
)
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
def audit_bank(
|
| 194 |
-
path: str | Path,
|
| 195 |
-
*,
|
| 196 |
-
expected_records: int,
|
| 197 |
-
pairs_per_record: int,
|
| 198 |
-
) -> dict[str, object]:
|
| 199 |
-
"""Revalidate a bank and return a compact, machine-readable audit."""
|
| 200 |
-
if expected_records <= 0 or pairs_per_record <= 0:
|
| 201 |
-
raise ValueError("latent profile audit cardinalities must be positive")
|
| 202 |
-
bank = LatentProfileBank.from_directory(path)
|
| 203 |
-
records_value = bank.manifest["records"]
|
| 204 |
-
assert isinstance(records_value, list)
|
| 205 |
-
if len(records_value) != expected_records:
|
| 206 |
-
raise ValueError("latent profile audit record count differs")
|
| 207 |
-
|
| 208 |
-
records: list[dict[str, object]] = []
|
| 209 |
-
for record_value in records_value:
|
| 210 |
-
record = _mapping(record_value, "record")
|
| 211 |
-
pair_count = int(record["pair_end"]) - int(record["pair_start"])
|
| 212 |
-
if pair_count != pairs_per_record:
|
| 213 |
-
raise ValueError("latent profile audit per-record pair count differs")
|
| 214 |
-
records.append({
|
| 215 |
-
"anchor_length": record["anchor_length"],
|
| 216 |
-
"name": record["name"],
|
| 217 |
-
"pair_count": pair_count,
|
| 218 |
-
"positive_length": record["positive_length"],
|
| 219 |
-
})
|
| 220 |
-
|
| 221 |
-
outputs_value = _mapping(bank.manifest["outputs"], "outputs")
|
| 222 |
-
outputs = {
|
| 223 |
-
str(filename): {
|
| 224 |
-
"sha256": _mapping(metadata, f"output {filename}")["sha256"],
|
| 225 |
-
}
|
| 226 |
-
for filename, metadata in outputs_value.items()
|
| 227 |
-
}
|
| 228 |
-
return {
|
| 229 |
-
"protocol": PROFILE_AUDIT_PROTOCOL,
|
| 230 |
-
"bank_protocol": bank.manifest["protocol"],
|
| 231 |
-
"manifest_sha256": sha256_file(bank.root / "manifest.json"),
|
| 232 |
-
"selection_uses_heldout_or_benchmark_data": bank.manifest[
|
| 233 |
-
"selection_uses_heldout_or_benchmark_data"
|
| 234 |
-
],
|
| 235 |
-
"pair_count": int(bank.manifest["pair_count"]),
|
| 236 |
-
"record_count": len(records),
|
| 237 |
-
"pairs_per_record": pairs_per_record,
|
| 238 |
-
"outputs": outputs,
|
| 239 |
-
"records": records,
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
def main() -> None:
|
| 244 |
-
parser = argparse.ArgumentParser(description=__doc__)
|
| 245 |
-
parser.add_argument("--bank", type=Path, required=True)
|
| 246 |
-
parser.add_argument("--output", type=Path, required=True)
|
| 247 |
-
parser.add_argument("--expected-records", type=int, required=True)
|
| 248 |
-
parser.add_argument("--pairs-per-record", type=int, required=True)
|
| 249 |
-
args = parser.parse_args()
|
| 250 |
-
audit = audit_bank(
|
| 251 |
-
args.bank,
|
| 252 |
-
expected_records=args.expected_records,
|
| 253 |
-
pairs_per_record=args.pairs_per_record,
|
| 254 |
-
)
|
| 255 |
-
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 256 |
-
with args.output.open("x", encoding="utf-8") as handle:
|
| 257 |
-
json.dump(audit, handle, indent=2, sort_keys=True)
|
| 258 |
-
handle.write("\n")
|
| 259 |
-
print(json.dumps({
|
| 260 |
-
"audit_protocol": audit["protocol"],
|
| 261 |
-
"pair_count": audit["pair_count"],
|
| 262 |
-
"record_count": audit["record_count"],
|
| 263 |
-
}, sort_keys=True))
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
if __name__ == "__main__":
|
| 267 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/latent_profiling.py
DELETED
|
@@ -1,975 +0,0 @@
|
|
| 1 |
-
"""Training-side gradient and runtime probes for the latent lexicon."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
import hashlib
|
| 7 |
-
import json
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
import time
|
| 10 |
-
from typing import Callable, Mapping
|
| 11 |
-
from typing import Sequence
|
| 12 |
-
|
| 13 |
-
import numpy as np
|
| 14 |
-
import torch
|
| 15 |
-
import torch.nn.functional as F
|
| 16 |
-
from safetensors import safe_open
|
| 17 |
-
|
| 18 |
-
from starse.latent_lexicon import PairGateIndex
|
| 19 |
-
from starse.latent_profile import sha256_file
|
| 20 |
-
from starse.latent_training import (
|
| 21 |
-
LatentLexiconModel,
|
| 22 |
-
LatentOptimizers,
|
| 23 |
-
PairedLatentBatch,
|
| 24 |
-
backward_deterministic_loss,
|
| 25 |
-
backward_latent_loss,
|
| 26 |
-
build_paired_latent_batch,
|
| 27 |
-
sample_paired_latent_samples,
|
| 28 |
-
sample_paired_latent_batch,
|
| 29 |
-
validate_exact_gate_gradient,
|
| 30 |
-
validate_finite_gradients,
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
@dataclass(frozen=True)
|
| 35 |
-
class ArchitectureGradient:
|
| 36 |
-
split: torch.Tensor
|
| 37 |
-
gate_rows: torch.Tensor
|
| 38 |
-
gate: torch.Tensor
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
@dataclass(frozen=True)
|
| 42 |
-
class MatchedArchitectureGradients:
|
| 43 |
-
paired: ArchitectureGradient
|
| 44 |
-
global_control: ArchitectureGradient
|
| 45 |
-
primary_loss: torch.Tensor
|
| 46 |
-
all_alternate_loss: torch.Tensor
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
@dataclass(frozen=True)
|
| 50 |
-
class MatchedGradientDraws:
|
| 51 |
-
gate_rows: torch.Tensor
|
| 52 |
-
paired_split: torch.Tensor
|
| 53 |
-
paired_gate: torch.Tensor
|
| 54 |
-
global_split: torch.Tensor
|
| 55 |
-
global_gate: torch.Tensor
|
| 56 |
-
primary_losses: torch.Tensor
|
| 57 |
-
all_alternate_losses: torch.Tensor
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
@dataclass(frozen=True)
|
| 61 |
-
class TimedLatentStep:
|
| 62 |
-
batch: PairedLatentBatch
|
| 63 |
-
phase_seconds: Mapping[str, float]
|
| 64 |
-
phase_cuda_seconds: Mapping[str, float]
|
| 65 |
-
total_seconds: float
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
@dataclass(frozen=True)
|
| 69 |
-
class TimedStaticStep:
|
| 70 |
-
loss: torch.Tensor
|
| 71 |
-
phase_seconds: Mapping[str, float]
|
| 72 |
-
phase_cuda_seconds: Mapping[str, float]
|
| 73 |
-
total_seconds: float
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
@dataclass(frozen=True)
|
| 77 |
-
class TimedBackward:
|
| 78 |
-
wall_seconds: float
|
| 79 |
-
cuda_seconds: float | None
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
@dataclass(frozen=True)
|
| 83 |
-
class ValidatedProfileModelInputs:
|
| 84 |
-
inventory_manifest: Path
|
| 85 |
-
initializer_model: Path
|
| 86 |
-
tokenizer: Path
|
| 87 |
-
pair_index: PairGateIndex
|
| 88 |
-
parent_left: torch.Tensor
|
| 89 |
-
parent_right: torch.Tensor
|
| 90 |
-
model_vocab_size: int
|
| 91 |
-
dimension: int
|
| 92 |
-
identities: Mapping[str, object]
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def timed_latent_training_step(
|
| 96 |
-
model: LatentLexiconModel,
|
| 97 |
-
optimizers: LatentOptimizers,
|
| 98 |
-
anchors: Sequence[Sequence[int]],
|
| 99 |
-
positives: Sequence[Sequence[int]],
|
| 100 |
-
generator: torch.Generator,
|
| 101 |
-
*,
|
| 102 |
-
scale: float,
|
| 103 |
-
explorer_epsilon: float,
|
| 104 |
-
synchronize: Callable[[], None] | None = None,
|
| 105 |
-
clock: Callable[[], float] = time.perf_counter,
|
| 106 |
-
record_cuda_events: bool = False,
|
| 107 |
-
) -> TimedLatentStep:
|
| 108 |
-
"""Apply an exact production update with synchronized phase boundaries."""
|
| 109 |
-
sync = synchronize if synchronize is not None else lambda: None
|
| 110 |
-
if record_cuda_events and model.embedding.weight.device.type != "cuda":
|
| 111 |
-
raise ValueError("CUDA events require a CUDA latent model")
|
| 112 |
-
phases: dict[str, float] = {}
|
| 113 |
-
cuda_phases: dict[str, float] = {}
|
| 114 |
-
|
| 115 |
-
def start_phase() -> tuple[float, torch.cuda.Event | None]:
|
| 116 |
-
event = torch.cuda.Event(enable_timing=True) if record_cuda_events else None
|
| 117 |
-
if event is not None:
|
| 118 |
-
event.record()
|
| 119 |
-
return clock(), event
|
| 120 |
-
|
| 121 |
-
def finish_phase(
|
| 122 |
-
name: str, started: float, start_event: torch.cuda.Event | None
|
| 123 |
-
) -> None:
|
| 124 |
-
end_event = (
|
| 125 |
-
torch.cuda.Event(enable_timing=True) if start_event is not None else None
|
| 126 |
-
)
|
| 127 |
-
if end_event is not None:
|
| 128 |
-
end_event.record()
|
| 129 |
-
sync()
|
| 130 |
-
phases[name] = clock() - started
|
| 131 |
-
if start_event is not None and end_event is not None:
|
| 132 |
-
cuda_phases[name] = start_event.elapsed_time(end_event) / 1000.0
|
| 133 |
-
|
| 134 |
-
sync()
|
| 135 |
-
total_start = clock()
|
| 136 |
-
|
| 137 |
-
start, start_event = start_phase()
|
| 138 |
-
optimizers.zero_grad()
|
| 139 |
-
samples = sample_paired_latent_samples(
|
| 140 |
-
model,
|
| 141 |
-
anchors,
|
| 142 |
-
positives,
|
| 143 |
-
generator,
|
| 144 |
-
explorer_epsilon=explorer_epsilon,
|
| 145 |
-
)
|
| 146 |
-
finish_phase("preparation_forward", start, start_event)
|
| 147 |
-
|
| 148 |
-
start, start_event = start_phase()
|
| 149 |
-
batch = build_paired_latent_batch(samples, scale=scale)
|
| 150 |
-
finish_phase("loss", start, start_event)
|
| 151 |
-
|
| 152 |
-
start, start_event = start_phase()
|
| 153 |
-
backward_latent_loss(batch)
|
| 154 |
-
finish_phase("backward_autograd", start, start_event)
|
| 155 |
-
|
| 156 |
-
start, start_event = start_phase()
|
| 157 |
-
validate_exact_gate_gradient(
|
| 158 |
-
model,
|
| 159 |
-
anchors,
|
| 160 |
-
positives,
|
| 161 |
-
explorer_epsilon=explorer_epsilon,
|
| 162 |
-
)
|
| 163 |
-
finish_phase("exact_gate_validation", start, start_event)
|
| 164 |
-
|
| 165 |
-
start, start_event = start_phase()
|
| 166 |
-
validate_finite_gradients(model)
|
| 167 |
-
finish_phase("finite_gradient_validation", start, start_event)
|
| 168 |
-
|
| 169 |
-
start, start_event = start_phase()
|
| 170 |
-
optimizers.dense.step()
|
| 171 |
-
finish_phase("adamw", start, start_event)
|
| 172 |
-
|
| 173 |
-
start, start_event = start_phase()
|
| 174 |
-
optimizers.sparse.step()
|
| 175 |
-
finish_phase("adagrad", start, start_event)
|
| 176 |
-
total_seconds = clock() - total_start
|
| 177 |
-
return TimedLatentStep(
|
| 178 |
-
batch=batch,
|
| 179 |
-
phase_seconds=phases,
|
| 180 |
-
phase_cuda_seconds=cuda_phases,
|
| 181 |
-
total_seconds=total_seconds,
|
| 182 |
-
)
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
def timed_loss_backward(
|
| 186 |
-
loss: torch.Tensor,
|
| 187 |
-
*,
|
| 188 |
-
synchronize: Callable[[], None] | None = None,
|
| 189 |
-
clock: Callable[[], float] = time.perf_counter,
|
| 190 |
-
record_cuda_event: bool = False,
|
| 191 |
-
) -> TimedBackward:
|
| 192 |
-
"""Time one scalar loss backward without optimizer or validation work."""
|
| 193 |
-
if loss.ndim != 0 or not loss.is_floating_point():
|
| 194 |
-
raise ValueError("timed backward loss must be a floating scalar")
|
| 195 |
-
if not bool(torch.isfinite(loss).item()):
|
| 196 |
-
raise FloatingPointError("timed backward loss is nonfinite")
|
| 197 |
-
if record_cuda_event and loss.device.type != "cuda":
|
| 198 |
-
raise ValueError("CUDA backward event requires a CUDA loss")
|
| 199 |
-
sync = synchronize if synchronize is not None else lambda: None
|
| 200 |
-
start_event = torch.cuda.Event(enable_timing=True) if record_cuda_event else None
|
| 201 |
-
end_event = torch.cuda.Event(enable_timing=True) if record_cuda_event else None
|
| 202 |
-
sync()
|
| 203 |
-
if start_event is not None:
|
| 204 |
-
start_event.record()
|
| 205 |
-
started = clock()
|
| 206 |
-
backward_deterministic_loss(loss)
|
| 207 |
-
if end_event is not None:
|
| 208 |
-
end_event.record()
|
| 209 |
-
sync()
|
| 210 |
-
return TimedBackward(
|
| 211 |
-
wall_seconds=clock() - started,
|
| 212 |
-
cuda_seconds=(
|
| 213 |
-
None
|
| 214 |
-
if start_event is None or end_event is None
|
| 215 |
-
else start_event.elapsed_time(end_event) / 1000.0
|
| 216 |
-
),
|
| 217 |
-
)
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
def static_sentence_numerators(
|
| 221 |
-
embedding_weight: torch.Tensor,
|
| 222 |
-
texts: Sequence[Sequence[int]],
|
| 223 |
-
) -> torch.Tensor:
|
| 224 |
-
"""Vectorize original-row additive sentence numerators."""
|
| 225 |
-
if (
|
| 226 |
-
embedding_weight.ndim != 2
|
| 227 |
-
or not embedding_weight.is_floating_point()
|
| 228 |
-
or embedding_weight.shape[0] <= 0
|
| 229 |
-
or embedding_weight.shape[1] <= 0
|
| 230 |
-
):
|
| 231 |
-
raise ValueError("static embedding weight must be a floating matrix")
|
| 232 |
-
if not texts or any(len(text) == 0 for text in texts):
|
| 233 |
-
raise ValueError("static timing texts must be nonempty")
|
| 234 |
-
flat_ids = [int(token) for text in texts for token in text]
|
| 235 |
-
if min(flat_ids) < 0 or max(flat_ids) >= embedding_weight.shape[0]:
|
| 236 |
-
raise ValueError("static timing token is outside the vocabulary")
|
| 237 |
-
lengths = [len(text) for text in texts]
|
| 238 |
-
offsets = [0]
|
| 239 |
-
for length in lengths[:-1]:
|
| 240 |
-
offsets.append(offsets[-1] + length)
|
| 241 |
-
tokens = torch.tensor(
|
| 242 |
-
flat_ids, dtype=torch.long, device=embedding_weight.device
|
| 243 |
-
)
|
| 244 |
-
starts = torch.tensor(
|
| 245 |
-
offsets, dtype=torch.long, device=embedding_weight.device
|
| 246 |
-
)
|
| 247 |
-
return F.embedding_bag(
|
| 248 |
-
tokens,
|
| 249 |
-
embedding_weight,
|
| 250 |
-
starts,
|
| 251 |
-
mode="sum",
|
| 252 |
-
include_last_offset=False,
|
| 253 |
-
)
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
def timed_static_training_step(
|
| 257 |
-
embedding: torch.nn.Embedding,
|
| 258 |
-
optimizer: torch.optim.AdamW,
|
| 259 |
-
anchors: Sequence[Sequence[int]],
|
| 260 |
-
positives: Sequence[Sequence[int]],
|
| 261 |
-
*,
|
| 262 |
-
scale: float,
|
| 263 |
-
synchronize: Callable[[], None] | None = None,
|
| 264 |
-
clock: Callable[[], float] = time.perf_counter,
|
| 265 |
-
record_cuda_events: bool = False,
|
| 266 |
-
) -> TimedStaticStep:
|
| 267 |
-
"""Apply the vectorized static additive control with phase timings."""
|
| 268 |
-
if len(anchors) == 0 or len(anchors) != len(positives):
|
| 269 |
-
raise ValueError("static timing batches must have equal positive size")
|
| 270 |
-
sync = synchronize if synchronize is not None else lambda: None
|
| 271 |
-
if record_cuda_events and embedding.weight.device.type != "cuda":
|
| 272 |
-
raise ValueError("CUDA events require a CUDA static model")
|
| 273 |
-
phases: dict[str, float] = {}
|
| 274 |
-
cuda_phases: dict[str, float] = {}
|
| 275 |
-
|
| 276 |
-
def start_phase() -> tuple[float, torch.cuda.Event | None]:
|
| 277 |
-
event = torch.cuda.Event(enable_timing=True) if record_cuda_events else None
|
| 278 |
-
if event is not None:
|
| 279 |
-
event.record()
|
| 280 |
-
return clock(), event
|
| 281 |
-
|
| 282 |
-
def finish_phase(
|
| 283 |
-
name: str, started: float, start_event: torch.cuda.Event | None
|
| 284 |
-
) -> None:
|
| 285 |
-
end_event = (
|
| 286 |
-
torch.cuda.Event(enable_timing=True) if start_event is not None else None
|
| 287 |
-
)
|
| 288 |
-
if end_event is not None:
|
| 289 |
-
end_event.record()
|
| 290 |
-
sync()
|
| 291 |
-
phases[name] = clock() - started
|
| 292 |
-
if start_event is not None and end_event is not None:
|
| 293 |
-
cuda_phases[name] = start_event.elapsed_time(end_event) / 1000.0
|
| 294 |
-
|
| 295 |
-
sync()
|
| 296 |
-
total_start = clock()
|
| 297 |
-
|
| 298 |
-
start, start_event = start_phase()
|
| 299 |
-
optimizer.zero_grad(set_to_none=True)
|
| 300 |
-
anchor_vectors = static_sentence_numerators(embedding.weight, anchors)
|
| 301 |
-
positive_vectors = static_sentence_numerators(embedding.weight, positives)
|
| 302 |
-
finish_phase("preparation_forward", start, start_event)
|
| 303 |
-
|
| 304 |
-
start, start_event = start_phase()
|
| 305 |
-
loss = symmetric_info_nce_loss(anchor_vectors, positive_vectors, scale)
|
| 306 |
-
if not bool(torch.isfinite(loss).item()):
|
| 307 |
-
raise FloatingPointError("static timing loss is nonfinite")
|
| 308 |
-
finish_phase("loss", start, start_event)
|
| 309 |
-
|
| 310 |
-
start, start_event = start_phase()
|
| 311 |
-
loss.backward()
|
| 312 |
-
gradient = embedding.weight.grad
|
| 313 |
-
if gradient is None or not bool(torch.isfinite(gradient).all().item()):
|
| 314 |
-
raise FloatingPointError("static timing gradient is missing or nonfinite")
|
| 315 |
-
finish_phase("backward", start, start_event)
|
| 316 |
-
|
| 317 |
-
start, start_event = start_phase()
|
| 318 |
-
optimizer.step()
|
| 319 |
-
finish_phase("adamw", start, start_event)
|
| 320 |
-
return TimedStaticStep(
|
| 321 |
-
loss=loss,
|
| 322 |
-
phase_seconds=phases,
|
| 323 |
-
phase_cuda_seconds=cuda_phases,
|
| 324 |
-
total_seconds=clock() - total_start,
|
| 325 |
-
)
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
def _require_mapping(value: object, name: str) -> Mapping[str, object]:
|
| 329 |
-
if not isinstance(value, Mapping):
|
| 330 |
-
raise ValueError(f"profile {name} must be a mapping")
|
| 331 |
-
return value
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
def _canonical_bpe_children(
|
| 335 |
-
tokenizer: Path,
|
| 336 |
-
model_vocab_size: int,
|
| 337 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 338 |
-
payload = _require_mapping(
|
| 339 |
-
json.loads(tokenizer.read_text(encoding="utf-8")), "tokenizer"
|
| 340 |
-
)
|
| 341 |
-
model = _require_mapping(payload.get("model"), "tokenizer model")
|
| 342 |
-
if model.get("type") != "BPE":
|
| 343 |
-
raise ValueError("profile tokenizer must use BPE")
|
| 344 |
-
vocab = _require_mapping(model.get("vocab"), "tokenizer vocabulary")
|
| 345 |
-
merges = model.get("merges")
|
| 346 |
-
if not isinstance(merges, list):
|
| 347 |
-
raise ValueError("profile tokenizer merges must be a list")
|
| 348 |
-
ids = [int(value) for value in vocab.values()]
|
| 349 |
-
if (
|
| 350 |
-
len(vocab) != model_vocab_size
|
| 351 |
-
or not ids
|
| 352 |
-
or min(ids) != 0
|
| 353 |
-
or max(ids) + 1 != len(vocab)
|
| 354 |
-
or len(set(ids)) != len(ids)
|
| 355 |
-
):
|
| 356 |
-
raise ValueError("profile tokenizer model vocabulary differs")
|
| 357 |
-
added_tokens = payload.get("added_tokens", [])
|
| 358 |
-
if not isinstance(added_tokens, list):
|
| 359 |
-
raise ValueError("profile tokenizer added tokens must be a list")
|
| 360 |
-
excluded = {
|
| 361 |
-
int(row["id"])
|
| 362 |
-
for row in added_tokens
|
| 363 |
-
if isinstance(row, Mapping)
|
| 364 |
-
and row.get("special")
|
| 365 |
-
and 0 <= int(row["id"]) < model_vocab_size
|
| 366 |
-
}
|
| 367 |
-
left = torch.full((model_vocab_size,), -1, dtype=torch.long)
|
| 368 |
-
right = torch.full((model_vocab_size,), -1, dtype=torch.long)
|
| 369 |
-
seen: set[int] = set()
|
| 370 |
-
for rank, raw_pair in enumerate(merges):
|
| 371 |
-
if not isinstance(raw_pair, list) or len(raw_pair) != 2:
|
| 372 |
-
raise ValueError(f"invalid profile BPE merge at rank {rank}")
|
| 373 |
-
first, second = raw_pair
|
| 374 |
-
if (
|
| 375 |
-
not isinstance(first, str)
|
| 376 |
-
or not isinstance(second, str)
|
| 377 |
-
or not first
|
| 378 |
-
or not second
|
| 379 |
-
):
|
| 380 |
-
continue
|
| 381 |
-
output = first + second
|
| 382 |
-
if output not in vocab or first not in vocab or second not in vocab:
|
| 383 |
-
continue
|
| 384 |
-
output_id = int(vocab[output])
|
| 385 |
-
if output_id in excluded or output_id in seen:
|
| 386 |
-
continue
|
| 387 |
-
if len(first) >= len(output) or len(second) >= len(output):
|
| 388 |
-
raise ValueError("profile BPE parent graph is not length-decreasing")
|
| 389 |
-
seen.add(output_id)
|
| 390 |
-
left[output_id] = int(vocab[first])
|
| 391 |
-
right[output_id] = int(vocab[second])
|
| 392 |
-
return left, right
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
def validate_profile_model_inputs(
|
| 396 |
-
*,
|
| 397 |
-
inventory_manifest: str | Path,
|
| 398 |
-
initializer_model: str | Path,
|
| 399 |
-
tokenizer: str | Path,
|
| 400 |
-
expected_inventory_sha256: str,
|
| 401 |
-
expected_initializer_sha256: str,
|
| 402 |
-
expected_tokenizer_sha256: str,
|
| 403 |
-
expected_vocab_size: int,
|
| 404 |
-
expected_dimension: int,
|
| 405 |
-
) -> ValidatedProfileModelInputs:
|
| 406 |
-
"""Hash and shape-check full profile inputs before allocating model state."""
|
| 407 |
-
inventory_path = Path(inventory_manifest).resolve()
|
| 408 |
-
initializer_path = Path(initializer_model).resolve()
|
| 409 |
-
tokenizer_path = Path(tokenizer).resolve()
|
| 410 |
-
inventory_hash = sha256_file(inventory_path)
|
| 411 |
-
if inventory_hash != expected_inventory_sha256:
|
| 412 |
-
raise ValueError("profile inventory hash mismatch")
|
| 413 |
-
initializer_hash = sha256_file(initializer_path)
|
| 414 |
-
if initializer_hash != expected_initializer_sha256:
|
| 415 |
-
raise ValueError("profile initializer hash mismatch")
|
| 416 |
-
tokenizer_hash = sha256_file(tokenizer_path)
|
| 417 |
-
if tokenizer_hash != expected_tokenizer_sha256:
|
| 418 |
-
raise ValueError("profile tokenizer hash mismatch")
|
| 419 |
-
|
| 420 |
-
inventory = _require_mapping(
|
| 421 |
-
json.loads(inventory_path.read_text(encoding="utf-8")), "inventory"
|
| 422 |
-
)
|
| 423 |
-
if inventory.get("protocol") != "latent-lexicon-pair-inventory-v1":
|
| 424 |
-
raise ValueError("profile inventory protocol mismatch")
|
| 425 |
-
if inventory.get("selection_uses_heldout_or_benchmark_data") is not False:
|
| 426 |
-
raise ValueError("profile inventory is not training-only")
|
| 427 |
-
tokenizer_metadata = _require_mapping(
|
| 428 |
-
inventory.get("tokenizer"), "inventory tokenizer"
|
| 429 |
-
)
|
| 430 |
-
if (
|
| 431 |
-
tokenizer_metadata.get("sha256") != tokenizer_hash
|
| 432 |
-
or int(tokenizer_metadata.get("model_vocab_size", -1))
|
| 433 |
-
!= expected_vocab_size
|
| 434 |
-
):
|
| 435 |
-
raise ValueError("profile inventory tokenizer identity differs")
|
| 436 |
-
outputs = inventory.get("outputs")
|
| 437 |
-
if not isinstance(outputs, list):
|
| 438 |
-
raise ValueError("profile inventory outputs must be a list")
|
| 439 |
-
key_outputs = [
|
| 440 |
-
_require_mapping(row, "inventory output")
|
| 441 |
-
for row in outputs
|
| 442 |
-
if isinstance(row, Mapping)
|
| 443 |
-
and Path(str(row.get("path", ""))).name == "pair_keys.npy"
|
| 444 |
-
]
|
| 445 |
-
if len(key_outputs) != 1:
|
| 446 |
-
raise ValueError("profile inventory must identify one pair-key array")
|
| 447 |
-
pair_key_path = Path(str(key_outputs[0]["path"])).resolve()
|
| 448 |
-
pair_key_hash = sha256_file(pair_key_path)
|
| 449 |
-
if key_outputs[0].get("sha256") != pair_key_hash:
|
| 450 |
-
raise ValueError("profile pair-key hash mismatch")
|
| 451 |
-
pair_index = PairGateIndex.from_npy(pair_key_path)
|
| 452 |
-
inventory_counts = _require_mapping(
|
| 453 |
-
inventory.get("inventory"), "inventory counts"
|
| 454 |
-
)
|
| 455 |
-
if int(inventory_counts.get("distinct_pairs", -1)) != len(pair_index.keys):
|
| 456 |
-
raise ValueError("profile pair-key cardinality differs")
|
| 457 |
-
|
| 458 |
-
with safe_open(initializer_path, framework="pt", device="cpu") as handle:
|
| 459 |
-
if set(handle.keys()) != {"embedding.weight"}:
|
| 460 |
-
raise ValueError("profile initializer tensor inventory differs")
|
| 461 |
-
shape = tuple(handle.get_slice("embedding.weight").get_shape())
|
| 462 |
-
if shape[0] < expected_vocab_size or shape[1:] != (expected_dimension,):
|
| 463 |
-
raise ValueError("profile initializer embedding shape differs")
|
| 464 |
-
parent_left, parent_right = _canonical_bpe_children(
|
| 465 |
-
tokenizer_path, expected_vocab_size
|
| 466 |
-
)
|
| 467 |
-
return ValidatedProfileModelInputs(
|
| 468 |
-
inventory_manifest=inventory_path,
|
| 469 |
-
initializer_model=initializer_path,
|
| 470 |
-
tokenizer=tokenizer_path,
|
| 471 |
-
pair_index=pair_index,
|
| 472 |
-
parent_left=parent_left,
|
| 473 |
-
parent_right=parent_right,
|
| 474 |
-
model_vocab_size=expected_vocab_size,
|
| 475 |
-
dimension=expected_dimension,
|
| 476 |
-
identities={
|
| 477 |
-
"inventory_manifest_sha256": inventory_hash,
|
| 478 |
-
"initializer_sha256": initializer_hash,
|
| 479 |
-
"tokenizer_sha256": tokenizer_hash,
|
| 480 |
-
"pair_keys_sha256": pair_key_hash,
|
| 481 |
-
},
|
| 482 |
-
)
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
def build_profile_model(
|
| 486 |
-
inputs: ValidatedProfileModelInputs,
|
| 487 |
-
*,
|
| 488 |
-
device: torch.device,
|
| 489 |
-
residual_rows: int,
|
| 490 |
-
residual_components: int,
|
| 491 |
-
training_seed: int,
|
| 492 |
-
initial_merge_potential: float = -2.0,
|
| 493 |
-
initial_split_potential: float = -2.0,
|
| 494 |
-
potential_bound: float = 4.0,
|
| 495 |
-
) -> LatentLexiconModel:
|
| 496 |
-
"""Allocate a latent model only after `validate_profile_model_inputs`."""
|
| 497 |
-
if device.type == "cuda" and not torch.cuda.is_available():
|
| 498 |
-
raise ValueError("CUDA profile model requested without CUDA")
|
| 499 |
-
with safe_open(
|
| 500 |
-
inputs.initializer_model, framework="pt", device="cpu"
|
| 501 |
-
) as handle:
|
| 502 |
-
embedding = handle.get_slice("embedding.weight")[
|
| 503 |
-
:inputs.model_vocab_size
|
| 504 |
-
]
|
| 505 |
-
if embedding.dtype != torch.float32:
|
| 506 |
-
raise ValueError("profile initializer embedding must be float32")
|
| 507 |
-
return LatentLexiconModel(
|
| 508 |
-
embedding_weight=embedding.to(device=device),
|
| 509 |
-
parent_left=inputs.parent_left,
|
| 510 |
-
parent_right=inputs.parent_right,
|
| 511 |
-
pair_index=inputs.pair_index,
|
| 512 |
-
residual_rows=residual_rows,
|
| 513 |
-
residual_components=residual_components,
|
| 514 |
-
training_seed=training_seed,
|
| 515 |
-
initial_merge_potential=initial_merge_potential,
|
| 516 |
-
initial_split_potential=initial_split_potential,
|
| 517 |
-
potential_bound=potential_bound,
|
| 518 |
-
)
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
def build_static_profile_embedding(
|
| 522 |
-
inputs: ValidatedProfileModelInputs,
|
| 523 |
-
*,
|
| 524 |
-
device: torch.device,
|
| 525 |
-
) -> torch.nn.Embedding:
|
| 526 |
-
"""Load only the identity-checked base table for the static control."""
|
| 527 |
-
if device.type == "cuda" and not torch.cuda.is_available():
|
| 528 |
-
raise ValueError("CUDA static profile requested without CUDA")
|
| 529 |
-
with safe_open(
|
| 530 |
-
inputs.initializer_model, framework="pt", device="cpu"
|
| 531 |
-
) as handle:
|
| 532 |
-
weight = handle.get_slice("embedding.weight")[:inputs.model_vocab_size]
|
| 533 |
-
if weight.dtype != torch.float32:
|
| 534 |
-
raise ValueError("profile initializer embedding must be float32")
|
| 535 |
-
return torch.nn.Embedding.from_pretrained(
|
| 536 |
-
weight.to(device=device), freeze=False
|
| 537 |
-
)
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
def _hash_state_value(digest: object, value: object) -> None:
|
| 541 |
-
update = digest.update # type: ignore[attr-defined]
|
| 542 |
-
if isinstance(value, torch.Tensor):
|
| 543 |
-
if value.layout != torch.strided:
|
| 544 |
-
raise ValueError("training-state digest requires strided tensors")
|
| 545 |
-
update(b"tensor\0")
|
| 546 |
-
update(str(value.dtype).encode("ascii") + b"\0")
|
| 547 |
-
update(json.dumps(list(value.shape), separators=(",", ":")).encode("ascii"))
|
| 548 |
-
update(b"\0")
|
| 549 |
-
flat = value.detach().reshape(-1)
|
| 550 |
-
elements = max(1, (8 * 1024 * 1024) // max(1, value.element_size()))
|
| 551 |
-
for start in range(0, flat.numel(), elements):
|
| 552 |
-
chunk = flat[start:start + elements].to(device="cpu").contiguous()
|
| 553 |
-
update(memoryview(chunk.numpy()).cast("B"))
|
| 554 |
-
return
|
| 555 |
-
if isinstance(value, Mapping):
|
| 556 |
-
update(b"mapping\0")
|
| 557 |
-
for key in sorted(value, key=lambda item: (type(item).__name__, repr(item))):
|
| 558 |
-
_hash_state_value(digest, key)
|
| 559 |
-
_hash_state_value(digest, value[key])
|
| 560 |
-
update(b"end-mapping\0")
|
| 561 |
-
return
|
| 562 |
-
if isinstance(value, (list, tuple)):
|
| 563 |
-
update(b"tuple\0" if isinstance(value, tuple) else b"list\0")
|
| 564 |
-
for item in value:
|
| 565 |
-
_hash_state_value(digest, item)
|
| 566 |
-
update(b"end-sequence\0")
|
| 567 |
-
return
|
| 568 |
-
if value is None or isinstance(value, (bool, int, float, str)):
|
| 569 |
-
update(type(value).__name__.encode("ascii") + b"\0")
|
| 570 |
-
update(json.dumps(value, sort_keys=True).encode("utf-8") + b"\0")
|
| 571 |
-
return
|
| 572 |
-
raise ValueError(f"unsupported training-state value: {type(value).__name__}")
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
def training_state_sha256(
|
| 576 |
-
model: LatentLexiconModel,
|
| 577 |
-
optimizers: LatentOptimizers,
|
| 578 |
-
generator: torch.Generator,
|
| 579 |
-
) -> str:
|
| 580 |
-
"""Stream a device-independent digest of exact resumable training state."""
|
| 581 |
-
digest = hashlib.sha256()
|
| 582 |
-
_hash_state_value(digest, {
|
| 583 |
-
"model": model.state_dict(),
|
| 584 |
-
"optimizers": optimizers.state_dict(),
|
| 585 |
-
"generator_state": generator.get_state(),
|
| 586 |
-
})
|
| 587 |
-
return digest.hexdigest()
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
def summarize_gradient_draws(draws: torch.Tensor) -> dict[str, object]:
|
| 591 |
-
"""Summarize iid gradient vectors without silently inventing an SNR."""
|
| 592 |
-
if draws.ndim == 2 and draws.shape[0] < 2:
|
| 593 |
-
raise ValueError("gradient summary needs at least two draws")
|
| 594 |
-
if (
|
| 595 |
-
draws.ndim != 2
|
| 596 |
-
or draws.shape[1] <= 0
|
| 597 |
-
or not draws.is_floating_point()
|
| 598 |
-
or not bool(torch.isfinite(draws).all())
|
| 599 |
-
):
|
| 600 |
-
raise ValueError("gradient draws must be a finite floating matrix")
|
| 601 |
-
values = draws.detach().to(device="cpu", dtype=torch.float64)
|
| 602 |
-
count, dimension = values.shape
|
| 603 |
-
total = values.sum(dim=0)
|
| 604 |
-
mean = total / count
|
| 605 |
-
centered = values - mean
|
| 606 |
-
trace_variance = float(centered.square().sum() / (count - 1))
|
| 607 |
-
mean_squared_norm = float(mean.square().sum())
|
| 608 |
-
corrected = mean_squared_norm - trace_variance / count
|
| 609 |
-
norms = torch.linalg.vector_norm(values, dim=1)
|
| 610 |
-
zero_fractions = (values == 0).to(torch.float64).mean(dim=1)
|
| 611 |
-
loo_cosines: list[float | None] = []
|
| 612 |
-
for index in range(count):
|
| 613 |
-
leave_one_out = (total - values[index]) / (count - 1)
|
| 614 |
-
denominator = float(norms[index] * torch.linalg.vector_norm(leave_one_out))
|
| 615 |
-
loo_cosines.append(
|
| 616 |
-
None
|
| 617 |
-
if denominator == 0.0
|
| 618 |
-
else float(torch.dot(values[index], leave_one_out) / denominator)
|
| 619 |
-
)
|
| 620 |
-
return {
|
| 621 |
-
"draw_count": count,
|
| 622 |
-
"dimension": dimension,
|
| 623 |
-
"gradient_norms": [float(value) for value in norms],
|
| 624 |
-
"zero_fractions": [float(value) for value in zero_fractions],
|
| 625 |
-
"loo_cosines": loo_cosines,
|
| 626 |
-
"trace_variance": trace_variance,
|
| 627 |
-
"mean_gradient_squared_norm": mean_squared_norm,
|
| 628 |
-
"variance_to_raw_signal_ratio": (
|
| 629 |
-
None if mean_squared_norm == 0.0
|
| 630 |
-
else trace_variance / mean_squared_norm
|
| 631 |
-
),
|
| 632 |
-
"noise_corrected_signal_squared_norm": (
|
| 633 |
-
corrected if corrected > 0.0 else None
|
| 634 |
-
),
|
| 635 |
-
"signal_status": (
|
| 636 |
-
"resolved positive" if corrected > 0.0 else "signal unresolved"
|
| 637 |
-
),
|
| 638 |
-
}
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
def summarize_single_gradient_draw(draws: torch.Tensor) -> dict[str, object]:
|
| 642 |
-
"""Describe one feasibility draw without implying a variance estimate."""
|
| 643 |
-
if draws.ndim != 2 or draws.shape[0] != 1:
|
| 644 |
-
raise ValueError("single-draw summary needs exactly one draw")
|
| 645 |
-
if (
|
| 646 |
-
draws.shape[1] <= 0
|
| 647 |
-
or not draws.is_floating_point()
|
| 648 |
-
or not bool(torch.isfinite(draws).all())
|
| 649 |
-
):
|
| 650 |
-
raise ValueError("single draw must be a finite floating matrix")
|
| 651 |
-
values = draws.detach().to(device="cpu", dtype=torch.float64)
|
| 652 |
-
return {
|
| 653 |
-
"draw_count": 1,
|
| 654 |
-
"dimension": int(values.shape[1]),
|
| 655 |
-
"gradient_norm": float(torch.linalg.vector_norm(values[0])),
|
| 656 |
-
"zero_fraction": float((values[0] == 0).to(torch.float64).mean()),
|
| 657 |
-
}
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
def paired_trace_variance_ratio(
|
| 661 |
-
paired: torch.Tensor,
|
| 662 |
-
global_control: torch.Tensor,
|
| 663 |
-
*,
|
| 664 |
-
bootstrap_replicates: int,
|
| 665 |
-
seed: int,
|
| 666 |
-
) -> dict[str, object]:
|
| 667 |
-
"""Bootstrap a matched trace-variance ratio using shared draw resamples."""
|
| 668 |
-
if (
|
| 669 |
-
paired.ndim != 2
|
| 670 |
-
or paired.shape != global_control.shape
|
| 671 |
-
or paired.shape[0] < 2
|
| 672 |
-
or paired.shape[1] <= 0
|
| 673 |
-
or not paired.is_floating_point()
|
| 674 |
-
or not global_control.is_floating_point()
|
| 675 |
-
or not bool(torch.isfinite(paired).all())
|
| 676 |
-
or not bool(torch.isfinite(global_control).all())
|
| 677 |
-
):
|
| 678 |
-
raise ValueError("matched bootstrap gradients are invalid")
|
| 679 |
-
if (
|
| 680 |
-
isinstance(bootstrap_replicates, bool)
|
| 681 |
-
or not isinstance(bootstrap_replicates, int)
|
| 682 |
-
or bootstrap_replicates <= 1
|
| 683 |
-
):
|
| 684 |
-
raise ValueError("matched bootstrap needs at least two replicates")
|
| 685 |
-
paired_array = paired.detach().to(device="cpu", dtype=torch.float64).numpy()
|
| 686 |
-
global_array = (
|
| 687 |
-
global_control.detach().to(device="cpu", dtype=torch.float64).numpy()
|
| 688 |
-
)
|
| 689 |
-
draws = paired_array.shape[0]
|
| 690 |
-
|
| 691 |
-
def trace_variance(values: np.ndarray) -> float:
|
| 692 |
-
centered = values - values.mean(axis=0)
|
| 693 |
-
return float(np.square(centered).sum() / (draws - 1))
|
| 694 |
-
|
| 695 |
-
paired_point = trace_variance(paired_array)
|
| 696 |
-
global_point = trace_variance(global_array)
|
| 697 |
-
point = None if global_point == 0.0 else paired_point / global_point
|
| 698 |
-
generator = np.random.Generator(np.random.PCG64(seed))
|
| 699 |
-
sampled = generator.integers(
|
| 700 |
-
0, draws, size=(bootstrap_replicates, draws), dtype=np.int64
|
| 701 |
-
)
|
| 702 |
-
counts = np.zeros((bootstrap_replicates, draws), dtype=np.float64)
|
| 703 |
-
np.add.at(
|
| 704 |
-
counts,
|
| 705 |
-
(np.repeat(np.arange(bootstrap_replicates), draws), sampled.reshape(-1)),
|
| 706 |
-
1.0,
|
| 707 |
-
)
|
| 708 |
-
|
| 709 |
-
def bootstrap_trace(values: np.ndarray) -> np.ndarray:
|
| 710 |
-
gram = values @ values.T
|
| 711 |
-
weighted_squares = counts @ np.diag(gram)
|
| 712 |
-
sum_squared = np.einsum("bi,ij,bj->b", counts, gram, counts)
|
| 713 |
-
result = (weighted_squares - sum_squared / draws) / (draws - 1)
|
| 714 |
-
return np.maximum(result, 0.0)
|
| 715 |
-
|
| 716 |
-
paired_bootstrap = bootstrap_trace(paired_array)
|
| 717 |
-
global_bootstrap = bootstrap_trace(global_array)
|
| 718 |
-
valid = global_bootstrap > 0.0
|
| 719 |
-
ratios = paired_bootstrap[valid] / global_bootstrap[valid]
|
| 720 |
-
if ratios.size < 2:
|
| 721 |
-
raise ValueError("matched bootstrap variance ratio is unresolved")
|
| 722 |
-
return {
|
| 723 |
-
"trace_variance_ratio": point,
|
| 724 |
-
"bootstrap_mean": float(ratios.mean()),
|
| 725 |
-
"bootstrap_sd": float(ratios.std(ddof=1)),
|
| 726 |
-
"bootstrap_replicates": bootstrap_replicates,
|
| 727 |
-
"valid_bootstrap_replicates": int(ratios.size),
|
| 728 |
-
"seed": int(seed),
|
| 729 |
-
}
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
def global_rloo_surrogate(
|
| 733 |
-
primary_log_probabilities: torch.Tensor,
|
| 734 |
-
alternate_log_probabilities: torch.Tensor,
|
| 735 |
-
primary_loss: torch.Tensor,
|
| 736 |
-
all_alternate_loss: torch.Tensor,
|
| 737 |
-
) -> torch.Tensor:
|
| 738 |
-
"""Return the global two-sample RLOO control for one matched draw."""
|
| 739 |
-
if primary_log_probabilities.ndim != 1:
|
| 740 |
-
raise ValueError("global RLOO log probabilities must be one-dimensional")
|
| 741 |
-
if primary_log_probabilities.shape != alternate_log_probabilities.shape:
|
| 742 |
-
raise ValueError("global RLOO log-probability shapes differ")
|
| 743 |
-
if primary_loss.ndim != 0 or all_alternate_loss.ndim != 0:
|
| 744 |
-
raise ValueError("global RLOO losses must be scalars")
|
| 745 |
-
tensors = (
|
| 746 |
-
alternate_log_probabilities,
|
| 747 |
-
primary_loss,
|
| 748 |
-
all_alternate_loss,
|
| 749 |
-
)
|
| 750 |
-
if any(tensor.device != primary_log_probabilities.device for tensor in tensors):
|
| 751 |
-
raise ValueError("global RLOO tensors must share a device")
|
| 752 |
-
if any(tensor.dtype != primary_log_probabilities.dtype for tensor in tensors):
|
| 753 |
-
raise ValueError("global RLOO tensors must share a dtype")
|
| 754 |
-
credit = (primary_loss - all_alternate_loss).detach()
|
| 755 |
-
return 0.5 * credit * (
|
| 756 |
-
primary_log_probabilities.sum() - alternate_log_probabilities.sum()
|
| 757 |
-
)
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
def symmetric_info_nce_loss(
|
| 761 |
-
anchors: torch.Tensor,
|
| 762 |
-
positives: torch.Tensor,
|
| 763 |
-
scale: float,
|
| 764 |
-
) -> torch.Tensor:
|
| 765 |
-
if (
|
| 766 |
-
anchors.ndim != 2
|
| 767 |
-
or anchors.shape != positives.shape
|
| 768 |
-
or anchors.shape[0] <= 0
|
| 769 |
-
or anchors.shape[1] <= 0
|
| 770 |
-
or not anchors.is_floating_point()
|
| 771 |
-
or positives.dtype != anchors.dtype
|
| 772 |
-
or positives.device != anchors.device
|
| 773 |
-
):
|
| 774 |
-
raise ValueError("symmetric InfoNCE inputs are invalid")
|
| 775 |
-
normalized_anchor = F.normalize(anchors, dim=1)
|
| 776 |
-
normalized_positive = F.normalize(positives, dim=1)
|
| 777 |
-
scores = normalized_anchor @ normalized_positive.t() * float(scale)
|
| 778 |
-
labels = torch.arange(scores.shape[0], device=scores.device)
|
| 779 |
-
return 0.5 * (
|
| 780 |
-
F.cross_entropy(scores, labels)
|
| 781 |
-
+ F.cross_entropy(scores.t(), labels)
|
| 782 |
-
)
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
def exact_gate_row_union(
|
| 786 |
-
model: LatentLexiconModel,
|
| 787 |
-
anchors: Sequence[Sequence[int]],
|
| 788 |
-
positives: Sequence[Sequence[int]],
|
| 789 |
-
) -> torch.Tensor:
|
| 790 |
-
"""Return sorted exact inventory rows present in a fixed root batch."""
|
| 791 |
-
if len(anchors) == 0 or len(anchors) != len(positives):
|
| 792 |
-
raise ValueError("gate-row union needs aligned nonempty text pairs")
|
| 793 |
-
rows = [
|
| 794 |
-
model.pair_gate.index.lookup_sequence(roots)
|
| 795 |
-
for roots in tuple(anchors) + tuple(positives)
|
| 796 |
-
if len(roots) > 1
|
| 797 |
-
]
|
| 798 |
-
if not rows:
|
| 799 |
-
return torch.empty(0, dtype=torch.long)
|
| 800 |
-
present = torch.cat(rows)
|
| 801 |
-
return torch.unique(present[present >= 0], sorted=True)
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
def _validate_gate_rows(model: LatentLexiconModel, rows: torch.Tensor) -> None:
|
| 805 |
-
if (
|
| 806 |
-
rows.device.type != "cpu"
|
| 807 |
-
or rows.dtype != torch.long
|
| 808 |
-
or rows.ndim != 1
|
| 809 |
-
or (rows.numel() and (
|
| 810 |
-
int(rows[0]) < 0
|
| 811 |
-
or int(rows[-1]) >= model.pair_gate.gates.num_embeddings
|
| 812 |
-
or bool((rows[1:] <= rows[:-1]).any())
|
| 813 |
-
))
|
| 814 |
-
):
|
| 815 |
-
raise ValueError("gate rows must be a sorted unique CPU int64 vector")
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
def align_sparse_gate_gradient(
|
| 819 |
-
gradient: torch.Tensor | None,
|
| 820 |
-
gate_rows: torch.Tensor,
|
| 821 |
-
dtype: torch.dtype,
|
| 822 |
-
) -> torch.Tensor:
|
| 823 |
-
if gradient is None:
|
| 824 |
-
if gate_rows.numel():
|
| 825 |
-
raise RuntimeError("exact gate gradient is missing")
|
| 826 |
-
return torch.empty(0, dtype=dtype)
|
| 827 |
-
if gradient.layout != torch.sparse_coo:
|
| 828 |
-
raise RuntimeError("exact gate gradient became dense")
|
| 829 |
-
coalesced = gradient.coalesce()
|
| 830 |
-
indices = coalesced.indices()
|
| 831 |
-
values = coalesced.values()
|
| 832 |
-
if indices.shape[0] != 1 or values.ndim not in (1, 2):
|
| 833 |
-
raise RuntimeError("exact gate sparse gradient has invalid shape")
|
| 834 |
-
sparse_rows = indices[0].detach().to(device="cpu")
|
| 835 |
-
sparse_values = values.detach().reshape(-1).to(device="cpu", dtype=dtype)
|
| 836 |
-
positions = torch.searchsorted(gate_rows, sparse_rows)
|
| 837 |
-
if positions.numel() and (
|
| 838 |
-
bool((positions >= gate_rows.numel()).any())
|
| 839 |
-
or not torch.equal(gate_rows.index_select(0, positions), sparse_rows)
|
| 840 |
-
):
|
| 841 |
-
raise RuntimeError("gate gradient contains a row outside the fixed union")
|
| 842 |
-
aligned = torch.zeros(gate_rows.numel(), dtype=dtype)
|
| 843 |
-
if positions.numel():
|
| 844 |
-
aligned.index_copy_(0, positions, sparse_values)
|
| 845 |
-
return aligned
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
def _architecture_gradient(
|
| 849 |
-
model: LatentLexiconModel,
|
| 850 |
-
loss: torch.Tensor,
|
| 851 |
-
gate_rows: torch.Tensor,
|
| 852 |
-
*,
|
| 853 |
-
retain_graph: bool,
|
| 854 |
-
) -> ArchitectureGradient:
|
| 855 |
-
split_gradient, gate_gradient = torch.autograd.grad(
|
| 856 |
-
loss,
|
| 857 |
-
(model.split_raw_scores, model.pair_gate.gates.weight),
|
| 858 |
-
retain_graph=retain_graph,
|
| 859 |
-
allow_unused=True,
|
| 860 |
-
)
|
| 861 |
-
if split_gradient is None:
|
| 862 |
-
split = torch.zeros_like(model.split_raw_scores, device="cpu")
|
| 863 |
-
else:
|
| 864 |
-
if split_gradient.layout != torch.strided:
|
| 865 |
-
raise RuntimeError("split-score gradient is not dense")
|
| 866 |
-
split = split_gradient.detach().to(device="cpu").clone()
|
| 867 |
-
gate = align_sparse_gate_gradient(gate_gradient, gate_rows, split.dtype)
|
| 868 |
-
if not bool(torch.isfinite(split).all()) or not bool(torch.isfinite(gate).all()):
|
| 869 |
-
raise FloatingPointError("architecture gradient is nonfinite")
|
| 870 |
-
return ArchitectureGradient(
|
| 871 |
-
split=split,
|
| 872 |
-
gate_rows=gate_rows.clone(),
|
| 873 |
-
gate=gate,
|
| 874 |
-
)
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
def paired_global_architecture_gradients(
|
| 878 |
-
model: LatentLexiconModel,
|
| 879 |
-
batch: PairedLatentBatch,
|
| 880 |
-
*,
|
| 881 |
-
scale: float,
|
| 882 |
-
gate_rows: torch.Tensor,
|
| 883 |
-
) -> MatchedArchitectureGradients:
|
| 884 |
-
"""Extract paired/global architecture gradients from identical hard paths."""
|
| 885 |
-
_validate_gate_rows(model, gate_rows)
|
| 886 |
-
alternate_anchors = torch.stack([
|
| 887 |
-
sample.numerator for sample in batch.alternate_anchors
|
| 888 |
-
])
|
| 889 |
-
alternate_positives = torch.stack([
|
| 890 |
-
sample.numerator for sample in batch.alternate_positives
|
| 891 |
-
])
|
| 892 |
-
all_alternate_loss = symmetric_info_nce_loss(
|
| 893 |
-
alternate_anchors, alternate_positives, scale
|
| 894 |
-
)
|
| 895 |
-
primary_logs = torch.stack([
|
| 896 |
-
sample.mixture_log_probability
|
| 897 |
-
for sample in batch.primary_anchors + batch.primary_positives
|
| 898 |
-
])
|
| 899 |
-
alternate_logs = torch.stack([
|
| 900 |
-
sample.mixture_log_probability
|
| 901 |
-
for sample in batch.alternate_anchors + batch.alternate_positives
|
| 902 |
-
])
|
| 903 |
-
global_loss = global_rloo_surrogate(
|
| 904 |
-
primary_logs,
|
| 905 |
-
alternate_logs,
|
| 906 |
-
batch.embedding_loss,
|
| 907 |
-
all_alternate_loss,
|
| 908 |
-
)
|
| 909 |
-
paired = _architecture_gradient(
|
| 910 |
-
model, batch.architecture_loss, gate_rows, retain_graph=True
|
| 911 |
-
)
|
| 912 |
-
global_control = _architecture_gradient(
|
| 913 |
-
model, global_loss, gate_rows, retain_graph=False
|
| 914 |
-
)
|
| 915 |
-
return MatchedArchitectureGradients(
|
| 916 |
-
paired=paired,
|
| 917 |
-
global_control=global_control,
|
| 918 |
-
primary_loss=batch.embedding_loss.detach().to(device="cpu"),
|
| 919 |
-
all_alternate_loss=all_alternate_loss.detach().to(device="cpu"),
|
| 920 |
-
)
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
def collect_matched_gradient_draws(
|
| 924 |
-
*,
|
| 925 |
-
model: LatentLexiconModel,
|
| 926 |
-
anchors: Sequence[Sequence[int]],
|
| 927 |
-
positives: Sequence[Sequence[int]],
|
| 928 |
-
draws: int,
|
| 929 |
-
seed: int,
|
| 930 |
-
scale: float,
|
| 931 |
-
explorer_epsilon: float,
|
| 932 |
-
) -> MatchedGradientDraws:
|
| 933 |
-
"""Collect iid paired/global gradients without changing model parameters.
|
| 934 |
-
|
| 935 |
-
A single draw is useful for memory/runtime feasibility probes. Statistical
|
| 936 |
-
summaries enforce their own minimum of two draws.
|
| 937 |
-
"""
|
| 938 |
-
if isinstance(draws, bool) or not isinstance(draws, int) or draws < 1:
|
| 939 |
-
raise ValueError("matched gradient profiling needs at least one draw")
|
| 940 |
-
gate_rows = exact_gate_row_union(model, anchors, positives)
|
| 941 |
-
paired_split: list[torch.Tensor] = []
|
| 942 |
-
paired_gate: list[torch.Tensor] = []
|
| 943 |
-
global_split: list[torch.Tensor] = []
|
| 944 |
-
global_gate: list[torch.Tensor] = []
|
| 945 |
-
primary_losses: list[torch.Tensor] = []
|
| 946 |
-
all_alternate_losses: list[torch.Tensor] = []
|
| 947 |
-
device = model.embedding.weight.device
|
| 948 |
-
for draw in range(draws):
|
| 949 |
-
generator = torch.Generator(device=device).manual_seed(seed + draw)
|
| 950 |
-
batch = sample_paired_latent_batch(
|
| 951 |
-
model,
|
| 952 |
-
anchors,
|
| 953 |
-
positives,
|
| 954 |
-
generator,
|
| 955 |
-
scale=scale,
|
| 956 |
-
explorer_epsilon=explorer_epsilon,
|
| 957 |
-
)
|
| 958 |
-
matched = paired_global_architecture_gradients(
|
| 959 |
-
model, batch, scale=scale, gate_rows=gate_rows
|
| 960 |
-
)
|
| 961 |
-
paired_split.append(matched.paired.split)
|
| 962 |
-
paired_gate.append(matched.paired.gate)
|
| 963 |
-
global_split.append(matched.global_control.split)
|
| 964 |
-
global_gate.append(matched.global_control.gate)
|
| 965 |
-
primary_losses.append(matched.primary_loss)
|
| 966 |
-
all_alternate_losses.append(matched.all_alternate_loss)
|
| 967 |
-
return MatchedGradientDraws(
|
| 968 |
-
gate_rows=gate_rows,
|
| 969 |
-
paired_split=torch.stack(paired_split),
|
| 970 |
-
paired_gate=torch.stack(paired_gate),
|
| 971 |
-
global_split=torch.stack(global_split),
|
| 972 |
-
global_gate=torch.stack(global_gate),
|
| 973 |
-
primary_losses=torch.stack(primary_losses),
|
| 974 |
-
all_alternate_losses=torch.stack(all_alternate_losses),
|
| 975 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/latent_streaming.py
DELETED
|
@@ -1,1065 +0,0 @@
|
|
| 1 |
-
"""Deterministic, held-out-safe Parquet batches for latent-lexicon training."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import bisect
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
import hashlib
|
| 8 |
-
import json
|
| 9 |
-
import math
|
| 10 |
-
import os
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
import tempfile
|
| 13 |
-
from typing import Mapping, Sequence
|
| 14 |
-
import unicodedata
|
| 15 |
-
|
| 16 |
-
import numpy as np
|
| 17 |
-
import pyarrow.parquet as pq
|
| 18 |
-
from tokenizers import Tokenizer
|
| 19 |
-
import torch
|
| 20 |
-
import yaml
|
| 21 |
-
|
| 22 |
-
from starse.latent_training import (
|
| 23 |
-
LatentLexiconModel,
|
| 24 |
-
LatentOptimizers,
|
| 25 |
-
load_latent_checkpoint,
|
| 26 |
-
save_latent_checkpoint,
|
| 27 |
-
)
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
PROTOCOL = "latent-lexicon-inventory-stream-v2"
|
| 31 |
-
STATE_FORMAT = 2
|
| 32 |
-
CHECKPOINT_PROTOCOL = "latent-lexicon-streaming-checkpoint-v2"
|
| 33 |
-
POINTER_PROTOCOL = "latent-lexicon-streaming-latest-v1"
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def sha256_file(path: str | Path) -> str:
|
| 37 |
-
digest = hashlib.sha256()
|
| 38 |
-
with Path(path).open("rb") as handle:
|
| 39 |
-
for block in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
| 40 |
-
digest.update(block)
|
| 41 |
-
return digest.hexdigest()
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def _canonical_json(value: object) -> bytes:
|
| 45 |
-
return json.dumps(
|
| 46 |
-
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
| 47 |
-
).encode("utf-8")
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def _json_copy(value: object) -> object:
|
| 51 |
-
return json.loads(json.dumps(value))
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def _mapping(value: object, name: str) -> Mapping[str, object]:
|
| 55 |
-
if not isinstance(value, Mapping):
|
| 56 |
-
raise ValueError(f"latent stream {name} must be a mapping")
|
| 57 |
-
return value
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def _load_json(path: Path, name: str) -> dict[str, object]:
|
| 61 |
-
value = json.loads(path.read_text(encoding="utf-8"))
|
| 62 |
-
if not isinstance(value, dict):
|
| 63 |
-
raise ValueError(f"latent stream {name} must be a JSON object")
|
| 64 |
-
return value
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def _checked_path(
|
| 68 |
-
record: Mapping[str, object], path_key: str, hash_key: str
|
| 69 |
-
) -> Path:
|
| 70 |
-
raw_path = record.get(path_key)
|
| 71 |
-
expected_hash = record.get(hash_key)
|
| 72 |
-
if not isinstance(raw_path, str) or not isinstance(expected_hash, str):
|
| 73 |
-
raise ValueError(f"latent stream input lacks {path_key}/{hash_key}")
|
| 74 |
-
path = Path(raw_path).resolve()
|
| 75 |
-
if sha256_file(path) != expected_hash:
|
| 76 |
-
raise ValueError(f"latent stream input hash mismatch: {path}")
|
| 77 |
-
return path
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def _canonical_text(text: str) -> str:
|
| 81 |
-
return " ".join(unicodedata.normalize("NFKC", text).casefold().split())
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
@dataclass(frozen=True)
|
| 85 |
-
class StreamRowProvenance:
|
| 86 |
-
record_index: int
|
| 87 |
-
record_kind: str
|
| 88 |
-
record_name: str
|
| 89 |
-
file_sha256: str
|
| 90 |
-
row_group: int
|
| 91 |
-
row_index: int
|
| 92 |
-
flat_row: int
|
| 93 |
-
reuse_cycle: int
|
| 94 |
-
identity: str
|
| 95 |
-
pair_sha256: str
|
| 96 |
-
anchor_language: str
|
| 97 |
-
positive_language: str
|
| 98 |
-
|
| 99 |
-
def as_dict(self) -> dict[str, object]:
|
| 100 |
-
return {
|
| 101 |
-
"record_index": self.record_index,
|
| 102 |
-
"record_kind": self.record_kind,
|
| 103 |
-
"record_name": self.record_name,
|
| 104 |
-
"file_sha256": self.file_sha256,
|
| 105 |
-
"row_group": self.row_group,
|
| 106 |
-
"row_index": self.row_index,
|
| 107 |
-
"flat_row": self.flat_row,
|
| 108 |
-
"reuse_cycle": self.reuse_cycle,
|
| 109 |
-
"identity": self.identity,
|
| 110 |
-
"pair_sha256": self.pair_sha256,
|
| 111 |
-
"anchor_language": self.anchor_language,
|
| 112 |
-
"positive_language": self.positive_language,
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
@dataclass(frozen=True)
|
| 117 |
-
class StreamingLatentBatch:
|
| 118 |
-
step: int
|
| 119 |
-
record_name: str
|
| 120 |
-
anchors: tuple[tuple[int, ...], ...]
|
| 121 |
-
positives: tuple[tuple[int, ...], ...]
|
| 122 |
-
rows: tuple[StreamRowProvenance, ...]
|
| 123 |
-
sha256: str
|
| 124 |
-
|
| 125 |
-
def _digest_payload(self) -> dict[str, object]:
|
| 126 |
-
return {
|
| 127 |
-
"step": self.step,
|
| 128 |
-
"record_name": self.record_name,
|
| 129 |
-
"anchors": self.anchors,
|
| 130 |
-
"positives": self.positives,
|
| 131 |
-
"rows": [row.as_dict() for row in self.rows],
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
def recompute_sha256(self) -> str:
|
| 135 |
-
return hashlib.sha256(_canonical_json(self._digest_payload())).hexdigest()
|
| 136 |
-
|
| 137 |
-
def provenance_record(self) -> dict[str, object]:
|
| 138 |
-
return {
|
| 139 |
-
"step": self.step,
|
| 140 |
-
"record_name": self.record_name,
|
| 141 |
-
"batch_size": len(self.rows),
|
| 142 |
-
"sha256": self.sha256,
|
| 143 |
-
"anchors": self.anchors,
|
| 144 |
-
"positives": self.positives,
|
| 145 |
-
"rows": [row.as_dict() for row in self.rows],
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
def provenance(self) -> dict[str, object]:
|
| 149 |
-
"""Return the compact, text-free record persisted by checkpoints."""
|
| 150 |
-
return self.provenance_record()
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
@dataclass(frozen=True)
|
| 154 |
-
class _CorpusRecord:
|
| 155 |
-
index: int
|
| 156 |
-
kind: str
|
| 157 |
-
name: str
|
| 158 |
-
path: Path
|
| 159 |
-
file_sha256: str
|
| 160 |
-
columns: tuple[str, str]
|
| 161 |
-
languages: tuple[str, str]
|
| 162 |
-
group_rows: tuple[int, ...]
|
| 163 |
-
group_offsets: tuple[int, ...]
|
| 164 |
-
sampling_weight: float
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
class InventoryLatentBatchStream:
|
| 168 |
-
"""Yield reproducible monolingual batches without reading held-out groups.
|
| 169 |
-
|
| 170 |
-
Record choices use a checkpointed PCG64 generator. Each record has one
|
| 171 |
-
independently seeded permutation over all of its training rows, so no row
|
| 172 |
-
can repeat merely because one row group was selected more often than another.
|
| 173 |
-
"""
|
| 174 |
-
|
| 175 |
-
def __init__(
|
| 176 |
-
self,
|
| 177 |
-
inventory_manifest: str | Path,
|
| 178 |
-
*,
|
| 179 |
-
batch_size: int,
|
| 180 |
-
seed: int,
|
| 181 |
-
) -> None:
|
| 182 |
-
if isinstance(batch_size, bool) or int(batch_size) <= 1:
|
| 183 |
-
raise ValueError("latent stream batch size must exceed one")
|
| 184 |
-
if isinstance(seed, bool) or int(seed) < 0:
|
| 185 |
-
raise ValueError("latent stream seed must be a nonnegative integer")
|
| 186 |
-
self.inventory_path = Path(inventory_manifest).resolve()
|
| 187 |
-
self.batch_size = int(batch_size)
|
| 188 |
-
self.seed = int(seed)
|
| 189 |
-
(
|
| 190 |
-
self.tokenizer,
|
| 191 |
-
self.model_vocab_size,
|
| 192 |
-
self.max_chars,
|
| 193 |
-
self.holdout_row_groups,
|
| 194 |
-
self._records,
|
| 195 |
-
input_identity,
|
| 196 |
-
) = self._validate_and_build()
|
| 197 |
-
weights = np.asarray(
|
| 198 |
-
[record.sampling_weight for record in self._records], dtype=np.float64
|
| 199 |
-
)
|
| 200 |
-
self._record_probabilities = weights / weights.sum()
|
| 201 |
-
self.identity = {
|
| 202 |
-
"protocol": PROTOCOL,
|
| 203 |
-
"selection_uses_heldout_or_benchmark_data": False,
|
| 204 |
-
"inventory_manifest": input_identity,
|
| 205 |
-
"batch_size": self.batch_size,
|
| 206 |
-
"seed": self.seed,
|
| 207 |
-
"rng": "numpy-pcg64",
|
| 208 |
-
"holdout_row_groups": self.holdout_row_groups,
|
| 209 |
-
"model_vocab_size": self.model_vocab_size,
|
| 210 |
-
"records": [
|
| 211 |
-
{
|
| 212 |
-
"index": record.index,
|
| 213 |
-
"kind": record.kind,
|
| 214 |
-
"name": record.name,
|
| 215 |
-
"file_sha256": record.file_sha256,
|
| 216 |
-
"columns": record.columns,
|
| 217 |
-
"languages": record.languages,
|
| 218 |
-
"group_rows": record.group_rows,
|
| 219 |
-
"sampling_weight": record.sampling_weight,
|
| 220 |
-
}
|
| 221 |
-
for record in self._records
|
| 222 |
-
],
|
| 223 |
-
}
|
| 224 |
-
self.identity_sha256 = hashlib.sha256(
|
| 225 |
-
_canonical_json(self.identity)
|
| 226 |
-
).hexdigest()
|
| 227 |
-
self._rng = np.random.default_rng(self.seed)
|
| 228 |
-
self._positions = {
|
| 229 |
-
record.index: [0, 0]
|
| 230 |
-
for record in self._records
|
| 231 |
-
}
|
| 232 |
-
self._permutation_cache: dict[int, tuple[int, np.ndarray]] = {}
|
| 233 |
-
self.completed_batches = 0
|
| 234 |
-
|
| 235 |
-
def _validate_and_build(
|
| 236 |
-
self,
|
| 237 |
-
) -> tuple[
|
| 238 |
-
Tokenizer,
|
| 239 |
-
int,
|
| 240 |
-
int,
|
| 241 |
-
int,
|
| 242 |
-
tuple[_CorpusRecord, ...],
|
| 243 |
-
dict[str, str],
|
| 244 |
-
]:
|
| 245 |
-
inventory = _load_json(self.inventory_path, "inventory")
|
| 246 |
-
if inventory.get("protocol") != "latent-lexicon-pair-inventory-v1":
|
| 247 |
-
raise ValueError("latent stream inventory protocol mismatch")
|
| 248 |
-
if inventory.get("selection_uses_heldout_or_benchmark_data") is not False:
|
| 249 |
-
raise ValueError("latent stream inventory is not training-only")
|
| 250 |
-
inventory_hash = sha256_file(self.inventory_path)
|
| 251 |
-
|
| 252 |
-
tokenizer_record = _mapping(inventory.get("tokenizer"), "tokenizer")
|
| 253 |
-
tokenizer_path = _checked_path(tokenizer_record, "path", "sha256")
|
| 254 |
-
model_vocab_size = int(tokenizer_record.get("model_vocab_size", -1))
|
| 255 |
-
tokenizer = Tokenizer.from_file(str(tokenizer_path))
|
| 256 |
-
if (
|
| 257 |
-
model_vocab_size <= 0
|
| 258 |
-
or tokenizer.get_vocab_size(with_added_tokens=False) != model_vocab_size
|
| 259 |
-
):
|
| 260 |
-
raise ValueError("latent stream tokenizer vocabulary mismatch")
|
| 261 |
-
|
| 262 |
-
training_record = _mapping(
|
| 263 |
-
inventory.get("training_config"), "training config"
|
| 264 |
-
)
|
| 265 |
-
training_path = _checked_path(training_record, "path", "sha256")
|
| 266 |
-
training = yaml.safe_load(training_path.read_text(encoding="utf-8"))
|
| 267 |
-
if not isinstance(training, dict):
|
| 268 |
-
raise ValueError("latent stream training config must be a mapping")
|
| 269 |
-
streaming = _mapping(training.get("streaming"), "streaming settings")
|
| 270 |
-
if (
|
| 271 |
-
streaming.get("monolingual_batches") is not True
|
| 272 |
-
or streaming.get("language_markers") is not True
|
| 273 |
-
or streaming.get("batch_no_duplicates") is not True
|
| 274 |
-
):
|
| 275 |
-
raise ValueError("latent stream requires frozen monolingual marker batches")
|
| 276 |
-
|
| 277 |
-
corpus_record = _mapping(inventory.get("corpus"), "corpus")
|
| 278 |
-
corpus_manifest_path = _checked_path(
|
| 279 |
-
corpus_record, "manifest_path", "manifest_sha256"
|
| 280 |
-
)
|
| 281 |
-
corpus = _load_json(corpus_manifest_path, "corpus manifest")
|
| 282 |
-
if corpus.get("format") != 1:
|
| 283 |
-
raise ValueError("latent stream corpus format mismatch")
|
| 284 |
-
holdout_row_groups = int(corpus.get("holdout_row_groups", -1))
|
| 285 |
-
if holdout_row_groups <= 0 or int(
|
| 286 |
-
streaming.get("holdout_row_groups", -2)
|
| 287 |
-
) != holdout_row_groups:
|
| 288 |
-
raise ValueError("latent stream holdout row-group contract mismatch")
|
| 289 |
-
max_chars = int(corpus.get("max_chars", -1))
|
| 290 |
-
if max_chars <= 0 or int(streaming.get("max_chars", -2)) != max_chars:
|
| 291 |
-
raise ValueError("latent stream max_chars contract mismatch")
|
| 292 |
-
|
| 293 |
-
raw_streams = training.get("streams")
|
| 294 |
-
if not isinstance(raw_streams, list):
|
| 295 |
-
raise ValueError("latent stream config has no streams")
|
| 296 |
-
streams = {
|
| 297 |
-
str(row.get("name")): row
|
| 298 |
-
for row in raw_streams
|
| 299 |
-
if isinstance(row, Mapping) and isinstance(row.get("name"), str)
|
| 300 |
-
}
|
| 301 |
-
if "fineweb2" not in streams or "ccmatrix" not in streams:
|
| 302 |
-
raise ValueError("latent stream needs fineweb2 and ccmatrix streams")
|
| 303 |
-
|
| 304 |
-
corpus_rows = corpus.get("records")
|
| 305 |
-
raw_files = corpus_record.get("files")
|
| 306 |
-
if not isinstance(corpus_rows, list) or not isinstance(raw_files, list):
|
| 307 |
-
raise ValueError("latent stream corpus record inventory is invalid")
|
| 308 |
-
corpus_by_key = {
|
| 309 |
-
(row.get("kind"), row.get("name")): row
|
| 310 |
-
for row in corpus_rows
|
| 311 |
-
if isinstance(row, Mapping)
|
| 312 |
-
}
|
| 313 |
-
manifest_keys = [
|
| 314 |
-
(row.get("kind"), row.get("name"))
|
| 315 |
-
for row in corpus_rows
|
| 316 |
-
if isinstance(row, Mapping)
|
| 317 |
-
]
|
| 318 |
-
file_keys = [
|
| 319 |
-
(row.get("kind"), row.get("name"))
|
| 320 |
-
for row in raw_files
|
| 321 |
-
if isinstance(row, Mapping)
|
| 322 |
-
]
|
| 323 |
-
if (
|
| 324 |
-
len(manifest_keys) != len(corpus_rows)
|
| 325 |
-
or len(file_keys) != len(raw_files)
|
| 326 |
-
or len(set(manifest_keys)) != len(manifest_keys)
|
| 327 |
-
or len(set(file_keys)) != len(file_keys)
|
| 328 |
-
or set(file_keys) != set(manifest_keys)
|
| 329 |
-
):
|
| 330 |
-
raise ValueError("latent stream corpus file inventory is not one-to-one")
|
| 331 |
-
prepared: list[dict[str, object]] = []
|
| 332 |
-
family_indices: dict[str, list[int]] = {"fineweb": [], "parallel": []}
|
| 333 |
-
for index, raw_file in enumerate(raw_files):
|
| 334 |
-
file_record = _mapping(raw_file, "corpus file")
|
| 335 |
-
kind = file_record.get("kind")
|
| 336 |
-
name = file_record.get("name")
|
| 337 |
-
if kind not in family_indices or not isinstance(name, str):
|
| 338 |
-
raise ValueError("latent stream corpus kind/name is invalid")
|
| 339 |
-
source = corpus_by_key.get((kind, name))
|
| 340 |
-
if not isinstance(source, Mapping):
|
| 341 |
-
raise ValueError(f"latent stream corpus record is absent: {kind}/{name}")
|
| 342 |
-
path = _checked_path(file_record, "path", "sha256")
|
| 343 |
-
output = source.get("output")
|
| 344 |
-
if (
|
| 345 |
-
not isinstance(output, str)
|
| 346 |
-
or path != (corpus_manifest_path.parent / output).resolve()
|
| 347 |
-
or source.get("sha256") != file_record.get("sha256")
|
| 348 |
-
):
|
| 349 |
-
raise ValueError(f"latent stream corpus identity differs: {kind}/{name}")
|
| 350 |
-
train_groups = int(file_record.get("train_row_groups", -1))
|
| 351 |
-
if train_groups != int(source.get("train_row_groups", -2)):
|
| 352 |
-
raise ValueError(f"latent stream train groups differ: {kind}/{name}")
|
| 353 |
-
heldout_rows = source.get("heldout_rows")
|
| 354 |
-
if not isinstance(heldout_rows, list) or len(heldout_rows) != holdout_row_groups:
|
| 355 |
-
raise ValueError(f"latent stream heldout inventory differs: {kind}/{name}")
|
| 356 |
-
parquet = pq.ParquetFile(path)
|
| 357 |
-
if parquet.num_row_groups != train_groups + holdout_row_groups:
|
| 358 |
-
raise ValueError(f"latent stream Parquet groups differ: {kind}/{name}")
|
| 359 |
-
group_rows = tuple(
|
| 360 |
-
int(parquet.metadata.row_group(group).num_rows)
|
| 361 |
-
for group in range(train_groups)
|
| 362 |
-
)
|
| 363 |
-
if not group_rows or any(rows <= 0 for rows in group_rows):
|
| 364 |
-
raise ValueError(f"latent stream training groups are empty: {kind}/{name}")
|
| 365 |
-
rows = sum(group_rows)
|
| 366 |
-
if rows != int(file_record.get("rows", -1)) or rows != int(
|
| 367 |
-
source.get("train_rows", -2)
|
| 368 |
-
):
|
| 369 |
-
raise ValueError(f"latent stream row count differs: {kind}/{name}")
|
| 370 |
-
|
| 371 |
-
if kind == "fineweb":
|
| 372 |
-
columns = ("sentence_1", "sentence_2")
|
| 373 |
-
languages = (name, name)
|
| 374 |
-
stream_name = "fineweb2"
|
| 375 |
-
else:
|
| 376 |
-
parallel = streams["ccmatrix"]
|
| 377 |
-
language_map = _mapping(
|
| 378 |
-
parallel.get("language_map"), "parallel language map"
|
| 379 |
-
)
|
| 380 |
-
anchor_language = parallel.get("anchor_language")
|
| 381 |
-
positive_language = language_map.get(name)
|
| 382 |
-
if not isinstance(anchor_language, str) or not isinstance(
|
| 383 |
-
positive_language, str
|
| 384 |
-
):
|
| 385 |
-
raise ValueError(f"latent stream language map lacks {name}")
|
| 386 |
-
columns = ("english", "non_english")
|
| 387 |
-
languages = (anchor_language, positive_language)
|
| 388 |
-
stream_name = "ccmatrix"
|
| 389 |
-
if any(
|
| 390 |
-
tokenizer.token_to_id(f"__{language}__") is None
|
| 391 |
-
for language in languages
|
| 392 |
-
):
|
| 393 |
-
raise ValueError(f"latent stream tokenizer lacks marker for {name}")
|
| 394 |
-
offsets = [0]
|
| 395 |
-
for count in group_rows:
|
| 396 |
-
offsets.append(offsets[-1] + count)
|
| 397 |
-
prepared.append({
|
| 398 |
-
"index": index,
|
| 399 |
-
"kind": kind,
|
| 400 |
-
"name": name,
|
| 401 |
-
"path": path,
|
| 402 |
-
"file_sha256": str(file_record["sha256"]),
|
| 403 |
-
"columns": columns,
|
| 404 |
-
"languages": languages,
|
| 405 |
-
"group_rows": group_rows,
|
| 406 |
-
"group_offsets": tuple(offsets),
|
| 407 |
-
"stream_name": stream_name,
|
| 408 |
-
"rows": rows,
|
| 409 |
-
})
|
| 410 |
-
family_indices[kind].append(index)
|
| 411 |
-
|
| 412 |
-
weights = [0.0] * len(prepared)
|
| 413 |
-
for kind, indices in family_indices.items():
|
| 414 |
-
if not indices:
|
| 415 |
-
continue
|
| 416 |
-
stream_name = "fineweb2" if kind == "fineweb" else "ccmatrix"
|
| 417 |
-
stream = streams[stream_name]
|
| 418 |
-
family_weight = float(stream.get("weight", 1.0))
|
| 419 |
-
sampling = _mapping(
|
| 420 |
-
stream.get("language_sampling", {}),
|
| 421 |
-
f"{stream_name} language sampling",
|
| 422 |
-
)
|
| 423 |
-
temperature = float(sampling.get("temperature", 1.0))
|
| 424 |
-
if not math.isfinite(family_weight) or not math.isfinite(temperature):
|
| 425 |
-
raise ValueError("latent stream sampling weights are nonfinite")
|
| 426 |
-
masses = [float(prepared[index]["rows"]) ** temperature for index in indices]
|
| 427 |
-
total = sum(masses)
|
| 428 |
-
if family_weight < 0 or total <= 0:
|
| 429 |
-
raise ValueError("latent stream sampling weights are invalid")
|
| 430 |
-
for index, mass in zip(indices, masses, strict=True):
|
| 431 |
-
weights[index] = family_weight * mass / total
|
| 432 |
-
if not any(weight > 0 for weight in weights):
|
| 433 |
-
raise ValueError("latent stream has no positive sampling weight")
|
| 434 |
-
|
| 435 |
-
records = tuple(
|
| 436 |
-
_CorpusRecord(
|
| 437 |
-
index=int(row["index"]),
|
| 438 |
-
kind=str(row["kind"]),
|
| 439 |
-
name=str(row["name"]),
|
| 440 |
-
path=Path(row["path"]),
|
| 441 |
-
file_sha256=str(row["file_sha256"]),
|
| 442 |
-
columns=tuple(row["columns"]), # type: ignore[arg-type]
|
| 443 |
-
languages=tuple(row["languages"]), # type: ignore[arg-type]
|
| 444 |
-
group_rows=tuple(row["group_rows"]), # type: ignore[arg-type]
|
| 445 |
-
group_offsets=tuple(row["group_offsets"]), # type: ignore[arg-type]
|
| 446 |
-
sampling_weight=weights[int(row["index"])],
|
| 447 |
-
)
|
| 448 |
-
for row in prepared
|
| 449 |
-
)
|
| 450 |
-
return (
|
| 451 |
-
tokenizer,
|
| 452 |
-
model_vocab_size,
|
| 453 |
-
max_chars,
|
| 454 |
-
holdout_row_groups,
|
| 455 |
-
records,
|
| 456 |
-
{"path": str(self.inventory_path), "sha256": inventory_hash},
|
| 457 |
-
)
|
| 458 |
-
|
| 459 |
-
def _permutation(self, record: int, epoch: int) -> np.ndarray:
|
| 460 |
-
cached = self._permutation_cache.get(record)
|
| 461 |
-
if cached is None or cached[0] != epoch:
|
| 462 |
-
count = self._records[record].group_offsets[-1]
|
| 463 |
-
seed = np.random.SeedSequence([self.seed, record, epoch])
|
| 464 |
-
cached = (epoch, np.random.default_rng(seed).permutation(count))
|
| 465 |
-
self._permutation_cache[record] = cached
|
| 466 |
-
return cached[1]
|
| 467 |
-
|
| 468 |
-
def _take_flat_row(self, record: int) -> tuple[int, int]:
|
| 469 |
-
epoch, cursor = self._positions[record]
|
| 470 |
-
count = self._records[record].group_offsets[-1]
|
| 471 |
-
if cursor >= count:
|
| 472 |
-
epoch += 1
|
| 473 |
-
cursor = 0
|
| 474 |
-
flat_row = int(self._permutation(record, epoch)[cursor])
|
| 475 |
-
self._positions[record] = [epoch, cursor + 1]
|
| 476 |
-
return flat_row, epoch
|
| 477 |
-
|
| 478 |
-
def _encode(
|
| 479 |
-
self, language: str, texts: Sequence[str]
|
| 480 |
-
) -> tuple[tuple[int, ...], ...]:
|
| 481 |
-
marker = self.tokenizer.token_to_id(f"__{language}__")
|
| 482 |
-
assert marker is not None
|
| 483 |
-
encoded = self.tokenizer.encode_batch(
|
| 484 |
-
[f"__{language}__ {text}" for text in texts],
|
| 485 |
-
add_special_tokens=False,
|
| 486 |
-
)
|
| 487 |
-
rows: list[tuple[int, ...]] = []
|
| 488 |
-
for item in encoded:
|
| 489 |
-
if not item.ids or item.ids[0] != marker:
|
| 490 |
-
raise ValueError(f"latent stream marker failed for {language}")
|
| 491 |
-
content = tuple(int(token) for token in item.ids[1:])
|
| 492 |
-
if (
|
| 493 |
-
not content
|
| 494 |
-
or min(content) < 0
|
| 495 |
-
or max(content) >= self.model_vocab_size
|
| 496 |
-
):
|
| 497 |
-
raise ValueError("latent stream content leaves model vocabulary")
|
| 498 |
-
rows.append(content)
|
| 499 |
-
return tuple(rows)
|
| 500 |
-
|
| 501 |
-
def next_batch(self) -> StreamingLatentBatch:
|
| 502 |
-
record_index = int(
|
| 503 |
-
self._rng.choice(len(self._records), p=self._record_probabilities)
|
| 504 |
-
)
|
| 505 |
-
record = self._records[record_index]
|
| 506 |
-
parquet = pq.ParquetFile(record.path)
|
| 507 |
-
read_columns = (
|
| 508 |
-
("text_id", *record.columns)
|
| 509 |
-
if record.kind == "fineweb"
|
| 510 |
-
else record.columns
|
| 511 |
-
)
|
| 512 |
-
tables: dict[int, dict[str, list[object]]] = {}
|
| 513 |
-
anchors: list[str] = []
|
| 514 |
-
positives: list[str] = []
|
| 515 |
-
provenance: list[StreamRowProvenance] = []
|
| 516 |
-
seen: set[str] = set()
|
| 517 |
-
attempts = 0
|
| 518 |
-
attempt_limit = max(record.group_offsets[-1] * 2, self.batch_size * 40)
|
| 519 |
-
while len(anchors) < self.batch_size and attempts < attempt_limit:
|
| 520 |
-
attempts += 1
|
| 521 |
-
flat_row, reuse_cycle = self._take_flat_row(record_index)
|
| 522 |
-
group = bisect.bisect_right(record.group_offsets, flat_row) - 1
|
| 523 |
-
row_index = flat_row - record.group_offsets[group]
|
| 524 |
-
if group not in tables:
|
| 525 |
-
tables[group] = parquet.read_row_group(
|
| 526 |
-
group, columns=list(read_columns), use_threads=False
|
| 527 |
-
).to_pydict()
|
| 528 |
-
table = tables[group]
|
| 529 |
-
anchor = table[record.columns[0]][row_index]
|
| 530 |
-
positive = table[record.columns[1]][row_index]
|
| 531 |
-
if not isinstance(anchor, str) or not isinstance(positive, str):
|
| 532 |
-
continue
|
| 533 |
-
anchor = anchor.strip()
|
| 534 |
-
positive = positive.strip()
|
| 535 |
-
keys = (_canonical_text(anchor), _canonical_text(positive))
|
| 536 |
-
if (
|
| 537 |
-
not anchor
|
| 538 |
-
or not positive
|
| 539 |
-
or len(anchor) > self.max_chars
|
| 540 |
-
or len(positive) > self.max_chars
|
| 541 |
-
or keys[0] == keys[1]
|
| 542 |
-
or any(key in seen for key in keys)
|
| 543 |
-
):
|
| 544 |
-
continue
|
| 545 |
-
if record.kind == "fineweb":
|
| 546 |
-
identity = table["text_id"][row_index]
|
| 547 |
-
if not isinstance(identity, str) or not identity:
|
| 548 |
-
continue
|
| 549 |
-
else:
|
| 550 |
-
identity = hashlib.sha256(
|
| 551 |
-
(anchor + "\0" + positive).encode("utf-8")
|
| 552 |
-
).hexdigest()
|
| 553 |
-
seen.update(keys)
|
| 554 |
-
anchors.append(anchor)
|
| 555 |
-
positives.append(positive)
|
| 556 |
-
provenance.append(StreamRowProvenance(
|
| 557 |
-
record_index=record.index,
|
| 558 |
-
record_kind=record.kind,
|
| 559 |
-
record_name=record.name,
|
| 560 |
-
file_sha256=record.file_sha256,
|
| 561 |
-
row_group=group,
|
| 562 |
-
row_index=row_index,
|
| 563 |
-
flat_row=flat_row,
|
| 564 |
-
reuse_cycle=reuse_cycle,
|
| 565 |
-
identity=identity,
|
| 566 |
-
pair_sha256=hashlib.sha256(
|
| 567 |
-
(anchor + "\0" + positive).encode("utf-8")
|
| 568 |
-
).hexdigest(),
|
| 569 |
-
anchor_language=record.languages[0],
|
| 570 |
-
positive_language=record.languages[1],
|
| 571 |
-
))
|
| 572 |
-
if len(anchors) != self.batch_size:
|
| 573 |
-
raise RuntimeError(
|
| 574 |
-
f"latent stream could not form a unique batch from {record.name}"
|
| 575 |
-
)
|
| 576 |
-
anchor_ids = self._encode(record.languages[0], anchors)
|
| 577 |
-
positive_ids = self._encode(record.languages[1], positives)
|
| 578 |
-
step = self.completed_batches + 1
|
| 579 |
-
incomplete = StreamingLatentBatch(
|
| 580 |
-
step=step,
|
| 581 |
-
record_name=record.name,
|
| 582 |
-
anchors=anchor_ids,
|
| 583 |
-
positives=positive_ids,
|
| 584 |
-
rows=tuple(provenance),
|
| 585 |
-
sha256="",
|
| 586 |
-
)
|
| 587 |
-
batch = StreamingLatentBatch(
|
| 588 |
-
step=incomplete.step,
|
| 589 |
-
record_name=incomplete.record_name,
|
| 590 |
-
anchors=incomplete.anchors,
|
| 591 |
-
positives=incomplete.positives,
|
| 592 |
-
rows=incomplete.rows,
|
| 593 |
-
sha256=incomplete.recompute_sha256(),
|
| 594 |
-
)
|
| 595 |
-
self.completed_batches = step
|
| 596 |
-
return batch
|
| 597 |
-
|
| 598 |
-
def state_dict(self) -> dict[str, object]:
|
| 599 |
-
return {
|
| 600 |
-
"format_version": STATE_FORMAT,
|
| 601 |
-
"identity_sha256": self.identity_sha256,
|
| 602 |
-
"completed_batches": self.completed_batches,
|
| 603 |
-
"rng_state": _json_copy(self._rng.bit_generator.state),
|
| 604 |
-
"positions": [
|
| 605 |
-
{
|
| 606 |
-
"record": record,
|
| 607 |
-
"epoch": value[0],
|
| 608 |
-
"cursor": value[1],
|
| 609 |
-
}
|
| 610 |
-
for record, value in sorted(self._positions.items())
|
| 611 |
-
],
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
def validate_state_dict(self, state: object) -> dict[str, object]:
|
| 615 |
-
if not isinstance(state, dict) or set(state) != {
|
| 616 |
-
"format_version",
|
| 617 |
-
"identity_sha256",
|
| 618 |
-
"completed_batches",
|
| 619 |
-
"rng_state",
|
| 620 |
-
"positions",
|
| 621 |
-
}:
|
| 622 |
-
raise ValueError("latent stream state keys are invalid")
|
| 623 |
-
if state["format_version"] != STATE_FORMAT:
|
| 624 |
-
raise ValueError("latent stream state format differs")
|
| 625 |
-
if state["identity_sha256"] != self.identity_sha256:
|
| 626 |
-
raise ValueError("latent stream state identity differs")
|
| 627 |
-
completed = state["completed_batches"]
|
| 628 |
-
if isinstance(completed, bool) or not isinstance(completed, int) or completed < 0:
|
| 629 |
-
raise ValueError("latent stream completed batch count is invalid")
|
| 630 |
-
positions = state["positions"]
|
| 631 |
-
expected_keys = set(self._positions)
|
| 632 |
-
observed: dict[int, list[int]] = {}
|
| 633 |
-
if not isinstance(positions, list):
|
| 634 |
-
raise ValueError("latent stream positions are invalid")
|
| 635 |
-
for row in positions:
|
| 636 |
-
if not isinstance(row, dict) or set(row) != {
|
| 637 |
-
"record", "epoch", "cursor"
|
| 638 |
-
}:
|
| 639 |
-
raise ValueError("latent stream position row is invalid")
|
| 640 |
-
values = tuple(row[key] for key in ("record", "epoch", "cursor"))
|
| 641 |
-
if any(isinstance(value, bool) or not isinstance(value, int) for value in values):
|
| 642 |
-
raise ValueError("latent stream position value is invalid")
|
| 643 |
-
record, epoch, cursor = values
|
| 644 |
-
if record not in expected_keys or record in observed or epoch < 0:
|
| 645 |
-
raise ValueError("latent stream position identity is invalid")
|
| 646 |
-
if not 0 <= cursor <= self._records[record].group_offsets[-1]:
|
| 647 |
-
raise ValueError("latent stream position cursor is invalid")
|
| 648 |
-
observed[record] = [epoch, cursor]
|
| 649 |
-
if set(observed) != expected_keys:
|
| 650 |
-
raise ValueError("latent stream position inventory differs")
|
| 651 |
-
rng_state = state["rng_state"]
|
| 652 |
-
probe = np.random.default_rng()
|
| 653 |
-
try:
|
| 654 |
-
probe.bit_generator.state = rng_state # type: ignore[assignment]
|
| 655 |
-
except (TypeError, ValueError) as error:
|
| 656 |
-
raise ValueError("latent stream RNG state is invalid") from error
|
| 657 |
-
return {
|
| 658 |
-
"format_version": STATE_FORMAT,
|
| 659 |
-
"identity_sha256": self.identity_sha256,
|
| 660 |
-
"completed_batches": completed,
|
| 661 |
-
"rng_state": _json_copy(rng_state),
|
| 662 |
-
"positions": positions,
|
| 663 |
-
"validated_positions": observed,
|
| 664 |
-
}
|
| 665 |
-
|
| 666 |
-
def load_state_dict(self, state: object) -> None:
|
| 667 |
-
validated = self.validate_state_dict(state)
|
| 668 |
-
self._rng.bit_generator.state = validated["rng_state"] # type: ignore[assignment]
|
| 669 |
-
self._positions = {
|
| 670 |
-
key: value.copy()
|
| 671 |
-
for key, value in validated["validated_positions"].items() # type: ignore[union-attr]
|
| 672 |
-
}
|
| 673 |
-
self._permutation_cache.clear()
|
| 674 |
-
self.completed_batches = int(validated["completed_batches"])
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
def _atomic_json(path: Path, payload: object) -> None:
|
| 678 |
-
path.parent.mkdir(parents=True, exist_ok=True)
|
| 679 |
-
temporary_path: Path | None = None
|
| 680 |
-
try:
|
| 681 |
-
with tempfile.NamedTemporaryFile(
|
| 682 |
-
mode="w",
|
| 683 |
-
encoding="utf-8",
|
| 684 |
-
dir=path.parent,
|
| 685 |
-
prefix=f".{path.name}.tmp-",
|
| 686 |
-
delete=False,
|
| 687 |
-
) as handle:
|
| 688 |
-
temporary_path = Path(handle.name)
|
| 689 |
-
json.dump(payload, handle, indent=2, sort_keys=True)
|
| 690 |
-
handle.write("\n")
|
| 691 |
-
handle.flush()
|
| 692 |
-
os.fsync(handle.fileno())
|
| 693 |
-
os.replace(temporary_path, path)
|
| 694 |
-
temporary_path = None
|
| 695 |
-
finally:
|
| 696 |
-
if temporary_path is not None:
|
| 697 |
-
temporary_path.unlink(missing_ok=True)
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
def _safe_child(root: Path, name: object, label: str) -> Path:
|
| 701 |
-
if not isinstance(name, str) or not name or Path(name).name != name:
|
| 702 |
-
raise ValueError(f"latent stream checkpoint {label} filename is invalid")
|
| 703 |
-
return root / name
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
def _read_latest(root: Path) -> tuple[dict[str, object], Path, dict[str, object]]:
|
| 707 |
-
pointer = _load_json(root / "latest.json", "latest checkpoint pointer")
|
| 708 |
-
if (
|
| 709 |
-
set(pointer) != {"protocol", "manifest", "manifest_sha256"}
|
| 710 |
-
or pointer.get("protocol") != POINTER_PROTOCOL
|
| 711 |
-
):
|
| 712 |
-
raise ValueError("latent stream latest checkpoint pointer is invalid")
|
| 713 |
-
manifest_path = _safe_child(root, pointer.get("manifest"), "manifest")
|
| 714 |
-
expected_hash = pointer.get("manifest_sha256")
|
| 715 |
-
if not isinstance(expected_hash, str) or sha256_file(manifest_path) != expected_hash:
|
| 716 |
-
raise ValueError("latent stream checkpoint manifest hash mismatch")
|
| 717 |
-
return pointer, manifest_path, _load_json(manifest_path, "checkpoint manifest")
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
def _validated_provenance(
|
| 721 |
-
provenance: Sequence[Mapping[str, object]],
|
| 722 |
-
*,
|
| 723 |
-
first_step: int,
|
| 724 |
-
completed_steps: int,
|
| 725 |
-
) -> list[dict[str, object]]:
|
| 726 |
-
expected_steps = list(range(first_step, completed_steps + 1))
|
| 727 |
-
if len(provenance) != len(expected_steps):
|
| 728 |
-
raise ValueError("latent stream checkpoint provenance interval differs")
|
| 729 |
-
copied: list[dict[str, object]] = []
|
| 730 |
-
for expected, row in zip(expected_steps, provenance, strict=True):
|
| 731 |
-
required = {
|
| 732 |
-
"step", "record_name", "batch_size", "sha256",
|
| 733 |
-
"anchors", "positives", "rows",
|
| 734 |
-
}
|
| 735 |
-
if (
|
| 736 |
-
not isinstance(row, Mapping)
|
| 737 |
-
or set(row) != required
|
| 738 |
-
or row.get("step") != expected
|
| 739 |
-
):
|
| 740 |
-
raise ValueError("latent stream checkpoint provenance is noncontiguous")
|
| 741 |
-
digest = row.get("sha256")
|
| 742 |
-
record_name = row.get("record_name")
|
| 743 |
-
anchors = row.get("anchors")
|
| 744 |
-
positives = row.get("positives")
|
| 745 |
-
rows = row.get("rows")
|
| 746 |
-
batch_size = row.get("batch_size")
|
| 747 |
-
if (
|
| 748 |
-
not isinstance(digest, str)
|
| 749 |
-
or len(digest) != 64
|
| 750 |
-
or not isinstance(record_name, str)
|
| 751 |
-
or not record_name
|
| 752 |
-
or not isinstance(anchors, (list, tuple))
|
| 753 |
-
or not isinstance(positives, (list, tuple))
|
| 754 |
-
or not isinstance(rows, list)
|
| 755 |
-
or isinstance(batch_size, bool)
|
| 756 |
-
or not isinstance(batch_size, int)
|
| 757 |
-
or batch_size <= 1
|
| 758 |
-
or len(rows) != batch_size
|
| 759 |
-
or len(anchors) != batch_size
|
| 760 |
-
or len(positives) != batch_size
|
| 761 |
-
):
|
| 762 |
-
raise ValueError("latent stream checkpoint provenance row is invalid")
|
| 763 |
-
for sequences in (anchors, positives):
|
| 764 |
-
if any(
|
| 765 |
-
not isinstance(sequence, (list, tuple))
|
| 766 |
-
or not sequence
|
| 767 |
-
or any(
|
| 768 |
-
isinstance(token, bool) or not isinstance(token, int) or token < 0
|
| 769 |
-
for token in sequence
|
| 770 |
-
)
|
| 771 |
-
for sequence in sequences
|
| 772 |
-
):
|
| 773 |
-
raise ValueError("latent stream checkpoint token provenance is invalid")
|
| 774 |
-
row_fields = {
|
| 775 |
-
"record_index", "record_kind", "record_name", "file_sha256",
|
| 776 |
-
"row_group", "row_index", "flat_row", "reuse_cycle", "identity",
|
| 777 |
-
"pair_sha256", "anchor_language", "positive_language",
|
| 778 |
-
}
|
| 779 |
-
for source in rows:
|
| 780 |
-
if not isinstance(source, Mapping) or set(source) != row_fields:
|
| 781 |
-
raise ValueError("latent stream checkpoint row provenance is invalid")
|
| 782 |
-
nonnegative = (
|
| 783 |
-
source.get("record_index"), source.get("row_group"),
|
| 784 |
-
source.get("row_index"), source.get("flat_row"),
|
| 785 |
-
source.get("reuse_cycle"),
|
| 786 |
-
)
|
| 787 |
-
strings = (
|
| 788 |
-
source.get("record_kind"), source.get("record_name"),
|
| 789 |
-
source.get("identity"), source.get("anchor_language"),
|
| 790 |
-
source.get("positive_language"),
|
| 791 |
-
)
|
| 792 |
-
if (
|
| 793 |
-
any(isinstance(value, bool) or not isinstance(value, int) or value < 0
|
| 794 |
-
for value in nonnegative)
|
| 795 |
-
or any(not isinstance(value, str) or not value for value in strings)
|
| 796 |
-
or source.get("record_name") != record_name
|
| 797 |
-
or not isinstance(source.get("file_sha256"), str)
|
| 798 |
-
or len(source["file_sha256"]) != 64
|
| 799 |
-
or not isinstance(source.get("pair_sha256"), str)
|
| 800 |
-
or len(source["pair_sha256"]) != 64
|
| 801 |
-
):
|
| 802 |
-
raise ValueError("latent stream checkpoint row provenance is invalid")
|
| 803 |
-
recomputed = hashlib.sha256(_canonical_json({
|
| 804 |
-
"step": expected,
|
| 805 |
-
"record_name": record_name,
|
| 806 |
-
"anchors": anchors,
|
| 807 |
-
"positives": positives,
|
| 808 |
-
"rows": rows,
|
| 809 |
-
})).hexdigest()
|
| 810 |
-
if digest != recomputed:
|
| 811 |
-
raise ValueError("latent stream checkpoint provenance digest differs")
|
| 812 |
-
copied.append(dict(_json_copy(dict(row))))
|
| 813 |
-
return copied
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
_CHECKPOINT_MANIFEST_KEYS = {
|
| 817 |
-
"protocol",
|
| 818 |
-
"completed_steps",
|
| 819 |
-
"selection_uses_heldout_or_benchmark_data",
|
| 820 |
-
"stream_identity",
|
| 821 |
-
"stream_identity_sha256",
|
| 822 |
-
"stream_state",
|
| 823 |
-
"model_checkpoint",
|
| 824 |
-
"provenance_first_step",
|
| 825 |
-
"provenance",
|
| 826 |
-
"parent",
|
| 827 |
-
}
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
def audit_streaming_checkpoint_chain(root: str | Path) -> dict[str, object]:
|
| 831 |
-
"""Verify every immutable provenance segment back to step one."""
|
| 832 |
-
root_path = Path(root)
|
| 833 |
-
_, manifest_path, manifest = _read_latest(root_path)
|
| 834 |
-
seen: set[str] = set()
|
| 835 |
-
newest: dict[str, object] | None = None
|
| 836 |
-
expected_end: int | None = None
|
| 837 |
-
chain_identity: str | None = None
|
| 838 |
-
segments = 0
|
| 839 |
-
rows = 0
|
| 840 |
-
while True:
|
| 841 |
-
if manifest_path.name in seen:
|
| 842 |
-
raise ValueError("latent stream checkpoint parent chain has a cycle")
|
| 843 |
-
seen.add(manifest_path.name)
|
| 844 |
-
if set(manifest) != _CHECKPOINT_MANIFEST_KEYS or manifest.get(
|
| 845 |
-
"protocol"
|
| 846 |
-
) != CHECKPOINT_PROTOCOL:
|
| 847 |
-
raise ValueError("latent stream checkpoint manifest is invalid")
|
| 848 |
-
if manifest.get("selection_uses_heldout_or_benchmark_data") is not False:
|
| 849 |
-
raise ValueError("latent stream checkpoint is not training-only")
|
| 850 |
-
completed = manifest.get("completed_steps")
|
| 851 |
-
first = manifest.get("provenance_first_step")
|
| 852 |
-
provenance = manifest.get("provenance")
|
| 853 |
-
if (
|
| 854 |
-
isinstance(completed, bool)
|
| 855 |
-
or not isinstance(completed, int)
|
| 856 |
-
or completed <= 0
|
| 857 |
-
or isinstance(first, bool)
|
| 858 |
-
or not isinstance(first, int)
|
| 859 |
-
or first <= 0
|
| 860 |
-
or first > completed
|
| 861 |
-
or not isinstance(provenance, list)
|
| 862 |
-
or (expected_end is not None and completed != expected_end)
|
| 863 |
-
):
|
| 864 |
-
raise ValueError("latent stream checkpoint chain steps differ")
|
| 865 |
-
checked = _validated_provenance(
|
| 866 |
-
provenance, first_step=first, completed_steps=completed
|
| 867 |
-
)
|
| 868 |
-
saved_identity = manifest.get("stream_identity")
|
| 869 |
-
saved_identity_hash = manifest.get("stream_identity_sha256")
|
| 870 |
-
state = manifest.get("stream_state")
|
| 871 |
-
if (
|
| 872 |
-
not isinstance(saved_identity, Mapping)
|
| 873 |
-
or not isinstance(saved_identity_hash, str)
|
| 874 |
-
or hashlib.sha256(_canonical_json(saved_identity)).hexdigest()
|
| 875 |
-
!= saved_identity_hash
|
| 876 |
-
or not isinstance(state, Mapping)
|
| 877 |
-
or state.get("identity_sha256") != saved_identity_hash
|
| 878 |
-
or state.get("completed_batches") != completed
|
| 879 |
-
):
|
| 880 |
-
raise ValueError("latent stream checkpoint chain identity differs")
|
| 881 |
-
if chain_identity is None:
|
| 882 |
-
chain_identity = saved_identity_hash
|
| 883 |
-
elif saved_identity_hash != chain_identity:
|
| 884 |
-
raise ValueError("latent stream checkpoint parent identity differs")
|
| 885 |
-
if newest is None:
|
| 886 |
-
newest = manifest
|
| 887 |
-
segments += 1
|
| 888 |
-
rows += sum(int(record["batch_size"]) for record in checked)
|
| 889 |
-
parent = manifest.get("parent")
|
| 890 |
-
if parent is None:
|
| 891 |
-
if first != 1:
|
| 892 |
-
raise ValueError("latent stream checkpoint chain does not reach step one")
|
| 893 |
-
break
|
| 894 |
-
parent_record = _mapping(parent, "checkpoint parent")
|
| 895 |
-
if set(parent_record) != {"manifest", "manifest_sha256"}:
|
| 896 |
-
raise ValueError("latent stream checkpoint parent record is invalid")
|
| 897 |
-
manifest_path = _safe_child(
|
| 898 |
-
root_path, parent_record.get("manifest"), "parent manifest"
|
| 899 |
-
)
|
| 900 |
-
parent_hash = parent_record.get("manifest_sha256")
|
| 901 |
-
if not isinstance(parent_hash, str) or sha256_file(manifest_path) != parent_hash:
|
| 902 |
-
raise ValueError("latent stream checkpoint parent hash mismatch")
|
| 903 |
-
expected_end = first - 1
|
| 904 |
-
manifest = _load_json(manifest_path, "parent checkpoint manifest")
|
| 905 |
-
assert newest is not None
|
| 906 |
-
return {
|
| 907 |
-
"latest_completed_steps": newest["completed_steps"],
|
| 908 |
-
"segments": segments,
|
| 909 |
-
"provenance_rows": rows,
|
| 910 |
-
"latest_manifest": newest,
|
| 911 |
-
}
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
def save_streaming_latent_checkpoint(
|
| 915 |
-
root: str | Path,
|
| 916 |
-
model: LatentLexiconModel,
|
| 917 |
-
optimizers: LatentOptimizers,
|
| 918 |
-
generator: torch.Generator,
|
| 919 |
-
stream: InventoryLatentBatchStream,
|
| 920 |
-
*,
|
| 921 |
-
completed_steps: int,
|
| 922 |
-
provenance: Sequence[Mapping[str, object]],
|
| 923 |
-
) -> dict[str, object]:
|
| 924 |
-
"""Atomically publish a model/sampler/corpus-cursor checkpoint generation."""
|
| 925 |
-
if not isinstance(generator, torch.Generator):
|
| 926 |
-
raise ValueError("latent stream checkpoint generator must be torch.Generator")
|
| 927 |
-
if (
|
| 928 |
-
isinstance(completed_steps, bool)
|
| 929 |
-
or not isinstance(completed_steps, int)
|
| 930 |
-
or completed_steps <= 0
|
| 931 |
-
or completed_steps != stream.completed_batches
|
| 932 |
-
):
|
| 933 |
-
raise ValueError("latent stream checkpoint step differs from stream cursor")
|
| 934 |
-
root_path = Path(root)
|
| 935 |
-
root_path.mkdir(parents=True, exist_ok=True)
|
| 936 |
-
parent: dict[str, object] | None = None
|
| 937 |
-
parent_steps = 0
|
| 938 |
-
if (root_path / "latest.json").exists():
|
| 939 |
-
audit = audit_streaming_checkpoint_chain(root_path)
|
| 940 |
-
latest = audit["latest_manifest"]
|
| 941 |
-
if (
|
| 942 |
-
not isinstance(latest, Mapping)
|
| 943 |
-
or latest.get("stream_identity_sha256") != stream.identity_sha256
|
| 944 |
-
):
|
| 945 |
-
raise ValueError("latent stream checkpoint parent identity differs")
|
| 946 |
-
pointer, parent_path, parent_manifest = _read_latest(root_path)
|
| 947 |
-
parent_steps = parent_manifest.get("completed_steps") # type: ignore[assignment]
|
| 948 |
-
if (
|
| 949 |
-
isinstance(parent_steps, bool)
|
| 950 |
-
or not isinstance(parent_steps, int)
|
| 951 |
-
or parent_steps <= 0
|
| 952 |
-
or parent_steps >= completed_steps
|
| 953 |
-
):
|
| 954 |
-
raise ValueError("latent stream checkpoint parent step is invalid")
|
| 955 |
-
parent = {
|
| 956 |
-
"manifest": parent_path.name,
|
| 957 |
-
"manifest_sha256": pointer["manifest_sha256"],
|
| 958 |
-
}
|
| 959 |
-
provenance_rows = _validated_provenance(
|
| 960 |
-
provenance,
|
| 961 |
-
first_step=parent_steps + 1,
|
| 962 |
-
completed_steps=completed_steps,
|
| 963 |
-
)
|
| 964 |
-
|
| 965 |
-
model_name = f"model-step-{completed_steps:08d}.pt"
|
| 966 |
-
model_path = root_path / model_name
|
| 967 |
-
save_latent_checkpoint(
|
| 968 |
-
model_path,
|
| 969 |
-
model,
|
| 970 |
-
optimizers,
|
| 971 |
-
generator,
|
| 972 |
-
completed_steps=completed_steps,
|
| 973 |
-
)
|
| 974 |
-
manifest: dict[str, object] = {
|
| 975 |
-
"protocol": CHECKPOINT_PROTOCOL,
|
| 976 |
-
"completed_steps": completed_steps,
|
| 977 |
-
"selection_uses_heldout_or_benchmark_data": False,
|
| 978 |
-
"stream_identity": stream.identity,
|
| 979 |
-
"stream_identity_sha256": stream.identity_sha256,
|
| 980 |
-
"stream_state": stream.state_dict(),
|
| 981 |
-
"model_checkpoint": {
|
| 982 |
-
"file": model_name,
|
| 983 |
-
"sha256": sha256_file(model_path),
|
| 984 |
-
"bytes": model_path.stat().st_size,
|
| 985 |
-
},
|
| 986 |
-
"provenance_first_step": parent_steps + 1,
|
| 987 |
-
"provenance": provenance_rows,
|
| 988 |
-
"parent": parent,
|
| 989 |
-
}
|
| 990 |
-
manifest_name = f"stream-step-{completed_steps:08d}.json"
|
| 991 |
-
manifest_path = root_path / manifest_name
|
| 992 |
-
_atomic_json(manifest_path, manifest)
|
| 993 |
-
_atomic_json(root_path / "latest.json", {
|
| 994 |
-
"protocol": POINTER_PROTOCOL,
|
| 995 |
-
"manifest": manifest_name,
|
| 996 |
-
"manifest_sha256": sha256_file(manifest_path),
|
| 997 |
-
})
|
| 998 |
-
return manifest
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
def load_latest_streaming_latent_checkpoint(
|
| 1002 |
-
root: str | Path,
|
| 1003 |
-
model: LatentLexiconModel,
|
| 1004 |
-
optimizers: LatentOptimizers,
|
| 1005 |
-
generator: torch.Generator,
|
| 1006 |
-
stream: InventoryLatentBatchStream,
|
| 1007 |
-
) -> dict[str, object]:
|
| 1008 |
-
"""Restore only a fully published generation after fail-closed validation."""
|
| 1009 |
-
if not isinstance(generator, torch.Generator):
|
| 1010 |
-
raise ValueError("latent stream checkpoint generator must be torch.Generator")
|
| 1011 |
-
root_path = Path(root)
|
| 1012 |
-
audit = audit_streaming_checkpoint_chain(root_path)
|
| 1013 |
-
manifest = audit["latest_manifest"]
|
| 1014 |
-
assert isinstance(manifest, dict)
|
| 1015 |
-
saved_identity = manifest.get("stream_identity")
|
| 1016 |
-
saved_identity_hash = manifest.get("stream_identity_sha256")
|
| 1017 |
-
if (
|
| 1018 |
-
not isinstance(saved_identity, Mapping)
|
| 1019 |
-
or saved_identity_hash != stream.identity_sha256
|
| 1020 |
-
or hashlib.sha256(_canonical_json(saved_identity)).hexdigest()
|
| 1021 |
-
!= stream.identity_sha256
|
| 1022 |
-
):
|
| 1023 |
-
raise ValueError("latent stream checkpoint identity differs")
|
| 1024 |
-
validated_state = stream.validate_state_dict(manifest.get("stream_state"))
|
| 1025 |
-
completed_steps = manifest.get("completed_steps")
|
| 1026 |
-
if (
|
| 1027 |
-
isinstance(completed_steps, bool)
|
| 1028 |
-
or not isinstance(completed_steps, int)
|
| 1029 |
-
or completed_steps != validated_state["completed_batches"]
|
| 1030 |
-
):
|
| 1031 |
-
raise ValueError("latent stream checkpoint completed steps differ")
|
| 1032 |
-
first_step = manifest.get("provenance_first_step")
|
| 1033 |
-
provenance = manifest.get("provenance")
|
| 1034 |
-
if (
|
| 1035 |
-
isinstance(first_step, bool)
|
| 1036 |
-
or not isinstance(first_step, int)
|
| 1037 |
-
or not isinstance(provenance, list)
|
| 1038 |
-
):
|
| 1039 |
-
raise ValueError("latent stream checkpoint provenance is invalid")
|
| 1040 |
-
_validated_provenance(
|
| 1041 |
-
provenance,
|
| 1042 |
-
first_step=first_step,
|
| 1043 |
-
completed_steps=completed_steps,
|
| 1044 |
-
)
|
| 1045 |
-
model_record = _mapping(manifest.get("model_checkpoint"), "model checkpoint")
|
| 1046 |
-
model_path = _safe_child(root_path, model_record.get("file"), "model")
|
| 1047 |
-
expected_hash = model_record.get("sha256")
|
| 1048 |
-
if (
|
| 1049 |
-
not isinstance(expected_hash, str)
|
| 1050 |
-
or sha256_file(model_path) != expected_hash
|
| 1051 |
-
or model_record.get("bytes") != model_path.stat().st_size
|
| 1052 |
-
):
|
| 1053 |
-
raise ValueError("latent stream model checkpoint identity differs")
|
| 1054 |
-
|
| 1055 |
-
restored_steps = load_latent_checkpoint(
|
| 1056 |
-
model_path,
|
| 1057 |
-
model,
|
| 1058 |
-
optimizers,
|
| 1059 |
-
generator,
|
| 1060 |
-
expected_completed_steps=completed_steps,
|
| 1061 |
-
)
|
| 1062 |
-
if restored_steps != completed_steps:
|
| 1063 |
-
raise ValueError("latent stream model and corpus checkpoint steps differ")
|
| 1064 |
-
stream.load_state_dict(manifest["stream_state"])
|
| 1065 |
-
return manifest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/latent_training.py
DELETED
|
@@ -1,762 +0,0 @@
|
|
| 1 |
-
"""Batched optimization state for the differentiable latent lexicon."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass, replace
|
| 6 |
-
import hashlib
|
| 7 |
-
import math
|
| 8 |
-
import os
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
import tempfile
|
| 11 |
-
from typing import Any, Mapping, Sequence
|
| 12 |
-
|
| 13 |
-
# Required by strict deterministic CUDA matmul backward. This module is imported
|
| 14 |
-
# before latent models create their first CUDA context.
|
| 15 |
-
os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
|
| 16 |
-
|
| 17 |
-
import torch
|
| 18 |
-
from torch import nn
|
| 19 |
-
|
| 20 |
-
from starse.latent_lexicon import (
|
| 21 |
-
CompositionalPairResidual,
|
| 22 |
-
CounterfactualInfoNCE,
|
| 23 |
-
LatentSentenceSample,
|
| 24 |
-
PairGateIndex,
|
| 25 |
-
SignedPairHasher,
|
| 26 |
-
SparsePairGate,
|
| 27 |
-
paired_rloo_surrogate,
|
| 28 |
-
sample_latent_sentence,
|
| 29 |
-
symmetric_info_nce_replacements,
|
| 30 |
-
)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@dataclass(frozen=True)
|
| 34 |
-
class PairedLatentSamples:
|
| 35 |
-
primary_anchors: tuple[LatentSentenceSample, ...]
|
| 36 |
-
primary_positives: tuple[LatentSentenceSample, ...]
|
| 37 |
-
alternate_anchors: tuple[LatentSentenceSample, ...]
|
| 38 |
-
alternate_positives: tuple[LatentSentenceSample, ...]
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
@dataclass(frozen=True)
|
| 42 |
-
class PairedLatentBatch:
|
| 43 |
-
primary_anchors: tuple[LatentSentenceSample, ...]
|
| 44 |
-
primary_positives: tuple[LatentSentenceSample, ...]
|
| 45 |
-
alternate_anchors: tuple[LatentSentenceSample, ...]
|
| 46 |
-
alternate_positives: tuple[LatentSentenceSample, ...]
|
| 47 |
-
counterfactual: CounterfactualInfoNCE
|
| 48 |
-
embedding_loss: torch.Tensor
|
| 49 |
-
architecture_loss: torch.Tensor
|
| 50 |
-
total_loss: torch.Tensor
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
class LatentLexiconModel(nn.Module):
|
| 54 |
-
"""Trainable tensors and immutable routing data for latent segmentation."""
|
| 55 |
-
|
| 56 |
-
def __init__(
|
| 57 |
-
self,
|
| 58 |
-
*,
|
| 59 |
-
embedding_weight: torch.Tensor,
|
| 60 |
-
parent_left: Sequence[int] | torch.Tensor,
|
| 61 |
-
parent_right: Sequence[int] | torch.Tensor,
|
| 62 |
-
pair_index: PairGateIndex,
|
| 63 |
-
residual_rows: int,
|
| 64 |
-
residual_components: int = 4,
|
| 65 |
-
training_seed: int = 0,
|
| 66 |
-
initial_merge_potential: float = -2.0,
|
| 67 |
-
initial_split_potential: float = -2.0,
|
| 68 |
-
potential_bound: float = 4.0,
|
| 69 |
-
) -> None:
|
| 70 |
-
super().__init__()
|
| 71 |
-
if embedding_weight.ndim != 2 or not embedding_weight.is_floating_point():
|
| 72 |
-
raise ValueError("embedding weight must be a floating rank-2 tensor")
|
| 73 |
-
if embedding_weight.dtype not in (torch.float32, torch.float64):
|
| 74 |
-
raise ValueError(
|
| 75 |
-
"latent training weights must use float32 or float64 for sparse AdaGrad"
|
| 76 |
-
)
|
| 77 |
-
if embedding_weight.shape[0] <= 0 or embedding_weight.shape[1] <= 0:
|
| 78 |
-
raise ValueError("embedding weight dimensions must be positive")
|
| 79 |
-
if not math.isfinite(initial_split_potential):
|
| 80 |
-
raise ValueError("initial split potential must be finite")
|
| 81 |
-
if abs(initial_split_potential) >= potential_bound:
|
| 82 |
-
raise ValueError("initial split potential must lie strictly inside its bound")
|
| 83 |
-
left = torch.as_tensor(parent_left, dtype=torch.long)
|
| 84 |
-
right = torch.as_tensor(parent_right, dtype=torch.long)
|
| 85 |
-
vocabulary_size = embedding_weight.shape[0]
|
| 86 |
-
if left.shape != (vocabulary_size,) or right.shape != (vocabulary_size,):
|
| 87 |
-
raise ValueError("canonical child arrays differ from the model vocabulary")
|
| 88 |
-
|
| 89 |
-
self.embedding = nn.Embedding.from_pretrained(
|
| 90 |
-
embedding_weight.detach().clone(), freeze=False
|
| 91 |
-
)
|
| 92 |
-
self.register_buffer("parent_left", left.detach().clone())
|
| 93 |
-
self.register_buffer("parent_right", right.detach().clone())
|
| 94 |
-
initial_split_raw = potential_bound * math.atanh(
|
| 95 |
-
initial_split_potential / potential_bound
|
| 96 |
-
)
|
| 97 |
-
self.split_raw_scores = nn.Parameter(embedding_weight.new_full(
|
| 98 |
-
(vocabulary_size,), initial_split_raw
|
| 99 |
-
))
|
| 100 |
-
self.pair_gate = SparsePairGate(
|
| 101 |
-
pair_index,
|
| 102 |
-
initial_potential=initial_merge_potential,
|
| 103 |
-
potential_bound=potential_bound,
|
| 104 |
-
).to(device=embedding_weight.device, dtype=embedding_weight.dtype)
|
| 105 |
-
self.residual = CompositionalPairResidual(
|
| 106 |
-
residual_rows,
|
| 107 |
-
embedding_weight.shape[1],
|
| 108 |
-
components=residual_components,
|
| 109 |
-
).to(device=embedding_weight.device, dtype=embedding_weight.dtype)
|
| 110 |
-
self.hasher = SignedPairHasher(
|
| 111 |
-
model_vocab_size=vocabulary_size,
|
| 112 |
-
rows_per_component=residual_rows,
|
| 113 |
-
components=residual_components,
|
| 114 |
-
training_seed=training_seed,
|
| 115 |
-
)
|
| 116 |
-
self.potential_bound = float(potential_bound)
|
| 117 |
-
|
| 118 |
-
def sample_sentence(
|
| 119 |
-
self,
|
| 120 |
-
roots: Sequence[int],
|
| 121 |
-
generator: torch.Generator,
|
| 122 |
-
*,
|
| 123 |
-
explorer_epsilon: float,
|
| 124 |
-
materialize_numerator: bool = True,
|
| 125 |
-
) -> LatentSentenceSample:
|
| 126 |
-
return sample_latent_sentence(
|
| 127 |
-
roots,
|
| 128 |
-
self.embedding.weight,
|
| 129 |
-
self.parent_left,
|
| 130 |
-
self.parent_right,
|
| 131 |
-
self.split_raw_scores,
|
| 132 |
-
self.pair_gate,
|
| 133 |
-
self.residual,
|
| 134 |
-
self.hasher,
|
| 135 |
-
explorer_epsilon=explorer_epsilon,
|
| 136 |
-
potential_bound=self.potential_bound,
|
| 137 |
-
generator=generator,
|
| 138 |
-
materialize_numerator=materialize_numerator,
|
| 139 |
-
)
|
| 140 |
-
|
| 141 |
-
def identity(self) -> dict[str, object]:
|
| 142 |
-
"""Return immutable segmentation identity required for exact resume."""
|
| 143 |
-
return {
|
| 144 |
-
"pair_keys": _array_identity(self.pair_gate.index.keys),
|
| 145 |
-
"parent_left": _tensor_identity(self.parent_left),
|
| 146 |
-
"parent_right": _tensor_identity(self.parent_right),
|
| 147 |
-
"hasher": {
|
| 148 |
-
"model_vocab_size": self.hasher.model_vocab_size,
|
| 149 |
-
"rows_per_component": self.hasher.rows_per_component,
|
| 150 |
-
"components": self.hasher.components,
|
| 151 |
-
"training_seed": self.hasher.training_seed,
|
| 152 |
-
},
|
| 153 |
-
"embedding_shape": list(self.embedding.weight.shape),
|
| 154 |
-
"embedding_dtype": str(self.embedding.weight.dtype),
|
| 155 |
-
"residual_shape": [
|
| 156 |
-
self.residual.components,
|
| 157 |
-
self.residual.rows_per_component,
|
| 158 |
-
self.residual.dimension,
|
| 159 |
-
],
|
| 160 |
-
"potential_bound": self.potential_bound,
|
| 161 |
-
"pair_gate_rows": self.pair_gate.gates.num_embeddings,
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
def _sample_texts(
|
| 166 |
-
model: LatentLexiconModel,
|
| 167 |
-
texts: Sequence[Sequence[int]],
|
| 168 |
-
generator: torch.Generator,
|
| 169 |
-
explorer_epsilon: float,
|
| 170 |
-
) -> tuple[LatentSentenceSample, ...]:
|
| 171 |
-
return tuple(
|
| 172 |
-
model.sample_sentence(
|
| 173 |
-
roots,
|
| 174 |
-
generator,
|
| 175 |
-
explorer_epsilon=explorer_epsilon,
|
| 176 |
-
materialize_numerator=False,
|
| 177 |
-
)
|
| 178 |
-
for roots in texts
|
| 179 |
-
)
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
def rematerialize_paired_latent_samples(
|
| 183 |
-
model: LatentLexiconModel,
|
| 184 |
-
samples: PairedLatentSamples,
|
| 185 |
-
) -> PairedLatentSamples:
|
| 186 |
-
"""Rebuild all emission numerators through shared batched table lookups."""
|
| 187 |
-
rows = (
|
| 188 |
-
samples.primary_anchors
|
| 189 |
-
+ samples.primary_positives
|
| 190 |
-
+ samples.alternate_anchors
|
| 191 |
-
+ samples.alternate_positives
|
| 192 |
-
)
|
| 193 |
-
original_ids = [
|
| 194 |
-
emission.token_ids[0]
|
| 195 |
-
for sample in rows
|
| 196 |
-
for emission in sample.emissions
|
| 197 |
-
if emission.kind == "original"
|
| 198 |
-
]
|
| 199 |
-
pair_ids = [
|
| 200 |
-
emission.token_ids
|
| 201 |
-
for sample in rows
|
| 202 |
-
for emission in sample.emissions
|
| 203 |
-
if emission.kind == "pair"
|
| 204 |
-
]
|
| 205 |
-
device = model.embedding.weight.device
|
| 206 |
-
if original_ids:
|
| 207 |
-
original_index = torch.tensor(original_ids, dtype=torch.long, device=device)
|
| 208 |
-
original_vectors = model.embedding.weight.index_select(0, original_index)
|
| 209 |
-
else:
|
| 210 |
-
original_vectors = model.embedding.weight.new_empty(
|
| 211 |
-
(0, model.embedding.embedding_dim)
|
| 212 |
-
)
|
| 213 |
-
if pair_ids:
|
| 214 |
-
left_ids = torch.tensor(
|
| 215 |
-
[pair[0] for pair in pair_ids], dtype=torch.long, device=device
|
| 216 |
-
)
|
| 217 |
-
right_ids = torch.tensor(
|
| 218 |
-
[pair[1] for pair in pair_ids], dtype=torch.long, device=device
|
| 219 |
-
)
|
| 220 |
-
buckets, signs = model.hasher.codes(
|
| 221 |
-
left_ids,
|
| 222 |
-
right_ids,
|
| 223 |
-
dtype=model.embedding.weight.dtype,
|
| 224 |
-
device=device,
|
| 225 |
-
)
|
| 226 |
-
pair_vectors = model.residual(
|
| 227 |
-
model.embedding.weight.index_select(0, left_ids),
|
| 228 |
-
model.embedding.weight.index_select(0, right_ids),
|
| 229 |
-
buckets,
|
| 230 |
-
signs,
|
| 231 |
-
)
|
| 232 |
-
else:
|
| 233 |
-
pair_vectors = model.embedding.weight.new_empty(
|
| 234 |
-
(0, model.embedding.embedding_dim)
|
| 235 |
-
)
|
| 236 |
-
|
| 237 |
-
original_cursor = 0
|
| 238 |
-
pair_cursor = 0
|
| 239 |
-
rebuilt = []
|
| 240 |
-
for sample in rows:
|
| 241 |
-
vectors: list[torch.Tensor] = []
|
| 242 |
-
for emission in sample.emissions:
|
| 243 |
-
if emission.kind == "original":
|
| 244 |
-
vectors.append(original_vectors[original_cursor])
|
| 245 |
-
original_cursor += 1
|
| 246 |
-
else:
|
| 247 |
-
vectors.append(pair_vectors[pair_cursor])
|
| 248 |
-
pair_cursor += 1
|
| 249 |
-
if not vectors:
|
| 250 |
-
raise RuntimeError("sampled latent sentence emitted no vectors")
|
| 251 |
-
rebuilt.append(replace(sample, numerator=torch.stack(vectors).sum(dim=0)))
|
| 252 |
-
if original_cursor != len(original_ids) or pair_cursor != len(pair_ids):
|
| 253 |
-
raise RuntimeError("batched emission cursor did not consume all vectors")
|
| 254 |
-
anchor_count = len(samples.primary_anchors)
|
| 255 |
-
positive_count = len(samples.primary_positives)
|
| 256 |
-
alternate_anchor_count = len(samples.alternate_anchors)
|
| 257 |
-
first = anchor_count
|
| 258 |
-
second = first + positive_count
|
| 259 |
-
third = second + alternate_anchor_count
|
| 260 |
-
return PairedLatentSamples(
|
| 261 |
-
primary_anchors=tuple(rebuilt[:first]),
|
| 262 |
-
primary_positives=tuple(rebuilt[first:second]),
|
| 263 |
-
alternate_anchors=tuple(rebuilt[second:third]),
|
| 264 |
-
alternate_positives=tuple(rebuilt[third:]),
|
| 265 |
-
)
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
def sample_paired_latent_samples(
|
| 269 |
-
model: LatentLexiconModel,
|
| 270 |
-
anchors: Sequence[Sequence[int]],
|
| 271 |
-
positives: Sequence[Sequence[int]],
|
| 272 |
-
generator: torch.Generator,
|
| 273 |
-
*,
|
| 274 |
-
explorer_epsilon: float,
|
| 275 |
-
) -> PairedLatentSamples:
|
| 276 |
-
"""Draw two iid hard paths and materialize their sentence numerators."""
|
| 277 |
-
if len(anchors) == 0 or len(anchors) != len(positives):
|
| 278 |
-
raise ValueError("anchor and positive batches must have equal positive size")
|
| 279 |
-
samples = PairedLatentSamples(
|
| 280 |
-
primary_anchors=_sample_texts(
|
| 281 |
-
model, anchors, generator, explorer_epsilon
|
| 282 |
-
),
|
| 283 |
-
primary_positives=_sample_texts(
|
| 284 |
-
model, positives, generator, explorer_epsilon
|
| 285 |
-
),
|
| 286 |
-
alternate_anchors=_sample_texts(
|
| 287 |
-
model, anchors, generator, explorer_epsilon
|
| 288 |
-
),
|
| 289 |
-
alternate_positives=_sample_texts(
|
| 290 |
-
model, positives, generator, explorer_epsilon
|
| 291 |
-
),
|
| 292 |
-
)
|
| 293 |
-
return rematerialize_paired_latent_samples(model, samples)
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
def build_paired_latent_batch(
|
| 297 |
-
samples: PairedLatentSamples,
|
| 298 |
-
*,
|
| 299 |
-
scale: float,
|
| 300 |
-
) -> PairedLatentBatch:
|
| 301 |
-
"""Build the counterfactual and RLOO losses from already sampled paths."""
|
| 302 |
-
counterfactual = symmetric_info_nce_replacements(
|
| 303 |
-
torch.stack([sample.numerator for sample in samples.primary_anchors]),
|
| 304 |
-
torch.stack([sample.numerator for sample in samples.primary_positives]),
|
| 305 |
-
torch.stack([sample.numerator for sample in samples.alternate_anchors]),
|
| 306 |
-
torch.stack([sample.numerator for sample in samples.alternate_positives]),
|
| 307 |
-
scale,
|
| 308 |
-
)
|
| 309 |
-
primary_logs = torch.stack([
|
| 310 |
-
sample.mixture_log_probability
|
| 311 |
-
for sample in samples.primary_anchors + samples.primary_positives
|
| 312 |
-
])
|
| 313 |
-
alternate_logs = torch.stack([
|
| 314 |
-
sample.mixture_log_probability
|
| 315 |
-
for sample in samples.alternate_anchors + samples.alternate_positives
|
| 316 |
-
])
|
| 317 |
-
alternate_losses = torch.cat((
|
| 318 |
-
counterfactual.alternate_anchor_losses,
|
| 319 |
-
counterfactual.alternate_positive_losses,
|
| 320 |
-
))
|
| 321 |
-
architecture_loss = paired_rloo_surrogate(
|
| 322 |
-
primary_logs,
|
| 323 |
-
alternate_logs,
|
| 324 |
-
counterfactual.primary_loss,
|
| 325 |
-
alternate_losses,
|
| 326 |
-
)
|
| 327 |
-
return PairedLatentBatch(
|
| 328 |
-
primary_anchors=samples.primary_anchors,
|
| 329 |
-
primary_positives=samples.primary_positives,
|
| 330 |
-
alternate_anchors=samples.alternate_anchors,
|
| 331 |
-
alternate_positives=samples.alternate_positives,
|
| 332 |
-
counterfactual=counterfactual,
|
| 333 |
-
embedding_loss=counterfactual.primary_loss,
|
| 334 |
-
architecture_loss=architecture_loss,
|
| 335 |
-
total_loss=counterfactual.primary_loss + architecture_loss,
|
| 336 |
-
)
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
def sample_paired_latent_batch(
|
| 340 |
-
model: LatentLexiconModel,
|
| 341 |
-
anchors: Sequence[Sequence[int]],
|
| 342 |
-
positives: Sequence[Sequence[int]],
|
| 343 |
-
generator: torch.Generator,
|
| 344 |
-
*,
|
| 345 |
-
scale: float,
|
| 346 |
-
explorer_epsilon: float,
|
| 347 |
-
) -> PairedLatentBatch:
|
| 348 |
-
"""Draw two iid hard paths per text and build the paired RLOO loss."""
|
| 349 |
-
samples = sample_paired_latent_samples(
|
| 350 |
-
model,
|
| 351 |
-
anchors,
|
| 352 |
-
positives,
|
| 353 |
-
generator,
|
| 354 |
-
explorer_epsilon=explorer_epsilon,
|
| 355 |
-
)
|
| 356 |
-
return build_paired_latent_batch(samples, scale=scale)
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
@dataclass
|
| 360 |
-
class LatentOptimizers:
|
| 361 |
-
dense: torch.optim.AdamW
|
| 362 |
-
sparse: torch.optim.Adagrad
|
| 363 |
-
|
| 364 |
-
def zero_grad(self) -> None:
|
| 365 |
-
self.dense.zero_grad(set_to_none=True)
|
| 366 |
-
self.sparse.zero_grad(set_to_none=True)
|
| 367 |
-
|
| 368 |
-
def step(self) -> None:
|
| 369 |
-
self.dense.step()
|
| 370 |
-
self.sparse.step()
|
| 371 |
-
|
| 372 |
-
def state_dict(self) -> dict[str, object]:
|
| 373 |
-
return {
|
| 374 |
-
"dense": self.dense.state_dict(),
|
| 375 |
-
"sparse": self.sparse.state_dict(),
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
def load_state_dict(self, state: Mapping[str, object]) -> None:
|
| 379 |
-
if set(state) != {"dense", "sparse"}:
|
| 380 |
-
raise ValueError("latent optimizer state must contain dense and sparse")
|
| 381 |
-
self.dense.load_state_dict(state["dense"]) # type: ignore[arg-type]
|
| 382 |
-
self.sparse.load_state_dict(state["sparse"]) # type: ignore[arg-type]
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
def _parameter_ids(optimizer: torch.optim.Optimizer) -> list[int]:
|
| 386 |
-
return [
|
| 387 |
-
id(parameter)
|
| 388 |
-
for group in optimizer.param_groups
|
| 389 |
-
for parameter in group["params"]
|
| 390 |
-
]
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
def build_latent_optimizers(
|
| 394 |
-
model: LatentLexiconModel,
|
| 395 |
-
*,
|
| 396 |
-
dense_lr: float,
|
| 397 |
-
sparse_lr: float,
|
| 398 |
-
weight_decay: float,
|
| 399 |
-
) -> LatentOptimizers:
|
| 400 |
-
"""Partition every trainable tensor between dense AdamW and sparse AdaGrad."""
|
| 401 |
-
gate = model.pair_gate.gates.weight
|
| 402 |
-
trainable = [parameter for parameter in model.parameters() if parameter.requires_grad]
|
| 403 |
-
if not gate.requires_grad or all(parameter is not gate for parameter in trainable):
|
| 404 |
-
raise ValueError("sparse pair gate must be a trainable model parameter")
|
| 405 |
-
dense_parameters = [parameter for parameter in trainable if parameter is not gate]
|
| 406 |
-
if not dense_parameters:
|
| 407 |
-
raise ValueError("latent model has no dense trainable parameters")
|
| 408 |
-
if len({id(parameter) for parameter in trainable}) != len(trainable):
|
| 409 |
-
raise ValueError("latent model exposes a duplicate trainable parameter")
|
| 410 |
-
|
| 411 |
-
optimizers = LatentOptimizers(
|
| 412 |
-
dense=torch.optim.AdamW(
|
| 413 |
-
dense_parameters, lr=dense_lr, weight_decay=weight_decay
|
| 414 |
-
),
|
| 415 |
-
sparse=torch.optim.Adagrad(
|
| 416 |
-
[gate],
|
| 417 |
-
lr=sparse_lr,
|
| 418 |
-
weight_decay=0.0,
|
| 419 |
-
initial_accumulator_value=0.0,
|
| 420 |
-
),
|
| 421 |
-
)
|
| 422 |
-
dense_ids = _parameter_ids(optimizers.dense)
|
| 423 |
-
sparse_ids = _parameter_ids(optimizers.sparse)
|
| 424 |
-
trainable_ids = {id(parameter) for parameter in trainable}
|
| 425 |
-
if len(dense_ids) != len(set(dense_ids)) or len(sparse_ids) != len(set(sparse_ids)):
|
| 426 |
-
raise RuntimeError("an optimizer contains duplicate parameters")
|
| 427 |
-
if set(dense_ids) & set(sparse_ids):
|
| 428 |
-
raise RuntimeError("dense and sparse optimizer parameters overlap")
|
| 429 |
-
if set(dense_ids) | set(sparse_ids) != trainable_ids:
|
| 430 |
-
raise RuntimeError("optimizer partition does not cover the latent model")
|
| 431 |
-
return optimizers
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
def _has_exact_edges(
|
| 435 |
-
model: LatentLexiconModel,
|
| 436 |
-
texts: Sequence[Sequence[int]],
|
| 437 |
-
) -> bool:
|
| 438 |
-
return any(
|
| 439 |
-
bool((model.pair_gate.index.lookup_sequence(roots) >= 0).any().item())
|
| 440 |
-
for roots in texts
|
| 441 |
-
if len(roots) > 1
|
| 442 |
-
)
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
def validate_finite_gradients(model: LatentLexiconModel) -> None:
|
| 446 |
-
for name, parameter in model.named_parameters():
|
| 447 |
-
gradient = parameter.grad
|
| 448 |
-
if gradient is None:
|
| 449 |
-
continue
|
| 450 |
-
values = gradient.coalesce().values() if gradient.is_sparse else gradient
|
| 451 |
-
if not bool(torch.isfinite(values).all().item()):
|
| 452 |
-
raise FloatingPointError(f"nonfinite gradient for {name}")
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
def backward_deterministic_loss(loss: torch.Tensor) -> None:
|
| 456 |
-
"""Backpropagate while requiring deterministic CUDA reductions."""
|
| 457 |
-
previous_enabled = torch.are_deterministic_algorithms_enabled()
|
| 458 |
-
previous_warn_only = torch.is_deterministic_algorithms_warn_only_enabled()
|
| 459 |
-
if not previous_enabled or previous_warn_only:
|
| 460 |
-
torch.use_deterministic_algorithms(True, warn_only=False)
|
| 461 |
-
try:
|
| 462 |
-
loss.backward()
|
| 463 |
-
finally:
|
| 464 |
-
if not previous_enabled or previous_warn_only:
|
| 465 |
-
torch.use_deterministic_algorithms(
|
| 466 |
-
previous_enabled, warn_only=previous_warn_only
|
| 467 |
-
)
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
def backward_latent_loss(batch: PairedLatentBatch) -> None:
|
| 471 |
-
"""Backpropagate with deterministic repeated-index gradient reduction."""
|
| 472 |
-
if not bool(torch.isfinite(batch.total_loss).item()):
|
| 473 |
-
raise FloatingPointError("latent training loss is nonfinite")
|
| 474 |
-
backward_deterministic_loss(batch.total_loss)
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
def validate_exact_gate_gradient(
|
| 478 |
-
model: LatentLexiconModel,
|
| 479 |
-
anchors: Sequence[Sequence[int]],
|
| 480 |
-
positives: Sequence[Sequence[int]],
|
| 481 |
-
*,
|
| 482 |
-
explorer_epsilon: float,
|
| 483 |
-
) -> None:
|
| 484 |
-
"""Require a sparse gate gradient whenever learned exact edges are eligible."""
|
| 485 |
-
gate_gradient = model.pair_gate.gates.weight.grad
|
| 486 |
-
if explorer_epsilon < 1.0 and _has_exact_edges(
|
| 487 |
-
model, tuple(anchors) + tuple(positives)
|
| 488 |
-
):
|
| 489 |
-
if gate_gradient is None or not gate_gradient.is_sparse:
|
| 490 |
-
raise RuntimeError("an exact pair gate did not receive a sparse gradient")
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
def backward_latent_batch(
|
| 494 |
-
model: LatentLexiconModel,
|
| 495 |
-
batch: PairedLatentBatch,
|
| 496 |
-
anchors: Sequence[Sequence[int]],
|
| 497 |
-
positives: Sequence[Sequence[int]],
|
| 498 |
-
*,
|
| 499 |
-
explorer_epsilon: float,
|
| 500 |
-
) -> None:
|
| 501 |
-
"""Backpropagate one built batch and enforce production invariants."""
|
| 502 |
-
backward_latent_loss(batch)
|
| 503 |
-
validate_exact_gate_gradient(
|
| 504 |
-
model,
|
| 505 |
-
anchors,
|
| 506 |
-
positives,
|
| 507 |
-
explorer_epsilon=explorer_epsilon,
|
| 508 |
-
)
|
| 509 |
-
validate_finite_gradients(model)
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
def latent_training_step(
|
| 513 |
-
model: LatentLexiconModel,
|
| 514 |
-
optimizers: LatentOptimizers,
|
| 515 |
-
anchors: Sequence[Sequence[int]],
|
| 516 |
-
positives: Sequence[Sequence[int]],
|
| 517 |
-
generator: torch.Generator,
|
| 518 |
-
*,
|
| 519 |
-
scale: float,
|
| 520 |
-
explorer_epsilon: float,
|
| 521 |
-
) -> PairedLatentBatch:
|
| 522 |
-
"""Backpropagate and apply one paired dense/sparse latent update."""
|
| 523 |
-
optimizers.zero_grad()
|
| 524 |
-
batch = sample_paired_latent_batch(
|
| 525 |
-
model,
|
| 526 |
-
anchors,
|
| 527 |
-
positives,
|
| 528 |
-
generator,
|
| 529 |
-
scale=scale,
|
| 530 |
-
explorer_epsilon=explorer_epsilon,
|
| 531 |
-
)
|
| 532 |
-
backward_latent_batch(
|
| 533 |
-
model,
|
| 534 |
-
batch,
|
| 535 |
-
anchors,
|
| 536 |
-
positives,
|
| 537 |
-
explorer_epsilon=explorer_epsilon,
|
| 538 |
-
)
|
| 539 |
-
optimizers.step()
|
| 540 |
-
return batch
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
_CHECKPOINT_FORMAT_VERSION = 1
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
def _sha256_bytes(buffer: memoryview) -> str:
|
| 547 |
-
digest = hashlib.sha256()
|
| 548 |
-
chunk_size = 8 * 1024 * 1024
|
| 549 |
-
for offset in range(0, len(buffer), chunk_size):
|
| 550 |
-
digest.update(buffer[offset:offset + chunk_size])
|
| 551 |
-
return digest.hexdigest()
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
def _array_identity(array: Any) -> dict[str, object]:
|
| 555 |
-
contiguous = memoryview(array).cast("B")
|
| 556 |
-
return {
|
| 557 |
-
"shape": list(array.shape),
|
| 558 |
-
"dtype": array.dtype.str,
|
| 559 |
-
"sha256": _sha256_bytes(contiguous),
|
| 560 |
-
}
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
def _tensor_identity(tensor: torch.Tensor) -> dict[str, object]:
|
| 564 |
-
cpu = tensor.detach().to(device="cpu").contiguous()
|
| 565 |
-
buffer = memoryview(cpu.numpy()).cast("B")
|
| 566 |
-
return {
|
| 567 |
-
"shape": list(cpu.shape),
|
| 568 |
-
"dtype": str(cpu.dtype),
|
| 569 |
-
"sha256": _sha256_bytes(buffer),
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
def _validate_model_state(
|
| 574 |
-
saved: object,
|
| 575 |
-
current: Mapping[str, torch.Tensor],
|
| 576 |
-
) -> Mapping[str, torch.Tensor]:
|
| 577 |
-
if not isinstance(saved, Mapping) or set(saved) != set(current):
|
| 578 |
-
raise ValueError("checkpoint model state keys differ from the model")
|
| 579 |
-
for name, expected in current.items():
|
| 580 |
-
value = saved[name]
|
| 581 |
-
if not isinstance(value, torch.Tensor):
|
| 582 |
-
raise ValueError(f"checkpoint model state {name} is not a tensor")
|
| 583 |
-
if value.shape != expected.shape or value.dtype != expected.dtype:
|
| 584 |
-
raise ValueError(f"checkpoint model state {name} has incompatible metadata")
|
| 585 |
-
return saved # type: ignore[return-value]
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
def _optimizer_membership(state: object) -> tuple[tuple[int, ...], ...]:
|
| 589 |
-
if not isinstance(state, Mapping):
|
| 590 |
-
raise ValueError("checkpoint optimizer state must be a mapping")
|
| 591 |
-
if set(state) != {"state", "param_groups"}:
|
| 592 |
-
raise ValueError("checkpoint optimizer state has invalid keys")
|
| 593 |
-
groups = state["param_groups"]
|
| 594 |
-
slots = state["state"]
|
| 595 |
-
if not isinstance(groups, list) or not isinstance(slots, Mapping):
|
| 596 |
-
raise ValueError("checkpoint optimizer groups or slots are invalid")
|
| 597 |
-
membership: list[tuple[int, ...]] = []
|
| 598 |
-
flattened: list[int] = []
|
| 599 |
-
for group in groups:
|
| 600 |
-
if not isinstance(group, Mapping) or not isinstance(group.get("params"), list):
|
| 601 |
-
raise ValueError("checkpoint optimizer parameter group is invalid")
|
| 602 |
-
parameters = tuple(group["params"])
|
| 603 |
-
if any(not isinstance(parameter, int) for parameter in parameters):
|
| 604 |
-
raise ValueError("checkpoint optimizer parameter ids must be integers")
|
| 605 |
-
membership.append(parameters)
|
| 606 |
-
flattened.extend(parameters)
|
| 607 |
-
if len(flattened) != len(set(flattened)):
|
| 608 |
-
raise ValueError("checkpoint optimizer repeats a parameter id")
|
| 609 |
-
if any(not isinstance(parameter, int) or parameter not in flattened for parameter in slots):
|
| 610 |
-
raise ValueError("checkpoint optimizer slot has no parameter membership")
|
| 611 |
-
return tuple(membership)
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
def _validate_optimizer_state(
|
| 615 |
-
saved: object,
|
| 616 |
-
optimizers: LatentOptimizers,
|
| 617 |
-
) -> Mapping[str, object]:
|
| 618 |
-
if not isinstance(saved, Mapping) or set(saved) != {"dense", "sparse"}:
|
| 619 |
-
raise ValueError("checkpoint must contain dense and sparse optimizer state")
|
| 620 |
-
current = optimizers.state_dict()
|
| 621 |
-
for name in ("dense", "sparse"):
|
| 622 |
-
if _optimizer_membership(saved[name]) != _optimizer_membership(current[name]):
|
| 623 |
-
raise ValueError(f"checkpoint {name} optimizer membership differs")
|
| 624 |
-
return saved # type: ignore[return-value]
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
def _optimizer_parameter_manifest(
|
| 628 |
-
model: LatentLexiconModel,
|
| 629 |
-
optimizers: LatentOptimizers,
|
| 630 |
-
) -> dict[str, list[str]]:
|
| 631 |
-
named = {
|
| 632 |
-
id(parameter): name
|
| 633 |
-
for name, parameter in model.named_parameters()
|
| 634 |
-
if parameter.requires_grad
|
| 635 |
-
}
|
| 636 |
-
gate_id = id(model.pair_gate.gates.weight)
|
| 637 |
-
dense_ids = _parameter_ids(optimizers.dense)
|
| 638 |
-
sparse_ids = _parameter_ids(optimizers.sparse)
|
| 639 |
-
if (
|
| 640 |
-
len(dense_ids) != len(set(dense_ids))
|
| 641 |
-
or len(sparse_ids) != len(set(sparse_ids))
|
| 642 |
-
or set(dense_ids) != set(named) - {gate_id}
|
| 643 |
-
or sparse_ids != [gate_id]
|
| 644 |
-
):
|
| 645 |
-
raise ValueError("latent optimizer partition differs from the model contract")
|
| 646 |
-
return {
|
| 647 |
-
"dense": [named[parameter] for parameter in dense_ids],
|
| 648 |
-
"sparse": [named[parameter] for parameter in sparse_ids],
|
| 649 |
-
}
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
def save_latent_checkpoint(
|
| 653 |
-
path: str | Path,
|
| 654 |
-
model: LatentLexiconModel,
|
| 655 |
-
optimizers: LatentOptimizers,
|
| 656 |
-
generator: torch.Generator,
|
| 657 |
-
*,
|
| 658 |
-
completed_steps: int,
|
| 659 |
-
) -> None:
|
| 660 |
-
"""Atomically save every state needed to reproduce the next materialized batch."""
|
| 661 |
-
if (
|
| 662 |
-
isinstance(completed_steps, bool)
|
| 663 |
-
or not isinstance(completed_steps, int)
|
| 664 |
-
or completed_steps < 0
|
| 665 |
-
):
|
| 666 |
-
raise ValueError("completed steps must be a nonnegative integer")
|
| 667 |
-
target = Path(path)
|
| 668 |
-
target.parent.mkdir(parents=True, exist_ok=True)
|
| 669 |
-
optimizer_parameters = _optimizer_parameter_manifest(model, optimizers)
|
| 670 |
-
payload = {
|
| 671 |
-
"format_version": _CHECKPOINT_FORMAT_VERSION,
|
| 672 |
-
"identity": model.identity(),
|
| 673 |
-
"model": model.state_dict(),
|
| 674 |
-
"optimizers": optimizers.state_dict(),
|
| 675 |
-
"optimizer_parameters": optimizer_parameters,
|
| 676 |
-
"generator_state": generator.get_state(),
|
| 677 |
-
"completed_steps": int(completed_steps),
|
| 678 |
-
}
|
| 679 |
-
temporary_path: Path | None = None
|
| 680 |
-
try:
|
| 681 |
-
with tempfile.NamedTemporaryFile(
|
| 682 |
-
mode="w+b",
|
| 683 |
-
dir=target.parent,
|
| 684 |
-
prefix=f".{target.name}.tmp-",
|
| 685 |
-
delete=False,
|
| 686 |
-
) as temporary:
|
| 687 |
-
temporary_path = Path(temporary.name)
|
| 688 |
-
torch.save(payload, temporary)
|
| 689 |
-
temporary.flush()
|
| 690 |
-
os.fsync(temporary.fileno())
|
| 691 |
-
os.replace(temporary_path, target)
|
| 692 |
-
temporary_path = None
|
| 693 |
-
finally:
|
| 694 |
-
if temporary_path is not None:
|
| 695 |
-
temporary_path.unlink(missing_ok=True)
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
def load_latent_checkpoint(
|
| 699 |
-
path: str | Path,
|
| 700 |
-
model: LatentLexiconModel,
|
| 701 |
-
optimizers: LatentOptimizers,
|
| 702 |
-
generator: torch.Generator,
|
| 703 |
-
*,
|
| 704 |
-
expected_completed_steps: int | None = None,
|
| 705 |
-
) -> int:
|
| 706 |
-
"""Validate immutable identity, then restore model, optimizers and sampler RNG."""
|
| 707 |
-
payload = torch.load(
|
| 708 |
-
Path(path),
|
| 709 |
-
map_location="cpu",
|
| 710 |
-
weights_only=True,
|
| 711 |
-
)
|
| 712 |
-
required = {
|
| 713 |
-
"format_version",
|
| 714 |
-
"identity",
|
| 715 |
-
"model",
|
| 716 |
-
"optimizers",
|
| 717 |
-
"optimizer_parameters",
|
| 718 |
-
"generator_state",
|
| 719 |
-
"completed_steps",
|
| 720 |
-
}
|
| 721 |
-
if not isinstance(payload, Mapping) or set(payload) != required:
|
| 722 |
-
raise ValueError("latent checkpoint payload has invalid keys")
|
| 723 |
-
if payload["format_version"] != _CHECKPOINT_FORMAT_VERSION:
|
| 724 |
-
raise ValueError("unsupported latent checkpoint format")
|
| 725 |
-
if payload["identity"] != model.identity():
|
| 726 |
-
raise ValueError("latent checkpoint identity differs from the model")
|
| 727 |
-
optimizer_parameters = _optimizer_parameter_manifest(model, optimizers)
|
| 728 |
-
if payload["optimizer_parameters"] != optimizer_parameters:
|
| 729 |
-
raise ValueError("latent checkpoint optimizer parameter manifest differs")
|
| 730 |
-
completed_steps = payload["completed_steps"]
|
| 731 |
-
if (
|
| 732 |
-
isinstance(completed_steps, bool)
|
| 733 |
-
or not isinstance(completed_steps, int)
|
| 734 |
-
or completed_steps < 0
|
| 735 |
-
):
|
| 736 |
-
raise ValueError("latent checkpoint has invalid completed steps")
|
| 737 |
-
if expected_completed_steps is not None and (
|
| 738 |
-
isinstance(expected_completed_steps, bool)
|
| 739 |
-
or not isinstance(expected_completed_steps, int)
|
| 740 |
-
or expected_completed_steps < 0
|
| 741 |
-
):
|
| 742 |
-
raise ValueError("expected latent checkpoint steps are invalid")
|
| 743 |
-
if (
|
| 744 |
-
expected_completed_steps is not None
|
| 745 |
-
and completed_steps != expected_completed_steps
|
| 746 |
-
):
|
| 747 |
-
raise ValueError("latent checkpoint completed steps differ")
|
| 748 |
-
generator_state = payload["generator_state"]
|
| 749 |
-
if not isinstance(generator_state, torch.Tensor):
|
| 750 |
-
raise ValueError("latent checkpoint generator state is not a tensor")
|
| 751 |
-
probe_generator = torch.Generator(device=generator.device)
|
| 752 |
-
try:
|
| 753 |
-
probe_generator.set_state(generator_state)
|
| 754 |
-
except RuntimeError as error:
|
| 755 |
-
raise ValueError("latent checkpoint generator state is invalid") from error
|
| 756 |
-
model_state = _validate_model_state(payload["model"], model.state_dict())
|
| 757 |
-
optimizer_state = _validate_optimizer_state(payload["optimizers"], optimizers)
|
| 758 |
-
|
| 759 |
-
model.load_state_dict(model_state)
|
| 760 |
-
optimizers.load_state_dict(optimizer_state)
|
| 761 |
-
generator.set_state(generator_state)
|
| 762 |
-
return completed_steps
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/loss.py
DELETED
|
@@ -1,182 +0,0 @@
|
|
| 1 |
-
"""Loss used for StaRSE sign-projected post-training."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import math
|
| 6 |
-
from typing import Any, Sequence
|
| 7 |
-
|
| 8 |
-
import torch
|
| 9 |
-
import torch.nn.functional as F
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def _unwrap_model(model: Any) -> Any:
|
| 13 |
-
unwrapped = model
|
| 14 |
-
seen: set[int] = set()
|
| 15 |
-
while hasattr(unwrapped, "module") and id(unwrapped) not in seen:
|
| 16 |
-
seen.add(id(unwrapped))
|
| 17 |
-
unwrapped = unwrapped.module
|
| 18 |
-
return unwrapped
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def static_embedding_weight(model: Any) -> torch.Tensor:
|
| 22 |
-
"""Return the trainable token table from a StaticEmbedding model."""
|
| 23 |
-
|
| 24 |
-
model = _unwrap_model(model)
|
| 25 |
-
first_module = model._first_module() if callable(getattr(model, "_first_module", None)) else model[0]
|
| 26 |
-
embedding = getattr(first_module, "embedding", None)
|
| 27 |
-
weight = getattr(embedding, "weight", None)
|
| 28 |
-
if weight is None or getattr(first_module, "tokenizer", None) is None:
|
| 29 |
-
raise TypeError("Expected a SentenceTransformer model whose first module is StaticEmbedding")
|
| 30 |
-
return weight
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
class SignProjectedContrastiveLoss(torch.nn.Module):
|
| 34 |
-
"""Symmetric InfoNCE on a sign-projected token table with teacher KL."""
|
| 35 |
-
|
| 36 |
-
def __init__(
|
| 37 |
-
self,
|
| 38 |
-
*,
|
| 39 |
-
model: Any,
|
| 40 |
-
teacher: Any,
|
| 41 |
-
scale: float,
|
| 42 |
-
pair_weight: float,
|
| 43 |
-
teacher_kl_weight: float,
|
| 44 |
-
cube_weight: float = 0.0,
|
| 45 |
-
balance_weight: float = 0.0,
|
| 46 |
-
independence_weight: float = 0.0,
|
| 47 |
-
unit_prior_weight: float = 0.0,
|
| 48 |
-
regularizer_sample_size: int = 8192,
|
| 49 |
-
) -> None:
|
| 50 |
-
super().__init__()
|
| 51 |
-
self.model = model
|
| 52 |
-
self.teacher = teacher
|
| 53 |
-
self.scale = float(scale)
|
| 54 |
-
self.pair_weight = float(pair_weight)
|
| 55 |
-
self.teacher_kl_weight = float(teacher_kl_weight)
|
| 56 |
-
self.cube_weight = float(cube_weight)
|
| 57 |
-
self.balance_weight = float(balance_weight)
|
| 58 |
-
self.independence_weight = float(independence_weight)
|
| 59 |
-
self.unit_prior_weight = float(unit_prior_weight)
|
| 60 |
-
self.regularizer_sample_size = int(regularizer_sample_size)
|
| 61 |
-
|
| 62 |
-
for parameter in self.teacher.parameters():
|
| 63 |
-
parameter.requires_grad_(False)
|
| 64 |
-
self.teacher.eval()
|
| 65 |
-
|
| 66 |
-
initial = static_embedding_weight(model).detach().float()
|
| 67 |
-
initial_norm = torch.linalg.vector_norm(initial, dim=1).clamp_min(1e-12)
|
| 68 |
-
self.register_buffer("initial_unit", initial / initial_norm.unsqueeze(1))
|
| 69 |
-
|
| 70 |
-
@staticmethod
|
| 71 |
-
def _features_to_device(features: dict[str, Any], device: torch.device) -> dict[str, Any]:
|
| 72 |
-
return {key: value.to(device) if isinstance(value, torch.Tensor) else value for key, value in features.items()}
|
| 73 |
-
|
| 74 |
-
@staticmethod
|
| 75 |
-
def _scores(left: torch.Tensor, right: torch.Tensor, scale: float) -> torch.Tensor:
|
| 76 |
-
return F.normalize(left, p=2, dim=1) @ F.normalize(right, p=2, dim=1).t() * scale
|
| 77 |
-
|
| 78 |
-
@staticmethod
|
| 79 |
-
def _symmetric_nce(scores: torch.Tensor) -> torch.Tensor:
|
| 80 |
-
targets = torch.arange(scores.shape[0], device=scores.device)
|
| 81 |
-
return 0.5 * (F.cross_entropy(scores, targets) + F.cross_entropy(scores.t(), targets))
|
| 82 |
-
|
| 83 |
-
@staticmethod
|
| 84 |
-
def _symmetric_kl(teacher_scores: torch.Tensor, student_scores: torch.Tensor) -> torch.Tensor:
|
| 85 |
-
forward = F.kl_div(
|
| 86 |
-
F.log_softmax(student_scores, dim=1),
|
| 87 |
-
F.softmax(teacher_scores.detach(), dim=1),
|
| 88 |
-
reduction="batchmean",
|
| 89 |
-
)
|
| 90 |
-
backward = F.kl_div(
|
| 91 |
-
F.log_softmax(student_scores.t(), dim=1),
|
| 92 |
-
F.softmax(teacher_scores.detach().t(), dim=1),
|
| 93 |
-
reduction="batchmean",
|
| 94 |
-
)
|
| 95 |
-
return 0.5 * (forward + backward)
|
| 96 |
-
|
| 97 |
-
def _sign_projected_weight(self) -> torch.Tensor:
|
| 98 |
-
weight = static_embedding_weight(self.model)
|
| 99 |
-
norms = torch.linalg.vector_norm(weight.float(), dim=1).to(weight.dtype).clamp_min(1e-12)
|
| 100 |
-
unit = weight / norms.unsqueeze(1)
|
| 101 |
-
hard_unit = torch.where(unit >= 0, torch.ones_like(unit), -torch.ones_like(unit)) / math.sqrt(unit.shape[1])
|
| 102 |
-
straight_through_unit = unit + (hard_unit - unit).detach()
|
| 103 |
-
return norms.unsqueeze(1) * straight_through_unit
|
| 104 |
-
|
| 105 |
-
@staticmethod
|
| 106 |
-
def _mean_pool(features: dict[str, Any], weight: torch.Tensor) -> torch.Tensor:
|
| 107 |
-
if "input_ids" not in features or "offsets" not in features:
|
| 108 |
-
raise ValueError("Sign-projected training requires StaticEmbedding input_ids and offsets")
|
| 109 |
-
input_ids = features["input_ids"].to(device=weight.device, dtype=torch.long).view(-1)
|
| 110 |
-
offsets = features["offsets"].to(device=weight.device, dtype=torch.long).view(-1)
|
| 111 |
-
ends = torch.cat([offsets[1:], input_ids.new_tensor([input_ids.numel()])])
|
| 112 |
-
lengths = ends - offsets
|
| 113 |
-
segment_ids = torch.repeat_interleave(torch.arange(offsets.numel(), device=weight.device), lengths)
|
| 114 |
-
pooled = weight.new_zeros((offsets.numel(), weight.shape[1]))
|
| 115 |
-
pooled.index_add_(0, segment_ids, weight.index_select(0, input_ids))
|
| 116 |
-
return pooled / lengths.clamp_min(1).to(pooled.dtype).unsqueeze(1)
|
| 117 |
-
|
| 118 |
-
def _regularizer(self) -> torch.Tensor:
|
| 119 |
-
weight = static_embedding_weight(self.model)
|
| 120 |
-
if not any(
|
| 121 |
-
value > 0
|
| 122 |
-
for value in (self.cube_weight, self.balance_weight, self.independence_weight, self.unit_prior_weight)
|
| 123 |
-
):
|
| 124 |
-
return weight.sum() * 0.0
|
| 125 |
-
|
| 126 |
-
norms = torch.linalg.vector_norm(weight.float(), dim=1).to(weight.dtype).clamp_min(1e-12)
|
| 127 |
-
unit = weight / norms.unsqueeze(1)
|
| 128 |
-
initial_unit = self.initial_unit.to(device=unit.device, dtype=unit.dtype)
|
| 129 |
-
if 0 < self.regularizer_sample_size < unit.shape[0]:
|
| 130 |
-
rows = torch.randint(0, unit.shape[0], (self.regularizer_sample_size,), device=unit.device)
|
| 131 |
-
unit = unit.index_select(0, rows)
|
| 132 |
-
initial_unit = initial_unit.index_select(0, rows)
|
| 133 |
-
|
| 134 |
-
total = unit.sum() * 0.0
|
| 135 |
-
if self.cube_weight > 0:
|
| 136 |
-
total = total + self.cube_weight * (unit.abs().sum(dim=1) - math.sqrt(unit.shape[1])).square().mean()
|
| 137 |
-
if self.balance_weight > 0 or self.independence_weight > 0:
|
| 138 |
-
hard_bits = torch.where(unit >= 0, torch.ones_like(unit), -torch.ones_like(unit))
|
| 139 |
-
bits = unit + (hard_bits - unit).detach()
|
| 140 |
-
if self.balance_weight > 0:
|
| 141 |
-
total = total + self.balance_weight * bits.mean(dim=0).square().mean()
|
| 142 |
-
if self.independence_weight > 0:
|
| 143 |
-
correlation = bits.t() @ bits / float(bits.shape[0])
|
| 144 |
-
identity = torch.eye(correlation.shape[0], device=unit.device, dtype=unit.dtype)
|
| 145 |
-
total = total + self.independence_weight * (correlation - identity).square().mean()
|
| 146 |
-
if self.unit_prior_weight > 0:
|
| 147 |
-
total = total + self.unit_prior_weight * (1.0 - (unit * initial_unit).sum(dim=1)).mean()
|
| 148 |
-
return total
|
| 149 |
-
|
| 150 |
-
def _teacher_embeddings(
|
| 151 |
-
self,
|
| 152 |
-
sentence_features: Sequence[dict[str, Any]],
|
| 153 |
-
device: torch.device,
|
| 154 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 155 |
-
self.teacher.to(device)
|
| 156 |
-
self.teacher.eval()
|
| 157 |
-
with torch.no_grad():
|
| 158 |
-
embeddings = [
|
| 159 |
-
self.teacher(self._features_to_device(features, device))["sentence_embedding"]
|
| 160 |
-
for features in sentence_features
|
| 161 |
-
]
|
| 162 |
-
return embeddings[0], embeddings[1]
|
| 163 |
-
|
| 164 |
-
def forward(self, sentence_features: Sequence[dict[str, Any]], labels: Any = None) -> torch.Tensor:
|
| 165 |
-
del labels
|
| 166 |
-
if len(sentence_features) != 2:
|
| 167 |
-
raise ValueError("SignProjectedContrastiveLoss expects anchor-positive pairs")
|
| 168 |
-
|
| 169 |
-
device = static_embedding_weight(self.model).device
|
| 170 |
-
features = [self._features_to_device(item, device) for item in sentence_features]
|
| 171 |
-
projected_weight = self._sign_projected_weight()
|
| 172 |
-
anchor = self._mean_pool(features[0], projected_weight)
|
| 173 |
-
positive = self._mean_pool(features[1], projected_weight)
|
| 174 |
-
scores = self._scores(anchor, positive, self.scale)
|
| 175 |
-
total = self.pair_weight * self._symmetric_nce(scores)
|
| 176 |
-
|
| 177 |
-
if self.teacher_kl_weight > 0:
|
| 178 |
-
teacher_anchor, teacher_positive = self._teacher_embeddings(features, device)
|
| 179 |
-
teacher_scores = self._scores(teacher_anchor, teacher_positive, self.scale)
|
| 180 |
-
total = total + self.teacher_kl_weight * self._symmetric_kl(teacher_scores, scores)
|
| 181 |
-
|
| 182 |
-
return total + self._regularizer()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/loss_path_vocabulary.py
DELETED
|
@@ -1,161 +0,0 @@
|
|
| 1 |
-
"""Loss-path evidence and learned-cardinality vocabulary selection.
|
| 2 |
-
|
| 3 |
-
The training graph supplies occurrence-level attributions at three points on
|
| 4 |
-
the path from a disabled pair gate to its learned probability. This module is
|
| 5 |
-
deliberately independent of the model so the statistical rule and the matched
|
| 6 |
-
support control can be tested without CUDA.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
from dataclasses import dataclass, field
|
| 12 |
-
import math
|
| 13 |
-
from typing import Callable
|
| 14 |
-
|
| 15 |
-
import torch
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
@dataclass
|
| 19 |
-
class PathPass:
|
| 20 |
-
"""Capture one alpha pass, preserving occurrence order across cache replay."""
|
| 21 |
-
|
| 22 |
-
keys: list[torch.Tensor] = field(default_factory=list)
|
| 23 |
-
attribution_integrand: list[torch.Tensor] = field(default_factory=list)
|
| 24 |
-
|
| 25 |
-
def observe(
|
| 26 |
-
self, pair_keys: torch.Tensor, probability: torch.Tensor, gradient: torch.Tensor
|
| 27 |
-
) -> None:
|
| 28 |
-
if pair_keys.shape != probability.shape or pair_keys.shape != gradient.shape:
|
| 29 |
-
raise ValueError("loss-path observer tensors must align occurrence-wise")
|
| 30 |
-
finite = torch.isfinite(probability) & torch.isfinite(gradient)
|
| 31 |
-
if not bool(finite.all()):
|
| 32 |
-
raise FloatingPointError("loss-path observer received nonfinite values")
|
| 33 |
-
self.keys.append(pair_keys.detach().to(device="cpu", dtype=torch.long))
|
| 34 |
-
self.attribution_integrand.append(
|
| 35 |
-
(-probability.detach().float() * gradient.detach().float()).cpu()
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
def tensors(self) -> tuple[torch.Tensor, torch.Tensor]:
|
| 39 |
-
if not self.keys:
|
| 40 |
-
return torch.empty(0, dtype=torch.long), torch.empty(0)
|
| 41 |
-
return torch.cat(self.keys), torch.cat(self.attribution_integrand)
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
@dataclass
|
| 45 |
-
class HalfEvidence:
|
| 46 |
-
"""Exact occurrence statistics for one pre-registered audit half."""
|
| 47 |
-
|
| 48 |
-
occurrences: dict[int, int] = field(default_factory=dict)
|
| 49 |
-
positives: dict[int, int] = field(default_factory=dict)
|
| 50 |
-
attribution_sum: dict[int, float] = field(default_factory=dict)
|
| 51 |
-
|
| 52 |
-
def add_simpson(self, passes: tuple[PathPass, PathPass, PathPass]) -> None:
|
| 53 |
-
tensors = [path_pass.tensors() for path_pass in passes]
|
| 54 |
-
keys = tensors[0][0]
|
| 55 |
-
if any(not torch.equal(keys, candidate[0]) for candidate in tensors[1:]):
|
| 56 |
-
raise RuntimeError("loss-path occurrence order changed across alpha passes")
|
| 57 |
-
values = (tensors[0][1] + 4.0 * tensors[1][1] + tensors[2][1]) / 6.0
|
| 58 |
-
if not bool(torch.isfinite(values).all()):
|
| 59 |
-
raise FloatingPointError("Simpson loss-path attribution is nonfinite")
|
| 60 |
-
unique, inverse = torch.unique(keys, sorted=True, return_inverse=True)
|
| 61 |
-
counts = torch.bincount(inverse, minlength=unique.numel())
|
| 62 |
-
positive_counts = torch.zeros_like(counts)
|
| 63 |
-
positive_counts.index_add_(0, inverse, (values > 0).to(counts.dtype))
|
| 64 |
-
sums = torch.zeros(unique.numel(), dtype=torch.float64)
|
| 65 |
-
sums.index_add_(0, inverse, values.to(torch.float64))
|
| 66 |
-
for key, count, positive, total in zip(
|
| 67 |
-
unique.tolist(), counts.tolist(), positive_counts.tolist(), sums.tolist()
|
| 68 |
-
):
|
| 69 |
-
pair = int(key)
|
| 70 |
-
self.occurrences[pair] = self.occurrences.get(pair, 0) + int(count)
|
| 71 |
-
self.positives[pair] = self.positives.get(pair, 0) + int(positive)
|
| 72 |
-
self.attribution_sum[pair] = self.attribution_sum.get(pair, 0.0) + float(total)
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def exact_one_sided_lower_bound(positives: int, trials: int, alpha: float) -> float:
|
| 76 |
-
"""Clopper--Pearson lower bound for a binomial positive-attribution rate."""
|
| 77 |
-
|
| 78 |
-
if not 0 <= positives <= trials or trials <= 0:
|
| 79 |
-
raise ValueError("binomial counts must satisfy 0 <= positives <= trials")
|
| 80 |
-
if not 0.0 < alpha < 1.0:
|
| 81 |
-
raise ValueError("alpha must lie strictly between zero and one")
|
| 82 |
-
if positives == 0:
|
| 83 |
-
return 0.0
|
| 84 |
-
from scipy.stats import beta
|
| 85 |
-
|
| 86 |
-
return float(beta.ppf(alpha, positives, trials - positives + 1))
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def select_loss_path_pairs(
|
| 90 |
-
first: HalfEvidence,
|
| 91 |
-
second: HalfEvidence,
|
| 92 |
-
*,
|
| 93 |
-
familywise_alpha: float = 0.05,
|
| 94 |
-
null_rate: float = 0.5,
|
| 95 |
-
) -> tuple[list[int], list[dict[str, float | int]]]:
|
| 96 |
-
"""Learn cardinality by requiring corrected positive evidence in both halves."""
|
| 97 |
-
|
| 98 |
-
first_keys = set(first.occurrences)
|
| 99 |
-
second_keys = set(second.occurrences)
|
| 100 |
-
candidates = sorted(first_keys & second_keys)
|
| 101 |
-
if not candidates:
|
| 102 |
-
return [], []
|
| 103 |
-
# Correct over every pair tested in either half, not the data-dependent
|
| 104 |
-
# intersection that is eligible for the final two-window set.
|
| 105 |
-
per_test_alpha = float(familywise_alpha) / (2 * len(first_keys | second_keys))
|
| 106 |
-
rows: list[dict[str, float | int]] = []
|
| 107 |
-
selected: list[int] = []
|
| 108 |
-
for key in candidates:
|
| 109 |
-
n1, n2 = first.occurrences[key], second.occurrences[key]
|
| 110 |
-
p1, p2 = first.positives.get(key, 0), second.positives.get(key, 0)
|
| 111 |
-
lower1 = exact_one_sided_lower_bound(p1, n1, per_test_alpha)
|
| 112 |
-
lower2 = exact_one_sided_lower_bound(p2, n2, per_test_alpha)
|
| 113 |
-
survives = lower1 > null_rate and lower2 > null_rate
|
| 114 |
-
if survives:
|
| 115 |
-
selected.append(key)
|
| 116 |
-
rows.append({
|
| 117 |
-
"pair_key": key,
|
| 118 |
-
"half1_occurrences": n1,
|
| 119 |
-
"half1_positives": p1,
|
| 120 |
-
"half1_lower": lower1,
|
| 121 |
-
"half2_occurrences": n2,
|
| 122 |
-
"half2_positives": p2,
|
| 123 |
-
"half2_lower": lower2,
|
| 124 |
-
"attribution_sum": (
|
| 125 |
-
first.attribution_sum.get(key, 0.0)
|
| 126 |
-
+ second.attribution_sum.get(key, 0.0)
|
| 127 |
-
),
|
| 128 |
-
"selected": int(survives),
|
| 129 |
-
})
|
| 130 |
-
return selected, rows
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
def select_support_control(
|
| 134 |
-
first: HalfEvidence, second: HalfEvidence, *, count: int
|
| 135 |
-
) -> list[int]:
|
| 136 |
-
"""Select exactly ``count`` pairs without reading attribution values."""
|
| 137 |
-
|
| 138 |
-
if count < 0:
|
| 139 |
-
raise ValueError("support-control count must be non-negative")
|
| 140 |
-
common = set(first.occurrences) & set(second.occurrences)
|
| 141 |
-
ranked = sorted(
|
| 142 |
-
common,
|
| 143 |
-
key=lambda key: (
|
| 144 |
-
-min(first.occurrences[key], second.occurrences[key]),
|
| 145 |
-
-(first.occurrences[key] + second.occurrences[key]),
|
| 146 |
-
key,
|
| 147 |
-
),
|
| 148 |
-
)
|
| 149 |
-
if count > len(ranked):
|
| 150 |
-
raise ValueError("support inventory is smaller than learned cardinality")
|
| 151 |
-
return sorted(ranked[:count])
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def completeness_error(
|
| 155 |
-
losses: tuple[float, float, float], attribution_sum: float
|
| 156 |
-
) -> tuple[float, float]:
|
| 157 |
-
"""Return absolute and scale-normalized Simpson completeness residual."""
|
| 158 |
-
|
| 159 |
-
target = float(losses[0]) - float(losses[2])
|
| 160 |
-
absolute = abs(target - float(attribution_sum))
|
| 161 |
-
return absolute, absolute / max(abs(target), 1e-12)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/ngram_table.py
DELETED
|
@@ -1,86 +0,0 @@
|
|
| 1 |
-
"""Hashed token n-gram rows for a static encoder.
|
| 2 |
-
|
| 3 |
-
Fifteen representation-side interventions in this repo produced nothing, and the
|
| 4 |
-
per-language numbers say why: quality tracks *tokenisation*, not capacity.
|
| 5 |
-
Spearman between tokens-per-word and held-out Recall@1 is -0.607 over 57
|
| 6 |
-
space-separated languages; the third that tokenise worst average 0.191 against
|
| 7 |
-
0.295 for the third that tokenise best. Georgian needs 5.7 tokens per word and
|
| 8 |
-
scores 0.050.
|
| 9 |
-
|
| 10 |
-
Those languages do not lack a good vector for a word — they lack any row that
|
| 11 |
-
*means* a word. A Transformer reassembles the pieces with attention; a
|
| 12 |
-
mean-pooled static encoder cannot. The only repair available to it is a bigger
|
| 13 |
-
lookup unit.
|
| 14 |
-
|
| 15 |
-
So each adjacent token n-gram inside a sentence gets a row, hashed into a fixed
|
| 16 |
-
table, pooled alongside the unigram rows:
|
| 17 |
-
|
| 18 |
-
h(x) = ( sum_w E[w] + sum_g F[hash(g)] ) / (|U| + |G|)
|
| 19 |
-
|
| 20 |
-
Allocation across languages needs no rule: a fragmented language simply emits
|
| 21 |
-
more distinct n-grams and takes more of the table, while a language that already
|
| 22 |
-
tokenises into words gains little. The budget follows the deficit by itself.
|
| 23 |
-
|
| 24 |
-
Rows start at zero, so the pooled direction — and therefore every cosine — is
|
| 25 |
-
identical to the unconditioned model at step zero, while the rows still receive
|
| 26 |
-
gradient from the first batch. Unlike every conditioning head tried here, an
|
| 27 |
-
n-gram row is a *new input feature* rather than a second route to an existing
|
| 28 |
-
one, so the shared table cannot absorb it: the feature does not otherwise exist.
|
| 29 |
-
"""
|
| 30 |
-
|
| 31 |
-
from __future__ import annotations
|
| 32 |
-
|
| 33 |
-
import torch
|
| 34 |
-
from torch import nn
|
| 35 |
-
|
| 36 |
-
# Odd 32-bit constants; the product mixes the low bits that adjacent token ids
|
| 37 |
-
# share, which matters because ids are correlated within a language.
|
| 38 |
-
_MIX_A = 2654435761
|
| 39 |
-
_MIX_B = 40503
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
class NgramTable(nn.Module):
|
| 43 |
-
def __init__(self, buckets: int, dim: int, orders: tuple[int, ...] = (2,)) -> None:
|
| 44 |
-
super().__init__()
|
| 45 |
-
if any(order < 2 for order in orders):
|
| 46 |
-
raise ValueError("n-gram orders must be at least 2")
|
| 47 |
-
self.buckets = int(buckets)
|
| 48 |
-
self.orders = tuple(orders)
|
| 49 |
-
self.rows = nn.Parameter(torch.zeros(self.buckets, dim))
|
| 50 |
-
|
| 51 |
-
def extra_repr(self) -> str:
|
| 52 |
-
return f"buckets={self.buckets}, orders={self.orders}"
|
| 53 |
-
|
| 54 |
-
def _hash(self, ids: torch.Tensor, order: int) -> torch.Tensor:
|
| 55 |
-
value = torch.zeros_like(ids[0])
|
| 56 |
-
for position in range(order):
|
| 57 |
-
value = value * _MIX_A + ids[position] * _MIX_B + position
|
| 58 |
-
return value.abs() % self.buckets
|
| 59 |
-
|
| 60 |
-
def gather(
|
| 61 |
-
self,
|
| 62 |
-
content: torch.Tensor,
|
| 63 |
-
segment: torch.Tensor,
|
| 64 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 65 |
-
"""Rows and their sentence index for every n-gram inside a sentence."""
|
| 66 |
-
vectors: list[torch.Tensor] = []
|
| 67 |
-
segments: list[torch.Tensor] = []
|
| 68 |
-
for order in self.orders:
|
| 69 |
-
if content.numel() <= order:
|
| 70 |
-
continue
|
| 71 |
-
window = [content[i: content.numel() - order + 1 + i] for i in range(order)]
|
| 72 |
-
starts = segment[: segment.numel() - order + 1]
|
| 73 |
-
# An n-gram must lie inside one sentence, so every position in the
|
| 74 |
-
# window has to carry the same segment id.
|
| 75 |
-
same = torch.ones_like(starts, dtype=torch.bool)
|
| 76 |
-
for i in range(1, order):
|
| 77 |
-
same &= segment[i: segment.numel() - order + 1 + i] == starts
|
| 78 |
-
if not bool(same.any()):
|
| 79 |
-
continue
|
| 80 |
-
index = self._hash([w[same] for w in window], order)
|
| 81 |
-
vectors.append(self.rows[index])
|
| 82 |
-
segments.append(starts[same])
|
| 83 |
-
if not vectors:
|
| 84 |
-
empty = content.new_zeros((0,), dtype=torch.long)
|
| 85 |
-
return self.rows.new_zeros((0, self.rows.shape[1])), empty
|
| 86 |
-
return torch.cat(vectors), torch.cat(segments)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/online_tokenizer.py
DELETED
|
@@ -1,378 +0,0 @@
|
|
| 1 |
-
"""Learned hard adjacent-pair tokenization for a static embedding table."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections import deque
|
| 6 |
-
import math
|
| 7 |
-
from typing import TYPE_CHECKING
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
-
from torch import nn
|
| 11 |
-
from torch.nn import functional as F
|
| 12 |
-
|
| 13 |
-
from starse.collapse import _MIX_A, _MIX_B
|
| 14 |
-
from starse.structured_tokenizer import batched_matching, structured_straight_through
|
| 15 |
-
|
| 16 |
-
if TYPE_CHECKING:
|
| 17 |
-
from starse.collapse import CollapseTable
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
class OnlineMergeTable(nn.Module):
|
| 21 |
-
"""Score every adjacent pair and emit a legal hard MAP token sequence."""
|
| 22 |
-
|
| 23 |
-
def __init__(
|
| 24 |
-
self,
|
| 25 |
-
buckets: int,
|
| 26 |
-
dim: int,
|
| 27 |
-
n_languages: int,
|
| 28 |
-
*,
|
| 29 |
-
rank: int = 32,
|
| 30 |
-
bias_init: float = -2.0,
|
| 31 |
-
pair_key_base: int,
|
| 32 |
-
compiled_pair_keys: torch.Tensor | None = None,
|
| 33 |
-
compiled_pair_scores: torch.Tensor | None = None,
|
| 34 |
-
) -> None:
|
| 35 |
-
super().__init__()
|
| 36 |
-
if buckets <= 0 or dim <= 0 or n_languages <= 0 or rank <= 0:
|
| 37 |
-
raise ValueError("online tokenizer dimensions must be positive")
|
| 38 |
-
if pair_key_base <= 0:
|
| 39 |
-
raise ValueError("pair_key_base must be positive")
|
| 40 |
-
if (compiled_pair_keys is None) != (compiled_pair_scores is None):
|
| 41 |
-
raise ValueError("compiled pair keys and scores must be provided together")
|
| 42 |
-
|
| 43 |
-
self.buckets = int(buckets)
|
| 44 |
-
self.dim = int(dim)
|
| 45 |
-
self.rank = int(rank)
|
| 46 |
-
self.pair_key_base = int(pair_key_base)
|
| 47 |
-
self.compiled = compiled_pair_keys is not None
|
| 48 |
-
keys = torch.as_tensor(
|
| 49 |
-
[] if compiled_pair_keys is None else compiled_pair_keys, dtype=torch.long
|
| 50 |
-
)
|
| 51 |
-
scores = torch.as_tensor(
|
| 52 |
-
[] if compiled_pair_scores is None else compiled_pair_scores,
|
| 53 |
-
dtype=torch.float32,
|
| 54 |
-
)
|
| 55 |
-
if keys.ndim != 1 or scores.ndim != 1 or keys.numel() != scores.numel():
|
| 56 |
-
raise ValueError("compiled pair keys and scores must have the same length")
|
| 57 |
-
if keys.numel() and not bool((keys[1:] > keys[:-1]).all()):
|
| 58 |
-
raise ValueError("compiled pair keys must be strictly increasing")
|
| 59 |
-
if keys.numel() and (int(keys.min()) < 0 or int(keys.max()) >= pair_key_base**2):
|
| 60 |
-
raise ValueError("compiled pair key is outside pair_key_base")
|
| 61 |
-
if not bool(torch.isfinite(scores).all()):
|
| 62 |
-
raise ValueError("compiled pair scores must be finite")
|
| 63 |
-
self.register_buffer("compiled_pair_keys", keys, persistent=True)
|
| 64 |
-
self.register_buffer("compiled_pair_scores", scores, persistent=True)
|
| 65 |
-
|
| 66 |
-
rows = keys.numel() if self.compiled else self.buckets
|
| 67 |
-
self.merged = nn.Parameter(
|
| 68 |
-
torch.zeros(rows, self.dim), requires_grad=not self.compiled
|
| 69 |
-
)
|
| 70 |
-
self.language_bias = nn.Parameter(
|
| 71 |
-
torch.full((n_languages,), float(bias_init)), requires_grad=not self.compiled
|
| 72 |
-
)
|
| 73 |
-
if not self.compiled:
|
| 74 |
-
self.score_hash = nn.Parameter(torch.zeros(self.buckets))
|
| 75 |
-
self.left_projection = nn.Parameter(torch.empty(self.rank, self.dim))
|
| 76 |
-
self.right_projection = nn.Parameter(torch.empty(self.rank, self.dim))
|
| 77 |
-
self.interaction_weight = nn.Parameter(torch.zeros(self.rank))
|
| 78 |
-
nn.init.xavier_uniform_(self.left_projection)
|
| 79 |
-
nn.init.xavier_uniform_(self.right_projection)
|
| 80 |
-
|
| 81 |
-
self.last_token_count = torch.empty(0)
|
| 82 |
-
self.last_map_edges = torch.empty(0, 0, dtype=torch.bool)
|
| 83 |
-
self._vocabulary_telemetry = None
|
| 84 |
-
self._cache_audit_active = False
|
| 85 |
-
self._cache_phase: str | None = None
|
| 86 |
-
self._cache_records: deque[
|
| 87 |
-
tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]
|
| 88 |
-
] = deque()
|
| 89 |
-
|
| 90 |
-
def extra_repr(self) -> str:
|
| 91 |
-
return (
|
| 92 |
-
f"compiled={self.compiled}, buckets={self.buckets}, "
|
| 93 |
-
f"exact_pairs={self.compiled_pair_keys.numel()}, dim={self.dim}, rank={self.rank}, "
|
| 94 |
-
f"languages={self.language_bias.numel()}"
|
| 95 |
-
)
|
| 96 |
-
|
| 97 |
-
def set_vocabulary_telemetry(self, window) -> None:
|
| 98 |
-
"""Attach detached run telemetry without registering module state."""
|
| 99 |
-
|
| 100 |
-
self._vocabulary_telemetry = window
|
| 101 |
-
|
| 102 |
-
def _hash(self, left_ids: torch.Tensor, right_ids: torch.Tensor) -> torch.Tensor:
|
| 103 |
-
return ((left_ids * _MIX_A + right_ids * _MIX_B).abs()) % self.buckets
|
| 104 |
-
|
| 105 |
-
def start_cache_audit(self) -> None:
|
| 106 |
-
if self._cache_phase is not None or self._cache_records:
|
| 107 |
-
raise RuntimeError("previous online tokenizer cache audit is incomplete")
|
| 108 |
-
self._cache_audit_active = True
|
| 109 |
-
|
| 110 |
-
def begin_cache_record(self) -> None:
|
| 111 |
-
if self._cache_audit_active:
|
| 112 |
-
if self._cache_phase is not None:
|
| 113 |
-
raise RuntimeError("online tokenizer cache phase is already active")
|
| 114 |
-
self._cache_phase = "record"
|
| 115 |
-
|
| 116 |
-
def begin_cache_replay(self) -> None:
|
| 117 |
-
if self._cache_audit_active:
|
| 118 |
-
if self._cache_phase is not None:
|
| 119 |
-
raise RuntimeError("online tokenizer cache phase is already active")
|
| 120 |
-
self._cache_phase = "replay"
|
| 121 |
-
|
| 122 |
-
def end_cache_phase(self) -> None:
|
| 123 |
-
self._cache_phase = None
|
| 124 |
-
|
| 125 |
-
def abort_cache_audit(self) -> None:
|
| 126 |
-
self._cache_records.clear()
|
| 127 |
-
self._cache_phase = None
|
| 128 |
-
self._cache_audit_active = False
|
| 129 |
-
|
| 130 |
-
def assert_cache_audit_empty(self) -> None:
|
| 131 |
-
if self._cache_phase is not None or self._cache_records:
|
| 132 |
-
raise RuntimeError("online tokenizer cache audit did not replay every record")
|
| 133 |
-
self._cache_audit_active = False
|
| 134 |
-
|
| 135 |
-
def _audit_map(
|
| 136 |
-
self,
|
| 137 |
-
content: torch.Tensor,
|
| 138 |
-
segment: torch.Tensor,
|
| 139 |
-
language: torch.Tensor,
|
| 140 |
-
map_edges: torch.Tensor,
|
| 141 |
-
) -> None:
|
| 142 |
-
if not self._cache_audit_active or self._cache_phase is None:
|
| 143 |
-
return
|
| 144 |
-
values = tuple(
|
| 145 |
-
tensor.detach().clone()
|
| 146 |
-
for tensor in (content, segment, language, map_edges)
|
| 147 |
-
)
|
| 148 |
-
if self._cache_phase == "record":
|
| 149 |
-
self._cache_records.append(values)
|
| 150 |
-
return
|
| 151 |
-
if self._cache_phase != "replay" or not self._cache_records:
|
| 152 |
-
self.abort_cache_audit()
|
| 153 |
-
raise RuntimeError("cache/replay MAP mismatch")
|
| 154 |
-
expected = self._cache_records.popleft()
|
| 155 |
-
if not all(
|
| 156 |
-
observed.shape == wanted.shape and torch.equal(observed, wanted)
|
| 157 |
-
for observed, wanted in zip(values, expected, strict=True)
|
| 158 |
-
):
|
| 159 |
-
self.abort_cache_audit()
|
| 160 |
-
raise RuntimeError("cache/replay MAP mismatch")
|
| 161 |
-
|
| 162 |
-
def seed_from_collapse(self, source: "CollapseTable") -> None:
|
| 163 |
-
"""Copy the registered Q15 tensors without sharing parameter storage."""
|
| 164 |
-
|
| 165 |
-
if self.compiled:
|
| 166 |
-
raise ValueError("cannot seed a compiled online tokenizer from Q15 hashes")
|
| 167 |
-
expected = {
|
| 168 |
-
"merged": tuple(self.merged.shape),
|
| 169 |
-
"score": tuple(self.score_hash.shape),
|
| 170 |
-
"language_bias": tuple(self.language_bias.shape),
|
| 171 |
-
}
|
| 172 |
-
observed = {
|
| 173 |
-
"merged": tuple(source.merged.shape),
|
| 174 |
-
"score": tuple(source.score.shape),
|
| 175 |
-
"language_bias": tuple(source.language_bias.shape),
|
| 176 |
-
}
|
| 177 |
-
if observed != expected:
|
| 178 |
-
raise ValueError(
|
| 179 |
-
f"Q15 source tensors do not fit online table: {observed} != {expected}"
|
| 180 |
-
)
|
| 181 |
-
with torch.no_grad():
|
| 182 |
-
self.merged.copy_(source.merged)
|
| 183 |
-
self.score_hash.copy_(source.score)
|
| 184 |
-
self.language_bias.copy_(source.language_bias)
|
| 185 |
-
|
| 186 |
-
def _edge_scores_and_index(
|
| 187 |
-
self,
|
| 188 |
-
left_ids: torch.Tensor,
|
| 189 |
-
right_ids: torch.Tensor,
|
| 190 |
-
language_ids: torch.Tensor,
|
| 191 |
-
embedding_weight: torch.Tensor,
|
| 192 |
-
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 193 |
-
if not (
|
| 194 |
-
left_ids.ndim == right_ids.ndim == language_ids.ndim == 1
|
| 195 |
-
and left_ids.shape == right_ids.shape == language_ids.shape
|
| 196 |
-
):
|
| 197 |
-
raise ValueError("edge scorer needs aligned one-dimensional tensors")
|
| 198 |
-
if embedding_weight.ndim != 2 or embedding_weight.shape[1] != self.dim:
|
| 199 |
-
raise ValueError("embedding_weight has the wrong shape")
|
| 200 |
-
if left_ids.numel() and (
|
| 201 |
-
int(torch.minimum(left_ids.min(), right_ids.min())) < 0
|
| 202 |
-
or int(torch.maximum(left_ids.max(), right_ids.max()))
|
| 203 |
-
>= embedding_weight.shape[0]
|
| 204 |
-
):
|
| 205 |
-
raise ValueError("pair token id is outside the embedding table")
|
| 206 |
-
if language_ids.numel() and (
|
| 207 |
-
int(language_ids.min()) < 0
|
| 208 |
-
or int(language_ids.max()) >= self.language_bias.numel()
|
| 209 |
-
):
|
| 210 |
-
raise ValueError("pair language id is outside the bias table")
|
| 211 |
-
|
| 212 |
-
if self.compiled:
|
| 213 |
-
keys = left_ids.to(torch.long) * self.pair_key_base + right_ids.to(torch.long)
|
| 214 |
-
if self.compiled_pair_keys.numel():
|
| 215 |
-
index = torch.searchsorted(self.compiled_pair_keys, keys)
|
| 216 |
-
safe_index = index.clamp(max=self.compiled_pair_keys.numel() - 1)
|
| 217 |
-
found = (index < self.compiled_pair_keys.numel()) & (
|
| 218 |
-
self.compiled_pair_keys.index_select(0, safe_index) == keys
|
| 219 |
-
)
|
| 220 |
-
global_score = self.compiled_pair_scores.index_select(0, safe_index)
|
| 221 |
-
else:
|
| 222 |
-
safe_index = torch.zeros_like(keys)
|
| 223 |
-
found = torch.zeros_like(keys, dtype=torch.bool)
|
| 224 |
-
global_score = torch.zeros_like(keys, dtype=torch.float32)
|
| 225 |
-
scores = torch.where(
|
| 226 |
-
found,
|
| 227 |
-
global_score
|
| 228 |
-
+ self.language_bias.index_select(0, language_ids).float(),
|
| 229 |
-
torch.zeros_like(global_score),
|
| 230 |
-
)
|
| 231 |
-
index = safe_index
|
| 232 |
-
else:
|
| 233 |
-
index = self._hash(left_ids, right_ids)
|
| 234 |
-
device_type = embedding_weight.device.type
|
| 235 |
-
with torch.autocast(device_type=device_type, enabled=False):
|
| 236 |
-
left = embedding_weight.index_select(0, left_ids).float()
|
| 237 |
-
right = embedding_weight.index_select(0, right_ids).float()
|
| 238 |
-
left_code = torch.tanh(F.linear(left, self.left_projection.float()))
|
| 239 |
-
right_code = torch.tanh(F.linear(right, self.right_projection.float()))
|
| 240 |
-
amortized = (
|
| 241 |
-
(left_code * right_code * self.interaction_weight.float()).sum(dim=1)
|
| 242 |
-
/ math.sqrt(self.rank)
|
| 243 |
-
)
|
| 244 |
-
scores = (
|
| 245 |
-
self.score_hash.index_select(0, index).float()
|
| 246 |
-
+ self.language_bias.index_select(0, language_ids).float()
|
| 247 |
-
+ amortized
|
| 248 |
-
)
|
| 249 |
-
found = torch.ones_like(index, dtype=torch.bool)
|
| 250 |
-
if not bool(torch.isfinite(scores).all()):
|
| 251 |
-
raise FloatingPointError("nonfinite online tokenizer edge score")
|
| 252 |
-
return scores, index, found
|
| 253 |
-
|
| 254 |
-
def edge_scores(
|
| 255 |
-
self,
|
| 256 |
-
left_ids: torch.Tensor,
|
| 257 |
-
right_ids: torch.Tensor,
|
| 258 |
-
language_ids: torch.Tensor,
|
| 259 |
-
embedding_weight: torch.Tensor,
|
| 260 |
-
) -> torch.Tensor:
|
| 261 |
-
"""Return float32 occurrence scores for aligned ids and languages."""
|
| 262 |
-
|
| 263 |
-
scores, _, _ = self._edge_scores_and_index(
|
| 264 |
-
left_ids, right_ids, language_ids, embedding_weight
|
| 265 |
-
)
|
| 266 |
-
return scores
|
| 267 |
-
|
| 268 |
-
@staticmethod
|
| 269 |
-
def _sentence_layout(
|
| 270 |
-
segment: torch.Tensor, n_sentences: int
|
| 271 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 272 |
-
lengths = torch.zeros(
|
| 273 |
-
n_sentences, dtype=torch.long, device=segment.device
|
| 274 |
-
)
|
| 275 |
-
lengths.index_add_(0, segment, torch.ones_like(segment))
|
| 276 |
-
if bool((lengths < 1).any()):
|
| 277 |
-
raise ValueError("online tokenizer requires at least one token per sentence")
|
| 278 |
-
starts = lengths.cumsum(0) - lengths
|
| 279 |
-
return lengths, starts
|
| 280 |
-
|
| 281 |
-
def pool(
|
| 282 |
-
self,
|
| 283 |
-
vectors: torch.Tensor,
|
| 284 |
-
content: torch.Tensor,
|
| 285 |
-
segment: torch.Tensor,
|
| 286 |
-
language: torch.Tensor,
|
| 287 |
-
n_sentences: int,
|
| 288 |
-
*,
|
| 289 |
-
embedding_weight: torch.Tensor,
|
| 290 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 291 |
-
"""Pool the literal tokens emitted by hard MAP with marginal gradients."""
|
| 292 |
-
|
| 293 |
-
if vectors.ndim != 2 or vectors.shape != (content.numel(), self.dim):
|
| 294 |
-
raise ValueError("vectors must align with content and the table dimension")
|
| 295 |
-
if content.ndim != 1 or segment.ndim != 1 or content.shape != segment.shape:
|
| 296 |
-
raise ValueError("content and segment must be aligned one-dimensional tensors")
|
| 297 |
-
if language.shape != (n_sentences,):
|
| 298 |
-
raise ValueError("language must contain one id per sentence")
|
| 299 |
-
if segment.numel() and (
|
| 300 |
-
int(segment.min()) < 0 or int(segment.max()) >= n_sentences
|
| 301 |
-
):
|
| 302 |
-
raise ValueError("segment index is outside the sentence batch")
|
| 303 |
-
lengths, starts = self._sentence_layout(segment, n_sentences)
|
| 304 |
-
|
| 305 |
-
numerator = torch.zeros(
|
| 306 |
-
n_sentences, self.dim, dtype=vectors.dtype, device=vectors.device
|
| 307 |
-
)
|
| 308 |
-
numerator.index_add_(0, segment, vectors)
|
| 309 |
-
token_count = lengths.to(dtype=vectors.dtype)
|
| 310 |
-
maximum_edges = int((lengths - 1).max().item())
|
| 311 |
-
edge_mask = torch.zeros(
|
| 312 |
-
n_sentences, maximum_edges, dtype=torch.bool, device=vectors.device
|
| 313 |
-
)
|
| 314 |
-
padded_scores = torch.zeros(
|
| 315 |
-
n_sentences, maximum_edges, dtype=torch.float32, device=vectors.device
|
| 316 |
-
)
|
| 317 |
-
|
| 318 |
-
if maximum_edges:
|
| 319 |
-
inside = segment[:-1] == segment[1:]
|
| 320 |
-
positions = torch.nonzero(inside, as_tuple=False).squeeze(1)
|
| 321 |
-
edge_segment = segment.index_select(0, positions)
|
| 322 |
-
within = positions - starts.index_select(0, edge_segment)
|
| 323 |
-
left_ids = content.index_select(0, positions)
|
| 324 |
-
right_ids = content.index_select(0, positions + 1)
|
| 325 |
-
edge_language = language.index_select(0, edge_segment)
|
| 326 |
-
scores, pair_index, eligible = self._edge_scores_and_index(
|
| 327 |
-
left_ids, right_ids, edge_language, embedding_weight
|
| 328 |
-
)
|
| 329 |
-
eligible_segment = edge_segment[eligible]
|
| 330 |
-
eligible_within = within[eligible]
|
| 331 |
-
edge_mask[eligible_segment, eligible_within] = True
|
| 332 |
-
padded_scores[eligible_segment, eligible_within] = scores[eligible]
|
| 333 |
-
matching = batched_matching(padded_scores, edge_mask)
|
| 334 |
-
if self.training:
|
| 335 |
-
padded_edges = structured_straight_through(
|
| 336 |
-
matching.marginals, matching.map_edges, dtype=vectors.dtype
|
| 337 |
-
)
|
| 338 |
-
else:
|
| 339 |
-
padded_edges = matching.map_edges.to(dtype=vectors.dtype)
|
| 340 |
-
edges = padded_edges[eligible_segment, eligible_within]
|
| 341 |
-
pair_vectors = self.merged.index_select(0, pair_index[eligible]).to(vectors.dtype)
|
| 342 |
-
eligible_positions = positions[eligible]
|
| 343 |
-
delta = (
|
| 344 |
-
pair_vectors
|
| 345 |
-
- vectors.index_select(0, eligible_positions)
|
| 346 |
-
- vectors.index_select(0, eligible_positions + 1)
|
| 347 |
-
)
|
| 348 |
-
numerator.index_add_(0, eligible_segment, delta * edges.unsqueeze(1))
|
| 349 |
-
token_count.index_add_(0, eligible_segment, -edges)
|
| 350 |
-
map_edges = matching.map_edges
|
| 351 |
-
telemetry_marginals = matching.marginals
|
| 352 |
-
telemetry_hash_indices = pair_index[eligible]
|
| 353 |
-
else:
|
| 354 |
-
map_edges = edge_mask
|
| 355 |
-
telemetry_marginals = torch.zeros_like(padded_scores)
|
| 356 |
-
telemetry_hash_indices = torch.empty(
|
| 357 |
-
0, dtype=torch.long, device=content.device
|
| 358 |
-
)
|
| 359 |
-
|
| 360 |
-
if not bool(torch.isfinite(token_count).all()) or bool((token_count < 1).any()):
|
| 361 |
-
raise FloatingPointError("invalid online tokenizer token count")
|
| 362 |
-
mean = numerator / token_count.unsqueeze(1)
|
| 363 |
-
embeddings = F.normalize(mean, dim=1)
|
| 364 |
-
if not bool(torch.isfinite(embeddings).all()):
|
| 365 |
-
raise FloatingPointError("nonfinite online tokenizer sentence embedding")
|
| 366 |
-
self.last_token_count = token_count.detach()
|
| 367 |
-
self.last_map_edges = map_edges.detach()
|
| 368 |
-
if self._vocabulary_telemetry is not None:
|
| 369 |
-
self._vocabulary_telemetry.observe_online(
|
| 370 |
-
base_tokens=int(content.numel()),
|
| 371 |
-
edge_scores=padded_scores,
|
| 372 |
-
marginals=telemetry_marginals,
|
| 373 |
-
map_edges=map_edges,
|
| 374 |
-
hash_indices=telemetry_hash_indices,
|
| 375 |
-
edge_mask=edge_mask,
|
| 376 |
-
)
|
| 377 |
-
self._audit_map(content, segment, language, map_edges)
|
| 378 |
-
return embeddings, map_edges
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/online_tokenizer_compilation.py
DELETED
|
@@ -1,686 +0,0 @@
|
|
| 1 |
-
"""Bounded-memory compilation of an online tokenizer to exact static routing."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
import hashlib
|
| 7 |
-
import heapq
|
| 8 |
-
import itertools
|
| 9 |
-
import json
|
| 10 |
-
import os
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
import shutil
|
| 13 |
-
import struct
|
| 14 |
-
from typing import Iterator, Mapping
|
| 15 |
-
|
| 16 |
-
import pyarrow.parquet as pq
|
| 17 |
-
import torch
|
| 18 |
-
|
| 19 |
-
from starse.conditioned_module import ConditionedStaticEmbedding, marker_for
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
_OCCURRENCE = struct.Struct("<QIIQ")
|
| 23 |
-
_RETAINED = struct.Struct("<Qf")
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
@dataclass(frozen=True, order=True)
|
| 27 |
-
class PairOccurrence:
|
| 28 |
-
key: int
|
| 29 |
-
language_index: int
|
| 30 |
-
source_index: int
|
| 31 |
-
count: int = 1
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def _sha256(path: Path) -> str:
|
| 35 |
-
digest = hashlib.sha256()
|
| 36 |
-
with path.open("rb") as handle:
|
| 37 |
-
for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
| 38 |
-
digest.update(chunk)
|
| 39 |
-
return digest.hexdigest()
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def _load_manifest(path: Path) -> dict[str, object]:
|
| 43 |
-
raw = json.loads(path.read_text(encoding="utf-8"))
|
| 44 |
-
if raw.get("protocol") != "multilingual-sota-corpus-manifest-v1":
|
| 45 |
-
raise ValueError("unknown online tokenizer corpus manifest")
|
| 46 |
-
groups = raw.get("groups")
|
| 47 |
-
if not isinstance(groups, list) or not groups:
|
| 48 |
-
raise ValueError("online tokenizer corpus manifest has no groups")
|
| 49 |
-
return raw
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def _resolve_file(
|
| 53 |
-
record: Mapping[str, object], source: str, roots: Mapping[str, Path]
|
| 54 |
-
) -> Path:
|
| 55 |
-
direct = Path(str(record.get("path", ""))).expanduser()
|
| 56 |
-
if direct.is_file():
|
| 57 |
-
return direct.resolve()
|
| 58 |
-
if source not in roots:
|
| 59 |
-
raise FileNotFoundError(direct)
|
| 60 |
-
resolved = (roots[source] / str(record["relative_path"])).resolve()
|
| 61 |
-
if not resolved.is_file():
|
| 62 |
-
raise FileNotFoundError(resolved)
|
| 63 |
-
return resolved
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def _iter_group_texts(
|
| 67 |
-
group: Mapping[str, object], roots: Mapping[str, Path]
|
| 68 |
-
) -> Iterator[tuple[str, str]]:
|
| 69 |
-
source = str(group["source"])
|
| 70 |
-
language = str(group["language"])
|
| 71 |
-
anchor_language = str(group.get("anchor_language") or language)
|
| 72 |
-
columns = (
|
| 73 |
-
(str(group["anchor_column"]), anchor_language),
|
| 74 |
-
(str(group["positive_column"]), language),
|
| 75 |
-
)
|
| 76 |
-
files = group.get("files")
|
| 77 |
-
if not isinstance(files, list) or not files:
|
| 78 |
-
raise ValueError("trainable compiler group has no files")
|
| 79 |
-
for record in files:
|
| 80 |
-
if not isinstance(record, dict):
|
| 81 |
-
raise ValueError("compiler file record must be an object")
|
| 82 |
-
parquet = pq.ParquetFile(_resolve_file(record, source, roots))
|
| 83 |
-
requested = sorted({column for column, _ in columns})
|
| 84 |
-
for batch in parquet.iter_batches(batch_size=4096, columns=requested):
|
| 85 |
-
values = batch.to_pydict()
|
| 86 |
-
for column, column_language in columns:
|
| 87 |
-
for text in values[column]:
|
| 88 |
-
if isinstance(text, str) and text:
|
| 89 |
-
yield column_language, text
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def _flush_occurrence_run(
|
| 93 |
-
rows: list[PairOccurrence], temporary_root: Path, run_index: int
|
| 94 |
-
) -> Path:
|
| 95 |
-
counts: dict[tuple[int, int, int], int] = {}
|
| 96 |
-
for row in rows:
|
| 97 |
-
if row.count <= 0:
|
| 98 |
-
raise ValueError("compiler occurrence count must be positive")
|
| 99 |
-
identity = (row.key, row.language_index, row.source_index)
|
| 100 |
-
counts[identity] = counts.get(identity, 0) + row.count
|
| 101 |
-
path = temporary_root / f"occurrences-{run_index:06d}.bin"
|
| 102 |
-
with path.open("wb") as handle:
|
| 103 |
-
for (key, language, source), count in sorted(counts.items()):
|
| 104 |
-
handle.write(_OCCURRENCE.pack(key, language, source, count))
|
| 105 |
-
handle.flush()
|
| 106 |
-
os.fsync(handle.fileno())
|
| 107 |
-
return path
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def write_sorted_occurrence_runs(
|
| 111 |
-
checkpoint: Path,
|
| 112 |
-
corpus_manifest: Path,
|
| 113 |
-
roots: Mapping[str, Path],
|
| 114 |
-
temporary_root: Path,
|
| 115 |
-
*,
|
| 116 |
-
rows_per_run: int,
|
| 117 |
-
) -> list[Path]:
|
| 118 |
-
if rows_per_run <= 0:
|
| 119 |
-
raise ValueError("rows_per_run must be positive")
|
| 120 |
-
checkpoint = checkpoint.expanduser().resolve()
|
| 121 |
-
temporary_root = temporary_root.expanduser().resolve()
|
| 122 |
-
if temporary_root.exists() and any(temporary_root.iterdir()):
|
| 123 |
-
raise FileExistsError(f"compiler temporary root is not empty: {temporary_root}")
|
| 124 |
-
temporary_root.mkdir(parents=True, exist_ok=True)
|
| 125 |
-
module = ConditionedStaticEmbedding.load(str(checkpoint))
|
| 126 |
-
run_config_path = checkpoint / "run_config.json"
|
| 127 |
-
run_config = json.loads(run_config_path.read_text(encoding="utf-8"))
|
| 128 |
-
if module.mode == "collapse_online" and module.online_collapse is not None:
|
| 129 |
-
table = module.online_collapse
|
| 130 |
-
if dict(run_config.get("online_tokenizer") or {}).get("protocol") != "structured-st-v1":
|
| 131 |
-
raise ValueError("compiler checkpoint has no structured-st-v1 provenance")
|
| 132 |
-
elif module.mode == "collapse_dynamic" and module.collapse is not None:
|
| 133 |
-
table = module.collapse
|
| 134 |
-
telemetry = dict(run_config.get("vocabulary_telemetry") or {})
|
| 135 |
-
if (
|
| 136 |
-
telemetry.get("protocol") != "vocabulary-telemetry-v1"
|
| 137 |
-
or telemetry.get("arm") != "q15"
|
| 138 |
-
or table.pair_keys.numel()
|
| 139 |
-
):
|
| 140 |
-
raise ValueError("Q15 occurrence audit requires an unpromoted telemetry checkpoint")
|
| 141 |
-
else:
|
| 142 |
-
raise ValueError("occurrence runs require collapse_online or unpromoted Q15")
|
| 143 |
-
max_tokens = int(dict(run_config.get("training") or {}).get("max_tokens", 0))
|
| 144 |
-
if max_tokens > 0:
|
| 145 |
-
module.tokenizer.enable_truncation(max_length=max_tokens)
|
| 146 |
-
|
| 147 |
-
manifest = _load_manifest(corpus_manifest)
|
| 148 |
-
language_index = {name: index for index, name in enumerate(module.languages)}
|
| 149 |
-
source_names = sorted(
|
| 150 |
-
{
|
| 151 |
-
str(group["source"])
|
| 152 |
-
for group in manifest["groups"]
|
| 153 |
-
if isinstance(group, dict) and bool(group.get("trainable"))
|
| 154 |
-
}
|
| 155 |
-
)
|
| 156 |
-
source_index = {name: index for index, name in enumerate(source_names)}
|
| 157 |
-
rows: list[PairOccurrence] = []
|
| 158 |
-
runs: list[Path] = []
|
| 159 |
-
for raw_group in manifest["groups"]:
|
| 160 |
-
group = dict(raw_group)
|
| 161 |
-
if not bool(group.get("trainable")):
|
| 162 |
-
continue
|
| 163 |
-
group_source_index = source_index[str(group["source"])]
|
| 164 |
-
for language, text in _iter_group_texts(group, roots):
|
| 165 |
-
if language not in language_index:
|
| 166 |
-
raise ValueError(f"compiler encountered unknown language {language!r}")
|
| 167 |
-
ids = module.tokenizer.encode(text, add_special_tokens=False).ids
|
| 168 |
-
rows.extend(
|
| 169 |
-
PairOccurrence(
|
| 170 |
-
left * table.pair_key_base + right,
|
| 171 |
-
language_index[language],
|
| 172 |
-
group_source_index,
|
| 173 |
-
)
|
| 174 |
-
for left, right in zip(ids, ids[1:])
|
| 175 |
-
)
|
| 176 |
-
while len(rows) >= rows_per_run:
|
| 177 |
-
current = rows[:rows_per_run]
|
| 178 |
-
del rows[:rows_per_run]
|
| 179 |
-
runs.append(_flush_occurrence_run(current, temporary_root, len(runs)))
|
| 180 |
-
if rows:
|
| 181 |
-
runs.append(_flush_occurrence_run(rows, temporary_root, len(runs)))
|
| 182 |
-
if not runs:
|
| 183 |
-
runs.append(_flush_occurrence_run([], temporary_root, 0))
|
| 184 |
-
return runs
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def _read_occurrence_run(path: Path) -> Iterator[PairOccurrence]:
|
| 188 |
-
with path.open("rb") as handle:
|
| 189 |
-
while raw := handle.read(_OCCURRENCE.size):
|
| 190 |
-
if len(raw) != _OCCURRENCE.size:
|
| 191 |
-
raise ValueError(f"truncated compiler run: {path}")
|
| 192 |
-
yield PairOccurrence(*_OCCURRENCE.unpack(raw))
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
def _merged_occurrences(runs: list[Path]) -> Iterator[PairOccurrence]:
|
| 196 |
-
merged = heapq.merge(*(_read_occurrence_run(path) for path in runs))
|
| 197 |
-
|
| 198 |
-
def identity(row: PairOccurrence) -> tuple[int, int, int]:
|
| 199 |
-
return row.key, row.language_index, row.source_index
|
| 200 |
-
|
| 201 |
-
for (key, language, source), grouped in itertools.groupby(merged, key=identity):
|
| 202 |
-
yield PairOccurrence(
|
| 203 |
-
key,
|
| 204 |
-
language,
|
| 205 |
-
source,
|
| 206 |
-
sum(row.count for row in grouped),
|
| 207 |
-
)
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def _score_and_write_retained(
|
| 211 |
-
module: ConditionedStaticEmbedding,
|
| 212 |
-
runs: list[Path],
|
| 213 |
-
output: Path,
|
| 214 |
-
*,
|
| 215 |
-
source_names: list[str],
|
| 216 |
-
) -> tuple[int, int, dict[str, object]]:
|
| 217 |
-
table = module.online_collapse
|
| 218 |
-
assert table is not None
|
| 219 |
-
observed_keys = 0
|
| 220 |
-
retained = 0
|
| 221 |
-
observed_occurrences = 0
|
| 222 |
-
observed_pair_languages = 0
|
| 223 |
-
retained_occurrences = 0
|
| 224 |
-
retained_pair_languages = 0
|
| 225 |
-
source_support = {
|
| 226 |
-
name: {
|
| 227 |
-
"observed": {"occurrences": 0, "unique_pair_types": 0, "unique_pair_language_records": 0},
|
| 228 |
-
"retained": {"occurrences": 0, "unique_pair_types": 0, "unique_pair_language_records": 0},
|
| 229 |
-
}
|
| 230 |
-
for name in source_names
|
| 231 |
-
}
|
| 232 |
-
language_support = {
|
| 233 |
-
name: {
|
| 234 |
-
"observed": {"occurrences": 0, "unique_pair_types": 0},
|
| 235 |
-
"retained": {"occurrences": 0, "unique_pair_types": 0},
|
| 236 |
-
}
|
| 237 |
-
for name in module.languages
|
| 238 |
-
}
|
| 239 |
-
with output.open("wb") as handle:
|
| 240 |
-
for key, grouped in itertools.groupby(_merged_occurrences(runs), key=lambda row: row.key):
|
| 241 |
-
records = list(grouped)
|
| 242 |
-
observed_keys += 1
|
| 243 |
-
key_occurrences = sum(row.count for row in records)
|
| 244 |
-
key_languages = sorted({row.language_index for row in records})
|
| 245 |
-
observed_occurrences += key_occurrences
|
| 246 |
-
observed_pair_languages += len(key_languages)
|
| 247 |
-
for source_index in sorted({row.source_index for row in records}):
|
| 248 |
-
source_rows = [row for row in records if row.source_index == source_index]
|
| 249 |
-
target = source_support[source_names[source_index]]["observed"]
|
| 250 |
-
target["occurrences"] += sum(row.count for row in source_rows)
|
| 251 |
-
target["unique_pair_types"] += 1
|
| 252 |
-
target["unique_pair_language_records"] += len(
|
| 253 |
-
{row.language_index for row in source_rows}
|
| 254 |
-
)
|
| 255 |
-
for language_index in key_languages:
|
| 256 |
-
target = language_support[module.languages[language_index]]["observed"]
|
| 257 |
-
target["occurrences"] += sum(
|
| 258 |
-
row.count for row in records if row.language_index == language_index
|
| 259 |
-
)
|
| 260 |
-
target["unique_pair_types"] += 1
|
| 261 |
-
languages = torch.tensor(
|
| 262 |
-
key_languages,
|
| 263 |
-
dtype=torch.long,
|
| 264 |
-
device=module.embedding.weight.device,
|
| 265 |
-
)
|
| 266 |
-
left = torch.full_like(languages, key // table.pair_key_base)
|
| 267 |
-
right = torch.full_like(languages, key % table.pair_key_base)
|
| 268 |
-
with torch.no_grad():
|
| 269 |
-
occurrence_scores = table.edge_scores(
|
| 270 |
-
left, right, languages, module.embedding.weight
|
| 271 |
-
)
|
| 272 |
-
if bool((occurrence_scores > 0).any()):
|
| 273 |
-
global_score = (
|
| 274 |
-
occurrence_scores[0] - table.language_bias[languages[0]].float()
|
| 275 |
-
)
|
| 276 |
-
handle.write(_RETAINED.pack(key, float(global_score)))
|
| 277 |
-
retained += 1
|
| 278 |
-
retained_occurrences += key_occurrences
|
| 279 |
-
retained_pair_languages += len(key_languages)
|
| 280 |
-
for source_index in sorted({row.source_index for row in records}):
|
| 281 |
-
source_rows = [
|
| 282 |
-
row for row in records if row.source_index == source_index
|
| 283 |
-
]
|
| 284 |
-
target = source_support[source_names[source_index]]["retained"]
|
| 285 |
-
target["occurrences"] += sum(row.count for row in source_rows)
|
| 286 |
-
target["unique_pair_types"] += 1
|
| 287 |
-
target["unique_pair_language_records"] += len(
|
| 288 |
-
{row.language_index for row in source_rows}
|
| 289 |
-
)
|
| 290 |
-
for language_index in key_languages:
|
| 291 |
-
target = language_support[module.languages[language_index]]["retained"]
|
| 292 |
-
target["occurrences"] += sum(
|
| 293 |
-
row.count
|
| 294 |
-
for row in records
|
| 295 |
-
if row.language_index == language_index
|
| 296 |
-
)
|
| 297 |
-
target["unique_pair_types"] += 1
|
| 298 |
-
handle.flush()
|
| 299 |
-
os.fsync(handle.fileno())
|
| 300 |
-
support: dict[str, object] = {
|
| 301 |
-
"observed": {
|
| 302 |
-
"occurrences": observed_occurrences,
|
| 303 |
-
"unique_pair_types": observed_keys,
|
| 304 |
-
"unique_pair_language_records": observed_pair_languages,
|
| 305 |
-
},
|
| 306 |
-
"retained": {
|
| 307 |
-
"occurrences": retained_occurrences,
|
| 308 |
-
"unique_pair_types": retained,
|
| 309 |
-
"unique_pair_language_records": retained_pair_languages,
|
| 310 |
-
},
|
| 311 |
-
"per_source": source_support,
|
| 312 |
-
"per_language": language_support,
|
| 313 |
-
}
|
| 314 |
-
return observed_keys, retained, support
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
def _read_retained(path: Path) -> tuple[torch.Tensor, torch.Tensor]:
|
| 318 |
-
keys: list[int] = []
|
| 319 |
-
scores: list[float] = []
|
| 320 |
-
with path.open("rb") as handle:
|
| 321 |
-
while raw := handle.read(_RETAINED.size):
|
| 322 |
-
if len(raw) != _RETAINED.size:
|
| 323 |
-
raise ValueError("truncated retained-pair compiler output")
|
| 324 |
-
key, score = _RETAINED.unpack(raw)
|
| 325 |
-
keys.append(key)
|
| 326 |
-
scores.append(score)
|
| 327 |
-
return torch.tensor(keys, dtype=torch.long), torch.tensor(scores, dtype=torch.float32)
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
def _equivalence_sample(
|
| 331 |
-
source: ConditionedStaticEmbedding,
|
| 332 |
-
compiled: ConditionedStaticEmbedding,
|
| 333 |
-
manifest: Mapping[str, object],
|
| 334 |
-
roots: Mapping[str, Path],
|
| 335 |
-
*,
|
| 336 |
-
limit: int = 32,
|
| 337 |
-
) -> dict[str, object]:
|
| 338 |
-
count = 0
|
| 339 |
-
maximum_error = 0.0
|
| 340 |
-
for raw_group in manifest["groups"]:
|
| 341 |
-
group = dict(raw_group)
|
| 342 |
-
if not bool(group.get("trainable")):
|
| 343 |
-
continue
|
| 344 |
-
for language, text in _iter_group_texts(group, roots):
|
| 345 |
-
ids = source.tokenizer.encode(text, add_special_tokens=False).ids
|
| 346 |
-
marker = source.tokenizer.token_to_id(marker_for(language))
|
| 347 |
-
if marker is None or not ids:
|
| 348 |
-
continue
|
| 349 |
-
features = {
|
| 350 |
-
"input_ids": torch.tensor([marker, *ids]),
|
| 351 |
-
"offsets": torch.tensor([0]),
|
| 352 |
-
}
|
| 353 |
-
with torch.no_grad():
|
| 354 |
-
source_output = source(dict(features))["sentence_embedding"]
|
| 355 |
-
source_map = source.online_collapse.last_map_edges.clone()
|
| 356 |
-
compiled_output = compiled(dict(features))["sentence_embedding"]
|
| 357 |
-
compiled_map = compiled.online_collapse.last_map_edges.clone()
|
| 358 |
-
if not torch.equal(source_map, compiled_map):
|
| 359 |
-
raise RuntimeError("compiled online tokenizer MAP equivalence failed")
|
| 360 |
-
error = float((source_output - compiled_output).abs().max())
|
| 361 |
-
maximum_error = max(maximum_error, error)
|
| 362 |
-
if error > 1e-6:
|
| 363 |
-
raise RuntimeError("compiled online tokenizer embedding equivalence failed")
|
| 364 |
-
count += 1
|
| 365 |
-
if count >= limit:
|
| 366 |
-
return {"count": count, "maps_exact": True, "max_abs_error": maximum_error}
|
| 367 |
-
if count == 0:
|
| 368 |
-
raise ValueError("compiler equivalence sample is empty")
|
| 369 |
-
return {"count": count, "maps_exact": True, "max_abs_error": maximum_error}
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
def compile_online_tokenizer(
|
| 373 |
-
checkpoint: Path,
|
| 374 |
-
corpus_manifest: Path,
|
| 375 |
-
roots: Mapping[str, Path],
|
| 376 |
-
output: Path,
|
| 377 |
-
temporary_root: Path,
|
| 378 |
-
*,
|
| 379 |
-
rows_per_run: int = 1_000_000,
|
| 380 |
-
) -> dict[str, object]:
|
| 381 |
-
checkpoint = checkpoint.expanduser().resolve()
|
| 382 |
-
corpus_manifest = corpus_manifest.expanduser().resolve()
|
| 383 |
-
output = output.expanduser().resolve()
|
| 384 |
-
temporary_root = temporary_root.expanduser().resolve()
|
| 385 |
-
if output.exists():
|
| 386 |
-
raise FileExistsError(f"refusing to reuse compiled output: {output}")
|
| 387 |
-
if str(output).startswith("/hdd/") or str(temporary_root).startswith("/hdd/"):
|
| 388 |
-
raise ValueError("online tokenizer compilation may not use /hdd")
|
| 389 |
-
|
| 390 |
-
source = ConditionedStaticEmbedding.load(str(checkpoint)).eval()
|
| 391 |
-
if source.mode != "collapse_online" or source.online_collapse is None:
|
| 392 |
-
raise ValueError("compiler checkpoint must use collapse_online")
|
| 393 |
-
runs = write_sorted_occurrence_runs(
|
| 394 |
-
checkpoint,
|
| 395 |
-
corpus_manifest,
|
| 396 |
-
roots,
|
| 397 |
-
temporary_root,
|
| 398 |
-
rows_per_run=rows_per_run,
|
| 399 |
-
)
|
| 400 |
-
manifest = _load_manifest(corpus_manifest)
|
| 401 |
-
source_names = sorted(
|
| 402 |
-
{
|
| 403 |
-
str(group["source"])
|
| 404 |
-
for group in manifest["groups"]
|
| 405 |
-
if isinstance(group, dict) and bool(group.get("trainable"))
|
| 406 |
-
}
|
| 407 |
-
)
|
| 408 |
-
retained_path = temporary_root / "retained-pairs.bin"
|
| 409 |
-
observed_unique, retained_count, support = _score_and_write_retained(
|
| 410 |
-
source, runs, retained_path, source_names=source_names
|
| 411 |
-
)
|
| 412 |
-
keys, global_scores = _read_retained(retained_path)
|
| 413 |
-
table = source.online_collapse
|
| 414 |
-
compiled = ConditionedStaticEmbedding(
|
| 415 |
-
source.tokenizer,
|
| 416 |
-
embedding_weights=source.embedding.weight.detach().clone(),
|
| 417 |
-
languages=source.languages,
|
| 418 |
-
mode="collapse_online_compiled",
|
| 419 |
-
code_dim=source.conditioner.code_dim,
|
| 420 |
-
n_senses=source.conditioner.n_senses,
|
| 421 |
-
rank=source.conditioner.rank,
|
| 422 |
-
ngram_buckets=max(1, retained_count),
|
| 423 |
-
online_rank=source.online_rank,
|
| 424 |
-
compiled_pair_keys=keys,
|
| 425 |
-
compiled_pair_scores=global_scores,
|
| 426 |
-
).eval()
|
| 427 |
-
with torch.no_grad():
|
| 428 |
-
compiled.online_collapse.language_bias.copy_(table.language_bias)
|
| 429 |
-
if keys.numel():
|
| 430 |
-
left = torch.div(keys, table.pair_key_base, rounding_mode="floor")
|
| 431 |
-
right = keys % table.pair_key_base
|
| 432 |
-
hashed = table._hash(left, right)
|
| 433 |
-
compiled.online_collapse.merged.copy_(table.merged.index_select(0, hashed))
|
| 434 |
-
|
| 435 |
-
output.mkdir(parents=True)
|
| 436 |
-
compiled.save(str(output))
|
| 437 |
-
for name in ("modules.json", "config_sentence_transformers.json"):
|
| 438 |
-
shutil.copy2(checkpoint / name, output / name)
|
| 439 |
-
equivalence = _equivalence_sample(source, compiled, manifest, roots)
|
| 440 |
-
run_peak_rows = max(
|
| 441 |
-
(sum(row.count for row in _read_occurrence_run(path)) for path in runs),
|
| 442 |
-
default=0,
|
| 443 |
-
)
|
| 444 |
-
report: dict[str, object] = {
|
| 445 |
-
"protocol": "online-tokenizer-compilation-v1",
|
| 446 |
-
"checkpoint": str(checkpoint),
|
| 447 |
-
"checkpoint_model_sha256": _sha256(checkpoint / "model.safetensors"),
|
| 448 |
-
"corpus_manifest": str(corpus_manifest),
|
| 449 |
-
"corpus_manifest_sha256": _sha256(corpus_manifest),
|
| 450 |
-
"observed_unique_pairs": observed_unique,
|
| 451 |
-
"retained_pairs": retained_count,
|
| 452 |
-
"base_vocabulary_rows": int(source.embedding.weight.shape[0]),
|
| 453 |
-
"compiled_total_vocabulary_rows": int(source.embedding.weight.shape[0])
|
| 454 |
-
+ retained_count,
|
| 455 |
-
"support": support,
|
| 456 |
-
"temporary_runs": len(runs),
|
| 457 |
-
"temporary_run_peak_rows": run_peak_rows,
|
| 458 |
-
"equivalence": equivalence,
|
| 459 |
-
"output_sha256": {
|
| 460 |
-
name: _sha256(output / name)
|
| 461 |
-
for name in (
|
| 462 |
-
"model.safetensors",
|
| 463 |
-
"tokenizer.json",
|
| 464 |
-
"language_conditioning.json",
|
| 465 |
-
"modules.json",
|
| 466 |
-
"config_sentence_transformers.json",
|
| 467 |
-
)
|
| 468 |
-
},
|
| 469 |
-
}
|
| 470 |
-
(output / "compilation_manifest.json").write_text(
|
| 471 |
-
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
| 472 |
-
)
|
| 473 |
-
for path in [*runs, retained_path]:
|
| 474 |
-
path.unlink()
|
| 475 |
-
temporary_root.rmdir()
|
| 476 |
-
return report
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
def _support_target() -> dict[str, int]:
|
| 480 |
-
return {
|
| 481 |
-
"occurrences": 0,
|
| 482 |
-
"unique_pair_types": 0,
|
| 483 |
-
"unique_pair_language_records": 0,
|
| 484 |
-
}
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
def _write_json_atomic(path: Path, payload: dict[str, object]) -> None:
|
| 488 |
-
path.parent.mkdir(parents=True, exist_ok=True)
|
| 489 |
-
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 490 |
-
if temporary.exists():
|
| 491 |
-
raise FileExistsError(temporary)
|
| 492 |
-
data = json.dumps(
|
| 493 |
-
payload, ensure_ascii=False, indent=2, sort_keys=True
|
| 494 |
-
).encode("utf-8") + b"\n"
|
| 495 |
-
with temporary.open("xb") as handle:
|
| 496 |
-
handle.write(data)
|
| 497 |
-
handle.flush()
|
| 498 |
-
os.fsync(handle.fileno())
|
| 499 |
-
os.replace(temporary, path)
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
def audit_q15_threshold(
|
| 503 |
-
checkpoint: Path,
|
| 504 |
-
corpus_manifest: Path,
|
| 505 |
-
roots: Mapping[str, Path],
|
| 506 |
-
output: Path,
|
| 507 |
-
temporary_root: Path,
|
| 508 |
-
*,
|
| 509 |
-
rows_per_run: int = 1_000_000,
|
| 510 |
-
) -> dict[str, object]:
|
| 511 |
-
"""Count Q15 threshold behavior without materialising an exact dictionary."""
|
| 512 |
-
|
| 513 |
-
checkpoint = checkpoint.expanduser().resolve()
|
| 514 |
-
corpus_manifest = corpus_manifest.expanduser().resolve()
|
| 515 |
-
output = output.expanduser().resolve()
|
| 516 |
-
temporary_root = temporary_root.expanduser().resolve()
|
| 517 |
-
if output.exists():
|
| 518 |
-
raise FileExistsError(f"refusing to reuse Q15 threshold output: {output}")
|
| 519 |
-
if str(output) == "/hdd" or str(output).startswith("/hdd/"):
|
| 520 |
-
raise ValueError("Q15 threshold audit may not use /hdd")
|
| 521 |
-
if str(temporary_root) == "/hdd" or str(temporary_root).startswith("/hdd/"):
|
| 522 |
-
raise ValueError("Q15 threshold audit may not use /hdd")
|
| 523 |
-
module = ConditionedStaticEmbedding.load(str(checkpoint)).cpu().eval()
|
| 524 |
-
if module.mode != "collapse_dynamic" or module.collapse is None:
|
| 525 |
-
raise ValueError("Q15 threshold audit requires collapse_dynamic")
|
| 526 |
-
table = module.collapse
|
| 527 |
-
if table.pair_keys.numel():
|
| 528 |
-
raise ValueError("Q15 threshold audit requires an unpromoted table")
|
| 529 |
-
for name, tensor in module.state_dict().items():
|
| 530 |
-
if not bool(torch.isfinite(tensor).all()):
|
| 531 |
-
raise FloatingPointError(f"nonfinite Q15 checkpoint tensor: {name}")
|
| 532 |
-
|
| 533 |
-
runs = write_sorted_occurrence_runs(
|
| 534 |
-
checkpoint,
|
| 535 |
-
corpus_manifest,
|
| 536 |
-
roots,
|
| 537 |
-
temporary_root,
|
| 538 |
-
rows_per_run=rows_per_run,
|
| 539 |
-
)
|
| 540 |
-
manifest = _load_manifest(corpus_manifest)
|
| 541 |
-
source_names = sorted(
|
| 542 |
-
{
|
| 543 |
-
str(group["source"])
|
| 544 |
-
for group in manifest["groups"]
|
| 545 |
-
if isinstance(group, dict) and bool(group.get("trainable"))
|
| 546 |
-
}
|
| 547 |
-
)
|
| 548 |
-
observed = _support_target()
|
| 549 |
-
threshold_union = _support_target()
|
| 550 |
-
positive_pair_language_records = 0
|
| 551 |
-
positive_occurrences = 0
|
| 552 |
-
expected_merge_mass = 0.0
|
| 553 |
-
pair_digest = hashlib.sha256()
|
| 554 |
-
bucket_counts: dict[int, int] = {}
|
| 555 |
-
per_source = {
|
| 556 |
-
name: {"observed": _support_target(), "threshold_union": _support_target()}
|
| 557 |
-
for name in source_names
|
| 558 |
-
}
|
| 559 |
-
per_language = {
|
| 560 |
-
name: {
|
| 561 |
-
"observed": {"occurrences": 0, "unique_pair_types": 0},
|
| 562 |
-
"threshold_union": {"occurrences": 0, "unique_pair_types": 0},
|
| 563 |
-
"positive": {"occurrences": 0, "unique_pair_types": 0},
|
| 564 |
-
}
|
| 565 |
-
for name in module.languages
|
| 566 |
-
}
|
| 567 |
-
|
| 568 |
-
with torch.inference_mode():
|
| 569 |
-
for key, grouped in itertools.groupby(
|
| 570 |
-
_merged_occurrences(runs), key=lambda row: row.key
|
| 571 |
-
):
|
| 572 |
-
records = list(grouped)
|
| 573 |
-
languages = sorted({row.language_index for row in records})
|
| 574 |
-
counts_by_language = {
|
| 575 |
-
language: sum(
|
| 576 |
-
row.count for row in records if row.language_index == language
|
| 577 |
-
)
|
| 578 |
-
for language in languages
|
| 579 |
-
}
|
| 580 |
-
left_id = key // table.pair_key_base
|
| 581 |
-
right_id = key % table.pair_key_base
|
| 582 |
-
bucket = int(
|
| 583 |
-
table._hash(torch.tensor([left_id]), torch.tensor([right_id]))[0]
|
| 584 |
-
)
|
| 585 |
-
bucket_counts[bucket] = bucket_counts.get(bucket, 0) + 1
|
| 586 |
-
language_tensor = torch.tensor(languages, dtype=torch.long)
|
| 587 |
-
scores = table.score[bucket].float() + table.language_bias.index_select(
|
| 588 |
-
0, language_tensor
|
| 589 |
-
).float()
|
| 590 |
-
probabilities = torch.sigmoid(scores)
|
| 591 |
-
positive_languages = {
|
| 592 |
-
language
|
| 593 |
-
for language, score in zip(languages, scores.tolist(), strict=True)
|
| 594 |
-
if score > 0.0
|
| 595 |
-
}
|
| 596 |
-
key_occurrences = sum(row.count for row in records)
|
| 597 |
-
observed["occurrences"] += key_occurrences
|
| 598 |
-
observed["unique_pair_types"] += 1
|
| 599 |
-
observed["unique_pair_language_records"] += len(languages)
|
| 600 |
-
positive_pair_language_records += len(positive_languages)
|
| 601 |
-
for language, probability in zip(
|
| 602 |
-
languages, probabilities.tolist(), strict=True
|
| 603 |
-
):
|
| 604 |
-
count = counts_by_language[language]
|
| 605 |
-
expected_merge_mass += probability * count
|
| 606 |
-
language_observed = per_language[module.languages[language]]["observed"]
|
| 607 |
-
language_observed["occurrences"] += count
|
| 608 |
-
language_observed["unique_pair_types"] += 1
|
| 609 |
-
if language in positive_languages:
|
| 610 |
-
positive_occurrences += count
|
| 611 |
-
language_positive = per_language[module.languages[language]]["positive"]
|
| 612 |
-
language_positive["occurrences"] += count
|
| 613 |
-
language_positive["unique_pair_types"] += 1
|
| 614 |
-
source_indices = sorted({row.source_index for row in records})
|
| 615 |
-
for source_index in source_indices:
|
| 616 |
-
source_rows = [row for row in records if row.source_index == source_index]
|
| 617 |
-
target = per_source[source_names[source_index]]["observed"]
|
| 618 |
-
target["occurrences"] += sum(row.count for row in source_rows)
|
| 619 |
-
target["unique_pair_types"] += 1
|
| 620 |
-
target["unique_pair_language_records"] += len(
|
| 621 |
-
{row.language_index for row in source_rows}
|
| 622 |
-
)
|
| 623 |
-
if positive_languages:
|
| 624 |
-
pair_digest.update(struct.pack("<Q", key))
|
| 625 |
-
threshold_union["occurrences"] += key_occurrences
|
| 626 |
-
threshold_union["unique_pair_types"] += 1
|
| 627 |
-
threshold_union["unique_pair_language_records"] += len(languages)
|
| 628 |
-
for language in languages:
|
| 629 |
-
count = counts_by_language[language]
|
| 630 |
-
target = per_language[module.languages[language]]["threshold_union"]
|
| 631 |
-
target["occurrences"] += count
|
| 632 |
-
target["unique_pair_types"] += 1
|
| 633 |
-
for source_index in source_indices:
|
| 634 |
-
source_rows = [
|
| 635 |
-
row for row in records if row.source_index == source_index
|
| 636 |
-
]
|
| 637 |
-
target = per_source[source_names[source_index]]["threshold_union"]
|
| 638 |
-
target["occurrences"] += sum(row.count for row in source_rows)
|
| 639 |
-
target["unique_pair_types"] += 1
|
| 640 |
-
target["unique_pair_language_records"] += len(
|
| 641 |
-
{row.language_index for row in source_rows}
|
| 642 |
-
)
|
| 643 |
-
|
| 644 |
-
loads = sorted(bucket_counts.values())
|
| 645 |
-
collisions = sum(max(0, count - 1) for count in loads)
|
| 646 |
-
threshold_payload: dict[str, object] = {
|
| 647 |
-
**threshold_union,
|
| 648 |
-
"pair_keys_sha256": pair_digest.hexdigest(),
|
| 649 |
-
}
|
| 650 |
-
report: dict[str, object] = {
|
| 651 |
-
"protocol": "q15-threshold-audit-v1",
|
| 652 |
-
"checkpoint": str(checkpoint),
|
| 653 |
-
"checkpoint_model_sha256": _sha256(checkpoint / "model.safetensors"),
|
| 654 |
-
"corpus_manifest": str(corpus_manifest),
|
| 655 |
-
"corpus_manifest_sha256": _sha256(corpus_manifest),
|
| 656 |
-
"observed": observed,
|
| 657 |
-
"threshold_union": threshold_payload,
|
| 658 |
-
"positive_pair_language_records": positive_pair_language_records,
|
| 659 |
-
"positive_occurrences": positive_occurrences,
|
| 660 |
-
"expected_merge_mass": expected_merge_mass,
|
| 661 |
-
"unique_hash_buckets": len(bucket_counts),
|
| 662 |
-
"hash_collision_count": collisions,
|
| 663 |
-
"pair_types_per_bucket": {
|
| 664 |
-
"minimum": loads[0] if loads else None,
|
| 665 |
-
"median": (
|
| 666 |
-
loads[len(loads) // 2]
|
| 667 |
-
if len(loads) % 2
|
| 668 |
-
else ((loads[len(loads) // 2 - 1] + loads[len(loads) // 2]) / 2)
|
| 669 |
-
if loads
|
| 670 |
-
else None
|
| 671 |
-
),
|
| 672 |
-
"maximum": loads[-1] if loads else None,
|
| 673 |
-
},
|
| 674 |
-
"per_source": per_source,
|
| 675 |
-
"per_language": per_language,
|
| 676 |
-
"temporary_runs": len(runs),
|
| 677 |
-
"temporary_run_peak_rows": max(
|
| 678 |
-
(sum(row.count for row in _read_occurrence_run(path)) for path in runs),
|
| 679 |
-
default=0,
|
| 680 |
-
),
|
| 681 |
-
}
|
| 682 |
-
_write_json_atomic(output, report)
|
| 683 |
-
for path in runs:
|
| 684 |
-
path.unlink()
|
| 685 |
-
temporary_root.rmdir()
|
| 686 |
-
return report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_compilation.py
DELETED
|
@@ -1,428 +0,0 @@
|
|
| 1 |
-
"""Compile a frozen OpenLex policy into an exact static trie and table."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
import math
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Iterable
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
from torch import nn
|
| 12 |
-
from torch.nn import functional as F
|
| 13 |
-
|
| 14 |
-
from starse.openlex_dictionary import dictionary_span_values
|
| 15 |
-
from starse.openlex_lattice import preferred_boundary_spans, solve_lattice
|
| 16 |
-
from starse.openlex_model import OpenLexEncoder
|
| 17 |
-
from starse.openlex_spans import _validate_atoms
|
| 18 |
-
from starse.openlex_tokenizer import CanonicalBatch
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
@dataclass(frozen=True)
|
| 22 |
-
class CompiledOpenLexOutput:
|
| 23 |
-
embeddings: torch.Tensor
|
| 24 |
-
numerators: torch.Tensor
|
| 25 |
-
map_keys: tuple[tuple[tuple[int, ...], ...], ...] | None
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class CompiledOpenLex(nn.Module):
|
| 29 |
-
"""A deterministic exact-key tokenizer followed by static table lookup."""
|
| 30 |
-
|
| 31 |
-
def __init__(
|
| 32 |
-
self,
|
| 33 |
-
type_keys: tuple[tuple[int, ...], ...],
|
| 34 |
-
type_scores: torch.Tensor,
|
| 35 |
-
table: torch.Tensor,
|
| 36 |
-
boundary_misalignment_penalty: float = 0.0,
|
| 37 |
-
) -> None:
|
| 38 |
-
super().__init__()
|
| 39 |
-
if not type_keys or len(set(type_keys)) != len(type_keys):
|
| 40 |
-
raise ValueError("compiled OpenLex keys must be nonempty and unique")
|
| 41 |
-
if any(not key or min(key) < 0 for key in type_keys):
|
| 42 |
-
raise ValueError("compiled OpenLex keys must contain nonnegative atoms")
|
| 43 |
-
if type_scores.shape != (len(type_keys),) or not type_scores.is_floating_point():
|
| 44 |
-
raise ValueError("compiled scores must contain one float per exact key")
|
| 45 |
-
if table.ndim != 2 or table.shape[0] != len(type_keys) or not table.is_floating_point():
|
| 46 |
-
raise ValueError("compiled table must contain one vector per exact key")
|
| 47 |
-
if not bool(torch.isfinite(type_scores).all()) or not bool(torch.isfinite(table).all()):
|
| 48 |
-
raise ValueError("compiled OpenLex tensors must be finite")
|
| 49 |
-
if not math.isfinite(boundary_misalignment_penalty) or boundary_misalignment_penalty < 0:
|
| 50 |
-
raise ValueError("compiled boundary penalty must be finite and nonnegative")
|
| 51 |
-
self.type_keys = tuple(tuple(int(atom) for atom in key) for key in type_keys)
|
| 52 |
-
self.boundary_misalignment_penalty = float(boundary_misalignment_penalty)
|
| 53 |
-
self.register_buffer("type_scores", type_scores.float().clone(), persistent=True)
|
| 54 |
-
self.register_buffer("table", table.clone(), persistent=True)
|
| 55 |
-
trie: dict[int | None, object] = {}
|
| 56 |
-
for row, key in enumerate(self.type_keys):
|
| 57 |
-
node = trie
|
| 58 |
-
for atom in key:
|
| 59 |
-
nested = node.setdefault(atom, {})
|
| 60 |
-
if not isinstance(nested, dict):
|
| 61 |
-
raise RuntimeError("invalid compiled OpenLex trie node")
|
| 62 |
-
node = nested
|
| 63 |
-
node[None] = row
|
| 64 |
-
self._trie = trie
|
| 65 |
-
|
| 66 |
-
def forward(
|
| 67 |
-
self,
|
| 68 |
-
atom_ids: torch.Tensor,
|
| 69 |
-
lengths: torch.Tensor,
|
| 70 |
-
*,
|
| 71 |
-
preferred_edges: torch.Tensor | None = None,
|
| 72 |
-
include_keys: bool = False,
|
| 73 |
-
) -> CompiledOpenLexOutput:
|
| 74 |
-
batch, atom_count = _validate_atoms(atom_ids, lengths)
|
| 75 |
-
boundaries = atom_count + 1
|
| 76 |
-
scores = torch.zeros(batch, boundaries, boundaries, dtype=torch.float32, device=atom_ids.device)
|
| 77 |
-
mask = torch.zeros_like(scores, dtype=torch.bool)
|
| 78 |
-
rows = torch.full_like(scores, -1, dtype=torch.long)
|
| 79 |
-
cpu_atoms = atom_ids.detach().to(device="cpu", dtype=torch.long)
|
| 80 |
-
cpu_lengths = lengths.detach().to(device="cpu", dtype=torch.long)
|
| 81 |
-
for sentence in range(batch):
|
| 82 |
-
length = int(cpu_lengths[sentence])
|
| 83 |
-
for start in range(length):
|
| 84 |
-
node = self._trie
|
| 85 |
-
for end in range(start + 1, length + 1):
|
| 86 |
-
atom = int(cpu_atoms[sentence, end - 1])
|
| 87 |
-
nested = node.get(atom)
|
| 88 |
-
if not isinstance(nested, dict):
|
| 89 |
-
break
|
| 90 |
-
node = nested
|
| 91 |
-
row = node.get(None)
|
| 92 |
-
if isinstance(row, int):
|
| 93 |
-
rows[sentence, start, end] = row
|
| 94 |
-
scores[sentence, start, end] = self.type_scores[row]
|
| 95 |
-
mask[sentence, start, end] = True
|
| 96 |
-
if preferred_edges is None:
|
| 97 |
-
preferred = torch.zeros_like(mask)
|
| 98 |
-
positions = torch.arange(atom_count, device=atom_ids.device)
|
| 99 |
-
preferred[:, positions, positions + 1] = mask[:, positions, positions + 1]
|
| 100 |
-
else:
|
| 101 |
-
if (
|
| 102 |
-
preferred_edges.shape != mask.shape
|
| 103 |
-
or preferred_edges.dtype != torch.bool
|
| 104 |
-
or preferred_edges.device != mask.device
|
| 105 |
-
):
|
| 106 |
-
raise ValueError("compiled preferred edges must be a boolean lattice mask")
|
| 107 |
-
if bool((preferred_edges & ~mask).any()):
|
| 108 |
-
raise ValueError("compiled preferred edge is absent from the vocabulary")
|
| 109 |
-
preferred = preferred_edges
|
| 110 |
-
if self.boundary_misalignment_penalty > 0:
|
| 111 |
-
aligned = preferred_boundary_spans(preferred, mask)
|
| 112 |
-
scores = scores - (~aligned & mask).float() * self.boundary_misalignment_penalty
|
| 113 |
-
lattice = solve_lattice(scores, mask, lengths, preferred_edges=preferred)
|
| 114 |
-
numerators = torch.zeros(batch, self.table.shape[1], dtype=torch.float32, device=atom_ids.device)
|
| 115 |
-
selected_keys: list[tuple[tuple[int, ...], ...]] = []
|
| 116 |
-
for sentence in range(batch):
|
| 117 |
-
sentence_keys: list[tuple[int, ...]] = []
|
| 118 |
-
selected = lattice.map_edges[sentence].nonzero(as_tuple=False)
|
| 119 |
-
for start, end in selected.tolist():
|
| 120 |
-
row = int(rows[sentence, start, end])
|
| 121 |
-
if row < 0:
|
| 122 |
-
raise RuntimeError("compiled MAP selected an unregistered edge")
|
| 123 |
-
numerators[sentence] += self.table[row].float()
|
| 124 |
-
sentence_keys.append(self.type_keys[row])
|
| 125 |
-
selected_keys.append(tuple(sentence_keys))
|
| 126 |
-
embeddings = F.normalize(numerators, dim=1)
|
| 127 |
-
if not bool(torch.isfinite(embeddings).all()):
|
| 128 |
-
raise FloatingPointError("nonfinite compiled OpenLex embedding")
|
| 129 |
-
return CompiledOpenLexOutput(
|
| 130 |
-
embeddings,
|
| 131 |
-
numerators,
|
| 132 |
-
tuple(selected_keys) if include_keys else None,
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def _map_type_inventory(
|
| 137 |
-
model: OpenLexEncoder,
|
| 138 |
-
batches: Iterable[tuple[torch.Tensor, torch.Tensor]],
|
| 139 |
-
) -> tuple[tuple[int, ...], ...]:
|
| 140 |
-
keys = {(atom,) for atom in range(model.atom_vocab_size)}
|
| 141 |
-
found_batch = False
|
| 142 |
-
for atom_ids, lengths in batches:
|
| 143 |
-
found_batch = True
|
| 144 |
-
output = model(atom_ids, lengths, include_keys=True)
|
| 145 |
-
if output.map_keys is None:
|
| 146 |
-
raise RuntimeError("OpenLex compiler did not receive MAP keys")
|
| 147 |
-
for sentence in output.map_keys:
|
| 148 |
-
keys.update(sentence)
|
| 149 |
-
if not found_batch:
|
| 150 |
-
raise ValueError("OpenLex compilation needs at least one audit batch")
|
| 151 |
-
return tuple(sorted(keys))
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def _materialize_type(model: OpenLexEncoder, key: tuple[int, ...]) -> tuple[torch.Tensor, torch.Tensor]:
|
| 155 |
-
device = model.atom_embedding.weight.device
|
| 156 |
-
atom_ids = torch.tensor([key], dtype=torch.long, device=device)
|
| 157 |
-
lengths = torch.tensor([len(key)], dtype=torch.long, device=device)
|
| 158 |
-
spans = model.span_generator(atom_ids, lengths)
|
| 159 |
-
score = spans.scores[0, 0, len(key)] / model.temperature.float()
|
| 160 |
-
base = model.atom_embedding(atom_ids[0]).float().sum(dim=0)
|
| 161 |
-
code = spans.residual_codes[0, 0, len(key)].float()
|
| 162 |
-
vector = base + model.residual_projection.float() @ code
|
| 163 |
-
return score.float(), vector.float()
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def _materialization_chunks(
|
| 167 |
-
batches: Iterable[tuple[torch.Tensor, torch.Tensor] | CanonicalBatch],
|
| 168 |
-
*,
|
| 169 |
-
batch_size: int,
|
| 170 |
-
) -> Iterable[tuple[torch.Tensor, torch.Tensor] | CanonicalBatch]:
|
| 171 |
-
"""Bound lattice memory while preserving deterministic inventory order."""
|
| 172 |
-
|
| 173 |
-
for materialization in batches:
|
| 174 |
-
atom_ids = materialization.atom_ids if isinstance(materialization, CanonicalBatch) else materialization[0]
|
| 175 |
-
rows = int(atom_ids.shape[0])
|
| 176 |
-
for start in range(0, rows, batch_size):
|
| 177 |
-
end = min(start + batch_size, rows)
|
| 178 |
-
if isinstance(materialization, CanonicalBatch):
|
| 179 |
-
yield CanonicalBatch(
|
| 180 |
-
materialization.atom_ids[start:end],
|
| 181 |
-
materialization.lengths[start:end],
|
| 182 |
-
materialization.preferred_edges[start:end],
|
| 183 |
-
materialization.override_ids[start:end],
|
| 184 |
-
)
|
| 185 |
-
else:
|
| 186 |
-
yield materialization[0][start:end], materialization[1][start:end]
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
def compile_openlex(
|
| 190 |
-
model: OpenLexEncoder,
|
| 191 |
-
batches: Iterable[tuple[torch.Tensor, torch.Tensor] | CanonicalBatch],
|
| 192 |
-
*,
|
| 193 |
-
fallback_atom_ids: Iterable[int] | None = None,
|
| 194 |
-
existing_type_spellings: Iterable[tuple[int, ...]] | None = None,
|
| 195 |
-
dtype: torch.dtype = torch.bfloat16,
|
| 196 |
-
materialization_batch_size: int = 64,
|
| 197 |
-
) -> CompiledOpenLex:
|
| 198 |
-
"""Materialize every exact type used by frozen MAP plus atomic fallback."""
|
| 199 |
-
|
| 200 |
-
if dtype not in (torch.float32, torch.bfloat16):
|
| 201 |
-
raise ValueError("compiled OpenLex table must use float32 or bfloat16")
|
| 202 |
-
if isinstance(materialization_batch_size, bool) or materialization_batch_size <= 0:
|
| 203 |
-
raise ValueError("OpenLex materialization batch size must be positive")
|
| 204 |
-
materialization_batches = tuple(batches)
|
| 205 |
-
if not materialization_batches:
|
| 206 |
-
raise ValueError("OpenLex compilation needs at least one audit batch")
|
| 207 |
-
was_training = model.training
|
| 208 |
-
model.eval()
|
| 209 |
-
with torch.no_grad():
|
| 210 |
-
records: dict[tuple[int, ...], tuple[torch.Tensor, torch.Tensor]] = {}
|
| 211 |
-
for materialization in _materialization_chunks(
|
| 212 |
-
materialization_batches,
|
| 213 |
-
batch_size=materialization_batch_size,
|
| 214 |
-
):
|
| 215 |
-
device = model.atom_embedding.weight.device
|
| 216 |
-
if isinstance(materialization, CanonicalBatch):
|
| 217 |
-
atom_ids = materialization.atom_ids.to(device=device)
|
| 218 |
-
lengths = materialization.lengths.to(device=device)
|
| 219 |
-
preferred_edges = materialization.preferred_edges.to(device=device)
|
| 220 |
-
override_ids = materialization.override_ids.to(device=device)
|
| 221 |
-
else:
|
| 222 |
-
atom_ids = materialization[0].to(device=device)
|
| 223 |
-
lengths = materialization[1].to(device=device)
|
| 224 |
-
preferred_edges = None
|
| 225 |
-
override_ids = None
|
| 226 |
-
output = model(
|
| 227 |
-
atom_ids,
|
| 228 |
-
lengths,
|
| 229 |
-
preferred_edges=preferred_edges,
|
| 230 |
-
override_ids=override_ids,
|
| 231 |
-
include_keys=True,
|
| 232 |
-
)
|
| 233 |
-
spans = model.span_generator(
|
| 234 |
-
atom_ids,
|
| 235 |
-
lengths,
|
| 236 |
-
include_keys=False,
|
| 237 |
-
emit_residual_codes=not (
|
| 238 |
-
model.disable_candidate_residuals
|
| 239 |
-
and model.dictionary_composition
|
| 240 |
-
),
|
| 241 |
-
)
|
| 242 |
-
dictionary_values = None
|
| 243 |
-
if model.dictionary_composition:
|
| 244 |
-
if override_ids is None:
|
| 245 |
-
raise ValueError("dictionary composition compiler requires canonical batches")
|
| 246 |
-
base_dictionary_values = dictionary_span_values(
|
| 247 |
-
model.atom_embedding,
|
| 248 |
-
override_ids,
|
| 249 |
-
spans.mask,
|
| 250 |
-
detach_embedding=False,
|
| 251 |
-
)
|
| 252 |
-
if model.disable_candidate_residuals:
|
| 253 |
-
dictionary_values = base_dictionary_values
|
| 254 |
-
else:
|
| 255 |
-
if spans.residual_codes is None:
|
| 256 |
-
raise RuntimeError("OpenLex compiler residual codes are missing")
|
| 257 |
-
residual_values = (
|
| 258 |
-
spans.residual_codes.float()
|
| 259 |
-
@ model.residual_projection.float().T
|
| 260 |
-
)
|
| 261 |
-
novel = spans.mask & (override_ids < 0)
|
| 262 |
-
dictionary_values = (
|
| 263 |
-
base_dictionary_values
|
| 264 |
-
+ residual_values * novel.unsqueeze(3)
|
| 265 |
-
)
|
| 266 |
-
if output.map_keys is None:
|
| 267 |
-
raise RuntimeError("OpenLex compiler did not receive exact span keys")
|
| 268 |
-
for sentence in range(atom_ids.shape[0]):
|
| 269 |
-
for start, end in output.lattice.map_edges[sentence].nonzero(as_tuple=False).tolist():
|
| 270 |
-
key = tuple(
|
| 271 |
-
int(value)
|
| 272 |
-
for value in atom_ids[sentence, start:end].detach().to(device="cpu", dtype=torch.long)
|
| 273 |
-
)
|
| 274 |
-
if key not in records:
|
| 275 |
-
score = spans.scores[sentence, start, end]
|
| 276 |
-
override_row = -1
|
| 277 |
-
if override_ids is not None:
|
| 278 |
-
override_row = int(override_ids[sentence, start, end])
|
| 279 |
-
if override_row >= 0 and end - start > 1:
|
| 280 |
-
score = score - float(model.span_generator.initial_non_atomic_score)
|
| 281 |
-
score = score + (end - start - 1) * float(model.existing_token_length_bonus)
|
| 282 |
-
score = (score / model.temperature.float()).float()
|
| 283 |
-
if override_row >= 0:
|
| 284 |
-
base = model.atom_embedding.weight[override_row].float()
|
| 285 |
-
vector = base
|
| 286 |
-
else:
|
| 287 |
-
if dictionary_values is None:
|
| 288 |
-
candidate_embedding = (
|
| 289 |
-
model.atom_embedding
|
| 290 |
-
if model.candidate_atom_embedding is None
|
| 291 |
-
else model.candidate_atom_embedding
|
| 292 |
-
)
|
| 293 |
-
base = candidate_embedding(atom_ids[sentence, start:end]).float().sum(dim=0)
|
| 294 |
-
if model.disable_candidate_residuals:
|
| 295 |
-
vector = base
|
| 296 |
-
else:
|
| 297 |
-
if spans.residual_codes is None:
|
| 298 |
-
raise RuntimeError("OpenLex compiler residual codes are missing")
|
| 299 |
-
code = spans.residual_codes[sentence, start, end].float()
|
| 300 |
-
vector = base + model.residual_projection.float() @ code
|
| 301 |
-
else:
|
| 302 |
-
# A view would keep the complete lattice chunk alive
|
| 303 |
-
# for every retained type until compilation finishes.
|
| 304 |
-
vector = dictionary_values[sentence, start, end].float().clone()
|
| 305 |
-
records[key] = (score, vector)
|
| 306 |
-
if existing_type_spellings is not None:
|
| 307 |
-
spellings = tuple(tuple(int(atom) for atom in key) for key in existing_type_spellings)
|
| 308 |
-
if (
|
| 309 |
-
not spellings
|
| 310 |
-
or len(spellings) > model.atom_vocab_size
|
| 311 |
-
or len(set(spellings)) != len(spellings)
|
| 312 |
-
or any(not key or min(key) < 0 or max(key) >= model.atom_vocab_size for key in spellings)
|
| 313 |
-
):
|
| 314 |
-
raise ValueError("existing canonical token spellings are malformed")
|
| 315 |
-
rows_by_length: dict[int, list[int]] = {}
|
| 316 |
-
for row, key in enumerate(spellings):
|
| 317 |
-
if key not in records:
|
| 318 |
-
rows_by_length.setdefault(len(key), []).append(row)
|
| 319 |
-
existing_chunk_size = 8_192
|
| 320 |
-
device = model.atom_embedding.weight.device
|
| 321 |
-
for length, all_rows in sorted(rows_by_length.items()):
|
| 322 |
-
for offset in range(0, len(all_rows), existing_chunk_size):
|
| 323 |
-
token_rows = all_rows[offset : offset + existing_chunk_size]
|
| 324 |
-
keys = [spellings[row] for row in token_rows]
|
| 325 |
-
atom_ids = torch.tensor(keys, dtype=torch.long, device=device)
|
| 326 |
-
atom_lengths = torch.full((len(keys),), length, dtype=torch.long, device=device)
|
| 327 |
-
spans = model.span_generator(
|
| 328 |
-
atom_ids,
|
| 329 |
-
atom_lengths,
|
| 330 |
-
include_keys=False,
|
| 331 |
-
emit_residual_codes=False,
|
| 332 |
-
)
|
| 333 |
-
scores = spans.scores[:, 0, length]
|
| 334 |
-
if length > 1:
|
| 335 |
-
scores = scores - float(model.span_generator.initial_non_atomic_score)
|
| 336 |
-
scores = scores + (length - 1) * float(model.existing_token_length_bonus)
|
| 337 |
-
scores = scores / model.temperature.float()
|
| 338 |
-
base = model.atom_embedding.weight[
|
| 339 |
-
torch.tensor(token_rows, dtype=torch.long, device=device)
|
| 340 |
-
].float()
|
| 341 |
-
vectors = base
|
| 342 |
-
for index, key in enumerate(keys):
|
| 343 |
-
records[key] = (
|
| 344 |
-
scores[index].float(),
|
| 345 |
-
vectors[index].float(),
|
| 346 |
-
)
|
| 347 |
-
if fallback_atom_ids is None:
|
| 348 |
-
fallback = tuple(range(model.atom_vocab_size))
|
| 349 |
-
else:
|
| 350 |
-
fallback = tuple(dict.fromkeys(int(atom) for atom in fallback_atom_ids))
|
| 351 |
-
if not fallback or min(fallback) < 0 or max(fallback) >= model.atom_vocab_size:
|
| 352 |
-
raise ValueError("fallback atom ids lie outside the OpenLex table")
|
| 353 |
-
missing_atoms = [atom for atom in fallback if (atom,) not in records]
|
| 354 |
-
atom_chunk_size = 8_192
|
| 355 |
-
device = model.atom_embedding.weight.device
|
| 356 |
-
for offset in range(0, len(missing_atoms), atom_chunk_size):
|
| 357 |
-
atoms = missing_atoms[offset : offset + atom_chunk_size]
|
| 358 |
-
atom_ids = torch.tensor(atoms, dtype=torch.long, device=device).unsqueeze(1)
|
| 359 |
-
atom_lengths = torch.ones(len(atoms), dtype=torch.long, device=device)
|
| 360 |
-
spans = model.span_generator(
|
| 361 |
-
atom_ids,
|
| 362 |
-
atom_lengths,
|
| 363 |
-
include_keys=False,
|
| 364 |
-
emit_residual_codes=False,
|
| 365 |
-
)
|
| 366 |
-
atom_scores = spans.scores[:, 0, 1] / model.temperature.float()
|
| 367 |
-
atom_vectors = model.atom_embedding(atom_ids[:, 0]).float()
|
| 368 |
-
for index, atom in enumerate(atoms):
|
| 369 |
-
records[(atom,)] = (atom_scores[index].float(), atom_vectors[index])
|
| 370 |
-
keys = tuple(sorted(records))
|
| 371 |
-
scores = torch.stack([records[key][0] for key in keys])
|
| 372 |
-
table = torch.stack([records[key][1] for key in keys]).to(dtype=dtype)
|
| 373 |
-
model.train(was_training)
|
| 374 |
-
compiled = CompiledOpenLex(
|
| 375 |
-
keys,
|
| 376 |
-
scores,
|
| 377 |
-
table,
|
| 378 |
-
boundary_misalignment_penalty=model.boundary_misalignment_penalty,
|
| 379 |
-
)
|
| 380 |
-
return compiled.to(device=model.atom_embedding.weight.device)
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
def save_compiled_openlex(model: CompiledOpenLex, path: str | Path) -> None:
|
| 384 |
-
"""Atomically save an exact compiled vocabulary and its static rows."""
|
| 385 |
-
|
| 386 |
-
destination = Path(path)
|
| 387 |
-
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 388 |
-
temporary = destination.with_name(f".{destination.name}.tmp")
|
| 389 |
-
payload = {
|
| 390 |
-
"format": "openlex-compiled-v2",
|
| 391 |
-
"type_keys": [list(key) for key in model.type_keys],
|
| 392 |
-
"type_scores": model.type_scores.detach().to(device="cpu", dtype=torch.float32),
|
| 393 |
-
"table": model.table.detach().to(device="cpu"),
|
| 394 |
-
"boundary_misalignment_penalty": model.boundary_misalignment_penalty,
|
| 395 |
-
}
|
| 396 |
-
try:
|
| 397 |
-
torch.save(payload, temporary)
|
| 398 |
-
temporary.replace(destination)
|
| 399 |
-
finally:
|
| 400 |
-
temporary.unlink(missing_ok=True)
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
def load_compiled_openlex(path: str | Path) -> CompiledOpenLex:
|
| 404 |
-
"""Load only the declared tensor/basic-type compiled format."""
|
| 405 |
-
|
| 406 |
-
payload = torch.load(Path(path), map_location="cpu", weights_only=True)
|
| 407 |
-
if not isinstance(payload, dict) or payload.get("format") not in {
|
| 408 |
-
"openlex-compiled-v1",
|
| 409 |
-
"openlex-compiled-v2",
|
| 410 |
-
}:
|
| 411 |
-
raise ValueError("unsupported compiled OpenLex format")
|
| 412 |
-
raw_keys = payload.get("type_keys")
|
| 413 |
-
scores = payload.get("type_scores")
|
| 414 |
-
table = payload.get("table")
|
| 415 |
-
if not isinstance(raw_keys, list) or not all(isinstance(key, list) for key in raw_keys):
|
| 416 |
-
raise ValueError("compiled OpenLex type keys are malformed")
|
| 417 |
-
if not isinstance(scores, torch.Tensor) or not isinstance(table, torch.Tensor):
|
| 418 |
-
raise ValueError("compiled OpenLex tensors are missing")
|
| 419 |
-
keys = tuple(tuple(int(atom) for atom in key) for key in raw_keys)
|
| 420 |
-
raw_penalty = payload.get("boundary_misalignment_penalty", 0.0)
|
| 421 |
-
if not isinstance(raw_penalty, (int, float)):
|
| 422 |
-
raise ValueError("compiled OpenLex boundary penalty is malformed")
|
| 423 |
-
return CompiledOpenLex(
|
| 424 |
-
keys,
|
| 425 |
-
scores,
|
| 426 |
-
table,
|
| 427 |
-
boundary_misalignment_penalty=float(raw_penalty),
|
| 428 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_dictionary.py
DELETED
|
@@ -1,322 +0,0 @@
|
|
| 1 |
-
"""Context-free novel-span values composed from exact dictionary rows."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
from torch import nn
|
| 7 |
-
from torch.nn import functional as F
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
class _WeightedEmbeddingBag(torch.autograd.Function):
|
| 11 |
-
"""Weighted fixed-width bags with a bounded-memory custom backward.
|
| 12 |
-
|
| 13 |
-
PyTorch's weighted ``embedding_bag`` currently asserts when the table is
|
| 14 |
-
frozen but the per-sample weights require gradients. OpenLex deliberately
|
| 15 |
-
uses that combination during replay: q15 is detached while dictionary-cover
|
| 16 |
-
traffic stays differentiable.
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
_BACKWARD_CHUNK_ROWS = 8_192
|
| 20 |
-
|
| 21 |
-
@staticmethod
|
| 22 |
-
def forward( # type: ignore[override]
|
| 23 |
-
ctx: object,
|
| 24 |
-
weight: torch.Tensor,
|
| 25 |
-
row_indices: torch.Tensor,
|
| 26 |
-
per_sample_weights: torch.Tensor,
|
| 27 |
-
) -> torch.Tensor:
|
| 28 |
-
ctx.save_for_backward(weight, row_indices, per_sample_weights)
|
| 29 |
-
return F.embedding_bag(
|
| 30 |
-
row_indices,
|
| 31 |
-
weight,
|
| 32 |
-
offsets=None,
|
| 33 |
-
mode="sum",
|
| 34 |
-
per_sample_weights=per_sample_weights,
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
@staticmethod
|
| 38 |
-
def backward(
|
| 39 |
-
ctx: object,
|
| 40 |
-
grad_output: torch.Tensor,
|
| 41 |
-
) -> tuple[torch.Tensor | None, None, torch.Tensor | None]:
|
| 42 |
-
weight, row_indices, per_sample_weights = ctx.saved_tensors
|
| 43 |
-
flat_rows = row_indices.reshape(-1)
|
| 44 |
-
flat_weights = per_sample_weights.reshape(-1)
|
| 45 |
-
bag_width = row_indices.shape[1]
|
| 46 |
-
|
| 47 |
-
grad_weight = torch.zeros_like(weight) if ctx.needs_input_grad[0] else None
|
| 48 |
-
grad_per_sample = (
|
| 49 |
-
torch.empty_like(flat_weights) if ctx.needs_input_grad[2] else None
|
| 50 |
-
)
|
| 51 |
-
chunk_rows = _WeightedEmbeddingBag._BACKWARD_CHUNK_ROWS
|
| 52 |
-
for start in range(0, flat_rows.numel(), chunk_rows):
|
| 53 |
-
stop = min(start + chunk_rows, flat_rows.numel())
|
| 54 |
-
rows = flat_rows[start:stop]
|
| 55 |
-
bags = torch.arange(start, stop, device=rows.device).div(
|
| 56 |
-
bag_width,
|
| 57 |
-
rounding_mode="floor",
|
| 58 |
-
)
|
| 59 |
-
bag_grad = grad_output.index_select(0, bags)
|
| 60 |
-
if grad_per_sample is not None:
|
| 61 |
-
selected = weight.index_select(0, rows)
|
| 62 |
-
grad_per_sample[start:stop] = (selected * bag_grad).sum(dim=-1)
|
| 63 |
-
if grad_weight is not None:
|
| 64 |
-
weighted_grad = bag_grad * flat_weights[start:stop].unsqueeze(-1)
|
| 65 |
-
grad_weight.index_add_(0, rows, weighted_grad)
|
| 66 |
-
|
| 67 |
-
if grad_per_sample is not None:
|
| 68 |
-
grad_per_sample = grad_per_sample.view_as(per_sample_weights)
|
| 69 |
-
return grad_weight, None, grad_per_sample
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def _dictionary_cover_metadata(
|
| 73 |
-
embedding: nn.Embedding,
|
| 74 |
-
override_ids: torch.Tensor,
|
| 75 |
-
span_mask: torch.Tensor,
|
| 76 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 77 |
-
"""Return a vectorized longest-prefix exact-row cover for every legal span."""
|
| 78 |
-
|
| 79 |
-
if override_ids.ndim != 3 or override_ids.shape[1] != override_ids.shape[2]:
|
| 80 |
-
raise ValueError("dictionary override ids must be a square rank-3 edge tensor")
|
| 81 |
-
if override_ids.dtype not in (torch.int32, torch.int64):
|
| 82 |
-
raise TypeError("dictionary override ids must contain integer rows")
|
| 83 |
-
if span_mask.shape != override_ids.shape or span_mask.dtype is not torch.bool:
|
| 84 |
-
raise ValueError("dictionary span mask must be boolean and align with override ids")
|
| 85 |
-
if override_ids.device != embedding.weight.device or span_mask.device != override_ids.device:
|
| 86 |
-
raise ValueError("dictionary tensors and embedding must share one device")
|
| 87 |
-
registered = override_ids >= 0
|
| 88 |
-
if bool((registered & ~span_mask).any()):
|
| 89 |
-
raise ValueError("dictionary override row lies outside the legal span mask")
|
| 90 |
-
if bool((override_ids[registered] >= embedding.num_embeddings).any()):
|
| 91 |
-
raise ValueError("dictionary override row lies outside the embedding table")
|
| 92 |
-
|
| 93 |
-
_, boundaries, _ = override_ids.shape
|
| 94 |
-
atom_count = boundaries - 1
|
| 95 |
-
positions = torch.arange(atom_count, device=override_ids.device)
|
| 96 |
-
active_atoms = span_mask[:, positions, positions + 1]
|
| 97 |
-
atomic_rows = override_ids[:, positions, positions + 1]
|
| 98 |
-
if bool((active_atoms & (atomic_rows < 0)).any()):
|
| 99 |
-
raise ValueError("every active atomic span must name an exact dictionary row")
|
| 100 |
-
|
| 101 |
-
end_indices = torch.arange(boundaries, device=override_ids.device).view(1, 1, -1)
|
| 102 |
-
registered_ends = torch.where(registered, end_indices, torch.full_like(override_ids, -1))
|
| 103 |
-
longest_at_or_before = registered_ends.cummax(dim=2).values
|
| 104 |
-
longest_before = torch.full_like(longest_at_or_before, -1)
|
| 105 |
-
longest_before[:, :, 1:] = longest_at_or_before[:, :, :-1]
|
| 106 |
-
exact = span_mask & registered
|
| 107 |
-
novel = span_mask & ~registered
|
| 108 |
-
chosen_splits = longest_before
|
| 109 |
-
start_indices = torch.arange(boundaries, device=override_ids.device).view(1, -1, 1)
|
| 110 |
-
if bool((novel & (chosen_splits <= start_indices)).any()):
|
| 111 |
-
raise ValueError("active dictionary span has no exact-row cover")
|
| 112 |
-
chosen_rows = override_ids.gather(2, chosen_splits.clamp_min(0))
|
| 113 |
-
head_rows = torch.where(
|
| 114 |
-
exact,
|
| 115 |
-
override_ids,
|
| 116 |
-
torch.where(novel, chosen_rows, torch.full_like(override_ids, -1)),
|
| 117 |
-
)
|
| 118 |
-
suffix_boundaries = torch.where(
|
| 119 |
-
novel,
|
| 120 |
-
chosen_splits,
|
| 121 |
-
torch.full_like(chosen_splits, -1),
|
| 122 |
-
)
|
| 123 |
-
return head_rows, suffix_boundaries
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
def dictionary_expected_numerator(
|
| 127 |
-
embedding: nn.Embedding,
|
| 128 |
-
override_ids: torch.Tensor,
|
| 129 |
-
span_mask: torch.Tensor,
|
| 130 |
-
edge_weights: torch.Tensor,
|
| 131 |
-
*,
|
| 132 |
-
detach_embedding: bool,
|
| 133 |
-
) -> torch.Tensor:
|
| 134 |
-
"""Pool exact dictionary covers without materializing full-dimensional spans."""
|
| 135 |
-
|
| 136 |
-
if edge_weights.shape != override_ids.shape or not edge_weights.is_floating_point():
|
| 137 |
-
raise ValueError("dictionary edge weights must be floating point and align with overrides")
|
| 138 |
-
if edge_weights.device != override_ids.device:
|
| 139 |
-
raise ValueError("dictionary edge weights must share the override device")
|
| 140 |
-
if not bool(torch.isfinite(edge_weights).all()):
|
| 141 |
-
raise FloatingPointError("nonfinite dictionary edge weight")
|
| 142 |
-
head_rows, suffix_boundaries = _dictionary_cover_metadata(
|
| 143 |
-
embedding, override_ids, span_mask
|
| 144 |
-
)
|
| 145 |
-
source_weight = embedding.weight.detach() if detach_embedding else embedding.weight
|
| 146 |
-
work_dtype = torch.float64 if source_weight.dtype == torch.float64 else torch.float32
|
| 147 |
-
traffic = edge_weights.to(dtype=work_dtype) * span_mask.to(dtype=work_dtype)
|
| 148 |
-
batch, boundaries, _ = override_ids.shape
|
| 149 |
-
atom_count = boundaries - 1
|
| 150 |
-
rows: list[torch.Tensor] = []
|
| 151 |
-
weights: list[torch.Tensor] = []
|
| 152 |
-
end_limit = atom_count + 1
|
| 153 |
-
for width in range(atom_count, 0, -1):
|
| 154 |
-
starts = torch.arange(atom_count - width + 1, device=override_ids.device)
|
| 155 |
-
ends = starts + width
|
| 156 |
-
active = span_mask[:, starts, ends]
|
| 157 |
-
current = traffic[:, starts, ends] * active.to(dtype=work_dtype)
|
| 158 |
-
rows.append(head_rows[:, starts, ends].clamp_min(0))
|
| 159 |
-
weights.append(current)
|
| 160 |
-
suffix = suffix_boundaries[:, starts, ends]
|
| 161 |
-
novel = active & (suffix >= 0)
|
| 162 |
-
targets = suffix.clamp_min(0) * end_limit + ends.unsqueeze(0)
|
| 163 |
-
traffic.reshape(batch, -1).scatter_add_(
|
| 164 |
-
1,
|
| 165 |
-
targets,
|
| 166 |
-
current * novel.to(dtype=work_dtype),
|
| 167 |
-
)
|
| 168 |
-
|
| 169 |
-
row_matrix = torch.cat(rows, dim=1).long()
|
| 170 |
-
traffic_matrix = torch.cat(weights, dim=1)
|
| 171 |
-
result = _WeightedEmbeddingBag.apply(
|
| 172 |
-
source_weight.to(dtype=work_dtype),
|
| 173 |
-
row_matrix,
|
| 174 |
-
traffic_matrix,
|
| 175 |
-
)
|
| 176 |
-
if not bool(torch.isfinite(result).all()):
|
| 177 |
-
raise FloatingPointError("nonfinite factored dictionary numerator")
|
| 178 |
-
return result
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def dictionary_ordered_map_numerator(
|
| 182 |
-
embedding: nn.Embedding,
|
| 183 |
-
override_ids: torch.Tensor,
|
| 184 |
-
span_mask: torch.Tensor,
|
| 185 |
-
map_edges: torch.Tensor,
|
| 186 |
-
*,
|
| 187 |
-
detach_embedding: bool,
|
| 188 |
-
) -> torch.Tensor:
|
| 189 |
-
"""Sum MAP dictionary covers in the exact order used by dense compilation."""
|
| 190 |
-
|
| 191 |
-
if map_edges.shape != override_ids.shape or map_edges.dtype is not torch.bool:
|
| 192 |
-
raise ValueError("dictionary MAP edges must be boolean and align with overrides")
|
| 193 |
-
if map_edges.device != override_ids.device:
|
| 194 |
-
raise ValueError("dictionary MAP edges must share the override device")
|
| 195 |
-
if bool((map_edges & ~span_mask).any()):
|
| 196 |
-
raise ValueError("dictionary MAP edge lies outside the legal span mask")
|
| 197 |
-
head_rows, suffix_boundaries = _dictionary_cover_metadata(
|
| 198 |
-
embedding, override_ids, span_mask
|
| 199 |
-
)
|
| 200 |
-
source_weight = embedding.weight.detach() if detach_embedding else embedding.weight
|
| 201 |
-
work_dtype = torch.float64 if source_weight.dtype == torch.float64 else torch.float32
|
| 202 |
-
weight = source_weight.to(dtype=work_dtype)
|
| 203 |
-
outputs: list[torch.Tensor] = []
|
| 204 |
-
|
| 205 |
-
for sentence in range(override_ids.shape[0]):
|
| 206 |
-
total = weight.new_zeros(embedding.embedding_dim)
|
| 207 |
-
selected = map_edges[sentence].nonzero(as_tuple=False).tolist()
|
| 208 |
-
selected.sort(key=lambda edge: (int(edge[0]), int(edge[1])))
|
| 209 |
-
|
| 210 |
-
def cover_value(start: int, end: int) -> torch.Tensor:
|
| 211 |
-
row = int(head_rows[sentence, start, end].item())
|
| 212 |
-
if row < 0:
|
| 213 |
-
raise ValueError("selected dictionary span has no cover row")
|
| 214 |
-
suffix = int(suffix_boundaries[sentence, start, end].item())
|
| 215 |
-
return weight[row] if suffix < 0 else weight[row] + cover_value(suffix, end)
|
| 216 |
-
|
| 217 |
-
for start, end in selected:
|
| 218 |
-
total = total + cover_value(int(start), int(end))
|
| 219 |
-
outputs.append(total)
|
| 220 |
-
result = torch.stack(outputs)
|
| 221 |
-
if not bool(torch.isfinite(result).all()):
|
| 222 |
-
raise FloatingPointError("nonfinite ordered dictionary MAP numerator")
|
| 223 |
-
return result
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
def dictionary_span_values(
|
| 227 |
-
embedding: nn.Embedding,
|
| 228 |
-
override_ids: torch.Tensor,
|
| 229 |
-
span_mask: torch.Tensor,
|
| 230 |
-
*,
|
| 231 |
-
detach_embedding: bool,
|
| 232 |
-
) -> torch.Tensor:
|
| 233 |
-
"""Materialize every legal span from its deterministic shortest exact-row cover."""
|
| 234 |
-
|
| 235 |
-
if override_ids.ndim != 3 or override_ids.shape[1] != override_ids.shape[2]:
|
| 236 |
-
raise ValueError("dictionary override ids must be a square rank-3 edge tensor")
|
| 237 |
-
if override_ids.dtype not in (torch.int32, torch.int64):
|
| 238 |
-
raise TypeError("dictionary override ids must contain integer rows")
|
| 239 |
-
if span_mask.shape != override_ids.shape or span_mask.dtype is not torch.bool:
|
| 240 |
-
raise ValueError("dictionary span mask must be boolean and align with override ids")
|
| 241 |
-
if override_ids.device != embedding.weight.device or span_mask.device != override_ids.device:
|
| 242 |
-
raise ValueError("dictionary tensors and embedding must share one device")
|
| 243 |
-
registered = override_ids >= 0
|
| 244 |
-
if bool((registered & ~span_mask).any()):
|
| 245 |
-
raise ValueError("dictionary override row lies outside the legal span mask")
|
| 246 |
-
if bool((override_ids[registered] >= embedding.num_embeddings).any()):
|
| 247 |
-
raise ValueError("dictionary override row lies outside the embedding table")
|
| 248 |
-
|
| 249 |
-
batch, boundaries, _ = override_ids.shape
|
| 250 |
-
atom_count = boundaries - 1
|
| 251 |
-
positions = torch.arange(atom_count, device=override_ids.device)
|
| 252 |
-
active_atoms = span_mask[:, positions, positions + 1]
|
| 253 |
-
atomic_rows = override_ids[:, positions, positions + 1]
|
| 254 |
-
if bool((active_atoms & (atomic_rows < 0)).any()):
|
| 255 |
-
raise ValueError("every active atomic span must name an exact dictionary row")
|
| 256 |
-
|
| 257 |
-
source_weight = embedding.weight.detach() if detach_embedding else embedding.weight
|
| 258 |
-
work_dtype = torch.float64 if source_weight.dtype == torch.float64 else torch.float32
|
| 259 |
-
weight = source_weight.to(dtype=work_dtype)
|
| 260 |
-
values = torch.zeros(
|
| 261 |
-
batch,
|
| 262 |
-
boundaries,
|
| 263 |
-
boundaries,
|
| 264 |
-
embedding.embedding_dim,
|
| 265 |
-
dtype=work_dtype,
|
| 266 |
-
device=override_ids.device,
|
| 267 |
-
)
|
| 268 |
-
unreachable = atom_count + 1
|
| 269 |
-
counts = torch.full(
|
| 270 |
-
(batch, boundaries, boundaries),
|
| 271 |
-
unreachable,
|
| 272 |
-
dtype=torch.long,
|
| 273 |
-
device=override_ids.device,
|
| 274 |
-
)
|
| 275 |
-
batch_indices = torch.arange(batch, device=override_ids.device)
|
| 276 |
-
|
| 277 |
-
for width in range(1, atom_count + 1):
|
| 278 |
-
for start in range(0, atom_count - width + 1):
|
| 279 |
-
end = start + width
|
| 280 |
-
active = span_mask[:, start, end]
|
| 281 |
-
exact = active & (override_ids[:, start, end] >= 0)
|
| 282 |
-
exact_rows = override_ids[:, start, end].clamp_min(0).long()
|
| 283 |
-
exact_values = weight[exact_rows]
|
| 284 |
-
if width == 1:
|
| 285 |
-
current_values = exact_values
|
| 286 |
-
current_counts = torch.ones(batch, dtype=torch.long, device=override_ids.device)
|
| 287 |
-
else:
|
| 288 |
-
# Descending split order makes torch.min choose the longest left
|
| 289 |
-
# exact edge when two covers use the same number of rows.
|
| 290 |
-
splits = torch.arange(end - 1, start, -1, device=override_ids.device)
|
| 291 |
-
direct_rows = override_ids[:, start, splits]
|
| 292 |
-
suffix_counts = counts[:, splits, end]
|
| 293 |
-
feasible = (direct_rows >= 0) & (suffix_counts < unreachable)
|
| 294 |
-
candidate_counts = torch.where(
|
| 295 |
-
feasible,
|
| 296 |
-
suffix_counts + 1,
|
| 297 |
-
torch.full_like(suffix_counts, unreachable),
|
| 298 |
-
)
|
| 299 |
-
best_counts, best_indices = candidate_counts.min(dim=1)
|
| 300 |
-
missing = active & ~exact & (best_counts >= unreachable)
|
| 301 |
-
if bool(missing.any()):
|
| 302 |
-
raise ValueError("active dictionary span has no exact-row cover")
|
| 303 |
-
chosen_splits = splits[best_indices]
|
| 304 |
-
chosen_rows = direct_rows.gather(1, best_indices.unsqueeze(1)).squeeze(1).clamp_min(0).long()
|
| 305 |
-
suffix_values = values[batch_indices, chosen_splits, end]
|
| 306 |
-
candidate_values = weight[chosen_rows] + suffix_values
|
| 307 |
-
current_values = torch.where(exact.unsqueeze(1), exact_values, candidate_values)
|
| 308 |
-
current_counts = torch.where(exact, torch.ones_like(best_counts), best_counts)
|
| 309 |
-
values[:, start, end] = torch.where(
|
| 310 |
-
active.unsqueeze(1),
|
| 311 |
-
current_values,
|
| 312 |
-
torch.zeros_like(current_values),
|
| 313 |
-
)
|
| 314 |
-
counts[:, start, end] = torch.where(
|
| 315 |
-
active,
|
| 316 |
-
current_counts,
|
| 317 |
-
torch.full_like(current_counts, unreachable),
|
| 318 |
-
)
|
| 319 |
-
|
| 320 |
-
if not bool(torch.isfinite(values).all()):
|
| 321 |
-
raise FloatingPointError("nonfinite dictionary-composed span value")
|
| 322 |
-
return values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_lattice.py
DELETED
|
@@ -1,396 +0,0 @@
|
|
| 1 |
-
"""Exact dynamic programs for an unrestricted finite segmentation lattice."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
|
| 7 |
-
import torch
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
@dataclass(frozen=True)
|
| 11 |
-
class LatticeResult:
|
| 12 |
-
"""Partition statistics and one deterministic maximum-score path."""
|
| 13 |
-
|
| 14 |
-
log_partition: torch.Tensor
|
| 15 |
-
marginals: torch.Tensor
|
| 16 |
-
map_edges: torch.Tensor
|
| 17 |
-
map_scores: torch.Tensor
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def preferred_boundary_spans(
|
| 21 |
-
preferred_edges: torch.Tensor,
|
| 22 |
-
span_mask: torch.Tensor,
|
| 23 |
-
) -> torch.Tensor:
|
| 24 |
-
"""Return legal spans whose endpoints lie on the preferred token path."""
|
| 25 |
-
|
| 26 |
-
if preferred_edges.shape != span_mask.shape or preferred_edges.ndim != 3:
|
| 27 |
-
raise ValueError("preferred edges and span mask must be aligned edge tensors")
|
| 28 |
-
if preferred_edges.dtype is not torch.bool or span_mask.dtype is not torch.bool:
|
| 29 |
-
raise TypeError("preferred edges and span mask must be boolean")
|
| 30 |
-
if preferred_edges.device != span_mask.device:
|
| 31 |
-
raise ValueError("preferred edges and span mask must share one device")
|
| 32 |
-
if bool((preferred_edges & ~span_mask).any()):
|
| 33 |
-
raise ValueError("preferred edges must be legal spans")
|
| 34 |
-
boundaries = preferred_edges.any(dim=2) | preferred_edges.any(dim=1)
|
| 35 |
-
return span_mask & boundaries.unsqueeze(2) & boundaries.unsqueeze(1)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def _validate_span_values(span_values: torch.Tensor, edge_weights: torch.Tensor) -> None:
|
| 39 |
-
if span_values.ndim != 4:
|
| 40 |
-
raise ValueError("span_values must have shape [batch, boundaries, boundaries, dim]")
|
| 41 |
-
if span_values.shape[:3] != edge_weights.shape:
|
| 42 |
-
raise ValueError("span_values and edge weights must align")
|
| 43 |
-
if not span_values.is_floating_point():
|
| 44 |
-
raise TypeError("span_values must be floating point")
|
| 45 |
-
if span_values.device != edge_weights.device:
|
| 46 |
-
raise ValueError("span_values and edge weights must use the same device")
|
| 47 |
-
if not bool(torch.isfinite(span_values).all()):
|
| 48 |
-
raise ValueError("span_values must be finite")
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def expected_span_sum(span_values: torch.Tensor, marginals: torch.Tensor) -> torch.Tensor:
|
| 52 |
-
"""Sum full-dimensional span values under exact edge marginals."""
|
| 53 |
-
|
| 54 |
-
if not marginals.is_floating_point():
|
| 55 |
-
raise TypeError("marginals must be floating point")
|
| 56 |
-
_validate_span_values(span_values, marginals)
|
| 57 |
-
return (span_values * marginals.to(span_values.dtype).unsqueeze(-1)).sum((1, 2))
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def map_span_sum(span_values: torch.Tensor, map_edges: torch.Tensor) -> torch.Tensor:
|
| 61 |
-
"""Sum full-dimensional values emitted by a deterministic MAP path."""
|
| 62 |
-
|
| 63 |
-
if map_edges.dtype is not torch.bool:
|
| 64 |
-
raise TypeError("map_edges must be boolean")
|
| 65 |
-
_validate_span_values(span_values, map_edges)
|
| 66 |
-
return (span_values * map_edges.to(span_values.dtype).unsqueeze(-1)).sum((1, 2))
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def ordered_map_span_sum(span_values: torch.Tensor, map_edges: torch.Tensor) -> torch.Tensor:
|
| 70 |
-
"""Sum MAP values in the same left-to-right FP order as static inference."""
|
| 71 |
-
|
| 72 |
-
if map_edges.dtype is not torch.bool:
|
| 73 |
-
raise TypeError("map_edges must be boolean")
|
| 74 |
-
_validate_span_values(span_values, map_edges)
|
| 75 |
-
work_dtype = torch.float64 if span_values.dtype == torch.float64 else torch.float32
|
| 76 |
-
sentences: list[torch.Tensor] = []
|
| 77 |
-
for sentence in range(span_values.shape[0]):
|
| 78 |
-
numerator = torch.zeros(
|
| 79 |
-
span_values.shape[3],
|
| 80 |
-
dtype=work_dtype,
|
| 81 |
-
device=span_values.device,
|
| 82 |
-
)
|
| 83 |
-
for start, end in map_edges[sentence].nonzero(as_tuple=False).tolist():
|
| 84 |
-
numerator = numerator + span_values[sentence, start, end].to(dtype=work_dtype)
|
| 85 |
-
sentences.append(numerator)
|
| 86 |
-
result = torch.stack(sentences)
|
| 87 |
-
if not bool(torch.isfinite(result).all()):
|
| 88 |
-
raise FloatingPointError("nonfinite ordered MAP span sum")
|
| 89 |
-
return result
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def _validate_factored_values(
|
| 93 |
-
atom_values: torch.Tensor,
|
| 94 |
-
residual_codes: torch.Tensor,
|
| 95 |
-
projection: torch.Tensor,
|
| 96 |
-
edge_weights: torch.Tensor,
|
| 97 |
-
lengths: torch.Tensor,
|
| 98 |
-
) -> torch.dtype:
|
| 99 |
-
if atom_values.ndim != 3 or residual_codes.ndim != 4 or projection.ndim != 2:
|
| 100 |
-
raise ValueError("factored values require rank-3 atoms, rank-4 codes and rank-2 projection")
|
| 101 |
-
batch, atom_count, dimension = atom_values.shape
|
| 102 |
-
boundaries = atom_count + 1
|
| 103 |
-
if residual_codes.shape[:3] != (batch, boundaries, boundaries):
|
| 104 |
-
raise ValueError("residual codes must cover every atom boundary pair")
|
| 105 |
-
if edge_weights.shape != (batch, boundaries, boundaries):
|
| 106 |
-
raise ValueError("edge weights must align with residual codes")
|
| 107 |
-
if projection.shape != (dimension, residual_codes.shape[-1]):
|
| 108 |
-
raise ValueError("projection must map residual rank to atom dimension")
|
| 109 |
-
if lengths.shape != (batch,) or lengths.dtype not in (torch.int32, torch.int64):
|
| 110 |
-
raise ValueError("lengths must contain one integer atom length per sentence")
|
| 111 |
-
tensors = (atom_values, residual_codes, projection)
|
| 112 |
-
if not all(tensor.is_floating_point() for tensor in tensors):
|
| 113 |
-
raise TypeError("atoms, residual codes and projection must be floating point")
|
| 114 |
-
if not (edge_weights.is_floating_point() or edge_weights.dtype is torch.bool):
|
| 115 |
-
raise TypeError("edge weights must be floating point or boolean")
|
| 116 |
-
device = atom_values.device
|
| 117 |
-
if any(tensor.device != device for tensor in (*tensors[1:], edge_weights, lengths)):
|
| 118 |
-
raise ValueError("every factored pooling tensor must use the same device")
|
| 119 |
-
if bool(((lengths < 1) | (lengths > atom_count)).any()):
|
| 120 |
-
raise ValueError("factored pooling length is outside the atom tensor")
|
| 121 |
-
if not all(bool(torch.isfinite(tensor).all()) for tensor in tensors):
|
| 122 |
-
raise ValueError("factored pooling values must be finite")
|
| 123 |
-
return torch.float64 if any(tensor.dtype == torch.float64 for tensor in tensors) else torch.float32
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
def _factored_sum(
|
| 127 |
-
atom_values: torch.Tensor,
|
| 128 |
-
residual_codes: torch.Tensor,
|
| 129 |
-
projection: torch.Tensor,
|
| 130 |
-
edge_weights: torch.Tensor,
|
| 131 |
-
lengths: torch.Tensor,
|
| 132 |
-
) -> torch.Tensor:
|
| 133 |
-
work_dtype = _validate_factored_values(atom_values, residual_codes, projection, edge_weights, lengths)
|
| 134 |
-
atom_positions = torch.arange(atom_values.shape[1], device=atom_values.device)
|
| 135 |
-
atom_mask = atom_positions.unsqueeze(0) < lengths.unsqueeze(1)
|
| 136 |
-
base = (atom_values.to(work_dtype) * atom_mask.to(work_dtype).unsqueeze(-1)).sum(dim=1)
|
| 137 |
-
expected_code = (residual_codes.to(work_dtype) * edge_weights.to(work_dtype).unsqueeze(-1)).sum(dim=(1, 2))
|
| 138 |
-
result = base + expected_code @ projection.to(work_dtype).T
|
| 139 |
-
if not bool(torch.isfinite(result).all()):
|
| 140 |
-
raise FloatingPointError("nonfinite factored span sum")
|
| 141 |
-
return result
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def factored_expected_sum(
|
| 145 |
-
atom_values: torch.Tensor,
|
| 146 |
-
residual_codes: torch.Tensor,
|
| 147 |
-
projection: torch.Tensor,
|
| 148 |
-
marginals: torch.Tensor,
|
| 149 |
-
lengths: torch.Tensor,
|
| 150 |
-
) -> torch.Tensor:
|
| 151 |
-
"""Pool every span without allocating its full-dimensional token vector."""
|
| 152 |
-
|
| 153 |
-
if not marginals.is_floating_point():
|
| 154 |
-
raise TypeError("marginals must be floating point")
|
| 155 |
-
return _factored_sum(atom_values, residual_codes, projection, marginals, lengths)
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def factored_map_sum(
|
| 159 |
-
atom_values: torch.Tensor,
|
| 160 |
-
residual_codes: torch.Tensor,
|
| 161 |
-
projection: torch.Tensor,
|
| 162 |
-
map_edges: torch.Tensor,
|
| 163 |
-
lengths: torch.Tensor,
|
| 164 |
-
) -> torch.Tensor:
|
| 165 |
-
"""Pool MAP token vectors in the same order used by static compilation."""
|
| 166 |
-
|
| 167 |
-
if map_edges.dtype is not torch.bool:
|
| 168 |
-
raise TypeError("map_edges must be boolean")
|
| 169 |
-
work_dtype = _validate_factored_values(atom_values, residual_codes, projection, map_edges, lengths)
|
| 170 |
-
sentences: list[torch.Tensor] = []
|
| 171 |
-
for sentence in range(atom_values.shape[0]):
|
| 172 |
-
numerator = torch.zeros(atom_values.shape[2], dtype=work_dtype, device=atom_values.device)
|
| 173 |
-
selected = map_edges[sentence].nonzero(as_tuple=False).tolist()
|
| 174 |
-
for start, end in selected:
|
| 175 |
-
base = atom_values[sentence, start:end].to(work_dtype).sum(dim=0)
|
| 176 |
-
residual = projection.to(work_dtype) @ residual_codes[sentence, start, end].to(work_dtype)
|
| 177 |
-
token_vector = base + residual
|
| 178 |
-
numerator = numerator + token_vector
|
| 179 |
-
sentences.append(numerator)
|
| 180 |
-
result = torch.stack(sentences)
|
| 181 |
-
if not bool(torch.isfinite(result).all()):
|
| 182 |
-
raise FloatingPointError("nonfinite factored MAP sum")
|
| 183 |
-
return result
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def _validate_inputs(
|
| 187 |
-
span_scores: torch.Tensor,
|
| 188 |
-
span_mask: torch.Tensor,
|
| 189 |
-
lengths: torch.Tensor,
|
| 190 |
-
preferred_edges: torch.Tensor | None,
|
| 191 |
-
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 192 |
-
if span_scores.ndim != 3 or span_scores.shape[1] != span_scores.shape[2]:
|
| 193 |
-
raise ValueError("span_scores must have shape [batch, boundaries, boundaries]")
|
| 194 |
-
if not span_scores.is_floating_point():
|
| 195 |
-
raise TypeError("span_scores must be floating point")
|
| 196 |
-
if span_mask.shape != span_scores.shape or span_mask.dtype is not torch.bool:
|
| 197 |
-
raise ValueError("span_mask must be boolean and match span_scores")
|
| 198 |
-
if span_mask.device != span_scores.device:
|
| 199 |
-
raise ValueError("span_mask and span_scores must use the same device")
|
| 200 |
-
batch, boundaries, _ = span_scores.shape
|
| 201 |
-
if lengths.shape != (batch,) or lengths.dtype not in (torch.int32, torch.int64):
|
| 202 |
-
raise ValueError("lengths must contain one integer atom length per sentence")
|
| 203 |
-
if lengths.device != span_scores.device:
|
| 204 |
-
raise ValueError("lengths and span_scores must use the same device")
|
| 205 |
-
if bool(((lengths < 1) | (lengths >= boundaries)).any()):
|
| 206 |
-
raise ValueError("every length must lie between one and boundaries minus one")
|
| 207 |
-
|
| 208 |
-
upper = torch.triu(
|
| 209 |
-
torch.ones(boundaries, boundaries, dtype=torch.bool, device=span_scores.device),
|
| 210 |
-
diagonal=1,
|
| 211 |
-
)
|
| 212 |
-
if bool((span_mask & ~upper.unsqueeze(0)).any()):
|
| 213 |
-
raise ValueError("span_mask may contain only forward spans")
|
| 214 |
-
if preferred_edges is None:
|
| 215 |
-
preferred = torch.zeros_like(span_mask)
|
| 216 |
-
else:
|
| 217 |
-
if preferred_edges.shape != span_scores.shape or preferred_edges.dtype is not torch.bool:
|
| 218 |
-
raise ValueError("preferred_edges must be boolean and match span_scores")
|
| 219 |
-
if preferred_edges.device != span_scores.device:
|
| 220 |
-
raise ValueError("preferred_edges and span_scores must use the same device")
|
| 221 |
-
if bool((preferred_edges & ~span_mask).any()):
|
| 222 |
-
raise ValueError("preferred edges must also be valid span edges")
|
| 223 |
-
preferred = preferred_edges
|
| 224 |
-
|
| 225 |
-
positions = torch.arange(boundaries, device=span_scores.device)
|
| 226 |
-
within_start = positions.view(1, boundaries, 1) < lengths.view(batch, 1, 1)
|
| 227 |
-
within_end = positions.view(1, 1, boundaries) <= lengths.view(batch, 1, 1)
|
| 228 |
-
valid = span_mask & upper.unsqueeze(0) & within_start & within_end
|
| 229 |
-
if bool((~torch.isfinite(span_scores) & valid).any()):
|
| 230 |
-
raise ValueError("valid span scores must be finite")
|
| 231 |
-
work_dtype = torch.float64 if span_scores.dtype == torch.float64 else torch.float32
|
| 232 |
-
scores = torch.where(valid, span_scores.to(work_dtype), torch.zeros_like(span_scores, dtype=work_dtype))
|
| 233 |
-
return scores, valid, lengths.to(torch.long), preferred & valid
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
def _forward(scores: torch.Tensor, valid: torch.Tensor, lengths: torch.Tensor) -> torch.Tensor:
|
| 237 |
-
batch, boundaries, _ = scores.shape
|
| 238 |
-
negative_infinity = torch.tensor(float("-inf"), dtype=scores.dtype, device=scores.device)
|
| 239 |
-
alpha = torch.full((batch, boundaries), negative_infinity, dtype=scores.dtype, device=scores.device)
|
| 240 |
-
alpha[:, 0] = 0
|
| 241 |
-
for end in range(1, boundaries):
|
| 242 |
-
candidates = alpha[:, :end] + scores[:, :end, end]
|
| 243 |
-
candidates = torch.where(valid[:, :end, end], candidates, negative_infinity)
|
| 244 |
-
reachable = (end <= lengths) & torch.isfinite(candidates).any(dim=1)
|
| 245 |
-
safe_candidates = torch.where(
|
| 246 |
-
reachable.unsqueeze(1), candidates, torch.zeros_like(candidates)
|
| 247 |
-
)
|
| 248 |
-
value = torch.logsumexp(safe_candidates, dim=1)
|
| 249 |
-
alpha[:, end] = torch.where(reachable, value, negative_infinity)
|
| 250 |
-
return alpha
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
def _backward(scores: torch.Tensor, valid: torch.Tensor, lengths: torch.Tensor) -> torch.Tensor:
|
| 254 |
-
batch, boundaries, _ = scores.shape
|
| 255 |
-
negative_infinity = torch.tensor(float("-inf"), dtype=scores.dtype, device=scores.device)
|
| 256 |
-
beta = torch.full((batch, boundaries), negative_infinity, dtype=scores.dtype, device=scores.device)
|
| 257 |
-
rows = torch.arange(batch, device=scores.device)
|
| 258 |
-
beta[rows, lengths] = 0
|
| 259 |
-
for start in range(boundaries - 2, -1, -1):
|
| 260 |
-
candidates = scores[:, start, start + 1 :] + beta[:, start + 1 :]
|
| 261 |
-
candidates = torch.where(valid[:, start, start + 1 :], candidates, negative_infinity)
|
| 262 |
-
reachable = (start < lengths) & torch.isfinite(candidates).any(dim=1)
|
| 263 |
-
safe_candidates = torch.where(
|
| 264 |
-
reachable.unsqueeze(1), candidates, torch.zeros_like(candidates)
|
| 265 |
-
)
|
| 266 |
-
value = torch.logsumexp(safe_candidates, dim=1)
|
| 267 |
-
beta[:, start] = torch.where(reachable, value, beta[:, start])
|
| 268 |
-
return beta
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
def _map_path(
|
| 272 |
-
scores: torch.Tensor,
|
| 273 |
-
valid: torch.Tensor,
|
| 274 |
-
lengths: torch.Tensor,
|
| 275 |
-
preferred: torch.Tensor,
|
| 276 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 277 |
-
batch, boundaries, _ = scores.shape
|
| 278 |
-
negative_infinity = torch.tensor(float("-inf"), dtype=scores.dtype, device=scores.device)
|
| 279 |
-
best = torch.full((batch, boundaries), negative_infinity, dtype=scores.dtype, device=scores.device)
|
| 280 |
-
preference_count = torch.full((batch, boundaries), -1, dtype=torch.long, device=scores.device)
|
| 281 |
-
backpointer = torch.zeros(batch, boundaries, dtype=torch.long, device=scores.device)
|
| 282 |
-
best[:, 0] = 0
|
| 283 |
-
preference_count[:, 0] = 0
|
| 284 |
-
|
| 285 |
-
for end in range(1, boundaries):
|
| 286 |
-
candidates = best[:, :end] + scores[:, :end, end]
|
| 287 |
-
candidates = torch.where(valid[:, :end, end], candidates, negative_infinity)
|
| 288 |
-
best_score = candidates.max(dim=1).values
|
| 289 |
-
roundoff = 8.0 * torch.finfo(scores.dtype).eps * torch.maximum(torch.ones_like(best_score), best_score.abs())
|
| 290 |
-
score_ties = torch.isfinite(candidates) & (
|
| 291 |
-
(candidates - best_score.unsqueeze(1)).abs() <= roundoff.unsqueeze(1)
|
| 292 |
-
)
|
| 293 |
-
candidate_preferences = preference_count[:, :end] + preferred[:, :end, end].long()
|
| 294 |
-
candidate_preferences = torch.where(
|
| 295 |
-
score_ties, candidate_preferences, torch.full_like(candidate_preferences, -1)
|
| 296 |
-
)
|
| 297 |
-
best_preference = candidate_preferences.max(dim=1).values
|
| 298 |
-
preference_ties = score_ties & (candidate_preferences == best_preference.unsqueeze(1))
|
| 299 |
-
starts = torch.arange(end, device=scores.device).unsqueeze(0).expand(batch, -1)
|
| 300 |
-
chosen_start = torch.where(preference_ties, starts, torch.full_like(starts, -1)).max(dim=1).values
|
| 301 |
-
chosen_score = candidates.gather(1, chosen_start.clamp_min(0).unsqueeze(1)).squeeze(1)
|
| 302 |
-
active = end <= lengths
|
| 303 |
-
best[:, end] = torch.where(active, chosen_score, negative_infinity)
|
| 304 |
-
preference_count[:, end] = torch.where(active, best_preference, torch.full_like(best_preference, -1))
|
| 305 |
-
backpointer[:, end] = torch.where(active, chosen_start.clamp_min(0), torch.zeros_like(chosen_start))
|
| 306 |
-
|
| 307 |
-
rows = torch.arange(batch, device=scores.device)
|
| 308 |
-
map_scores = best[rows, lengths]
|
| 309 |
-
selected = torch.zeros_like(valid)
|
| 310 |
-
boundary = lengths.clone()
|
| 311 |
-
for _ in range(boundaries - 1):
|
| 312 |
-
active = boundary > 0
|
| 313 |
-
active_rows = rows[active]
|
| 314 |
-
active_end = boundary[active]
|
| 315 |
-
active_start = backpointer[active_rows, active_end]
|
| 316 |
-
selected[active_rows, active_start, active_end] = True
|
| 317 |
-
boundary = torch.where(active, backpointer[rows, boundary], boundary)
|
| 318 |
-
return selected, map_scores
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
def solve_lattice(
|
| 322 |
-
span_scores: torch.Tensor,
|
| 323 |
-
span_mask: torch.Tensor,
|
| 324 |
-
lengths: torch.Tensor,
|
| 325 |
-
*,
|
| 326 |
-
preferred_edges: torch.Tensor | None = None,
|
| 327 |
-
) -> LatticeResult:
|
| 328 |
-
"""Solve every finite segmentation DAG exactly.
|
| 329 |
-
|
| 330 |
-
A valid edge ``(i, j)`` emits one token spanning atom positions ``[i, j)``.
|
| 331 |
-
Probabilistic recurrences use float32 unless the caller requests float64.
|
| 332 |
-
"""
|
| 333 |
-
|
| 334 |
-
scores, valid, lengths, preferred = _validate_inputs(span_scores, span_mask, lengths, preferred_edges)
|
| 335 |
-
alpha = _forward(scores, valid, lengths)
|
| 336 |
-
rows = torch.arange(scores.shape[0], device=scores.device)
|
| 337 |
-
log_partition = alpha[rows, lengths]
|
| 338 |
-
if not bool(torch.isfinite(log_partition).all()):
|
| 339 |
-
raise ValueError("every sentence lattice must contain a complete path")
|
| 340 |
-
beta = _backward(scores, valid, lengths)
|
| 341 |
-
log_marginals = alpha.unsqueeze(2) + scores + beta.unsqueeze(1) - log_partition.view(-1, 1, 1)
|
| 342 |
-
safe_log_marginals = torch.where(valid, log_marginals, torch.zeros_like(log_marginals))
|
| 343 |
-
marginals = torch.where(valid, torch.exp(safe_log_marginals), torch.zeros_like(scores))
|
| 344 |
-
map_edges, map_scores = _map_path(scores, valid, lengths, preferred)
|
| 345 |
-
if not bool(torch.isfinite(marginals).all()):
|
| 346 |
-
raise FloatingPointError("nonfinite segmentation marginals")
|
| 347 |
-
return LatticeResult(log_partition, marginals, map_edges, map_scores)
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
def preferred_path_nll(
|
| 351 |
-
lattice: LatticeResult,
|
| 352 |
-
span_scores: torch.Tensor,
|
| 353 |
-
span_mask: torch.Tensor,
|
| 354 |
-
lengths: torch.Tensor,
|
| 355 |
-
preferred_edges: torch.Tensor,
|
| 356 |
-
) -> torch.Tensor:
|
| 357 |
-
"""Return exact preferred-path NLL, normalized by preferred token count.
|
| 358 |
-
|
| 359 |
-
The result contains one scalar per sentence. ``span_scores`` must be the
|
| 360 |
-
same temperature-scaled scores used to produce ``lattice``.
|
| 361 |
-
"""
|
| 362 |
-
|
| 363 |
-
scores, _, checked_lengths, preferred = _validate_inputs(
|
| 364 |
-
span_scores,
|
| 365 |
-
span_mask,
|
| 366 |
-
lengths,
|
| 367 |
-
preferred_edges,
|
| 368 |
-
)
|
| 369 |
-
batch, boundaries, _ = scores.shape
|
| 370 |
-
if lattice.log_partition.shape != (batch,) or lattice.log_partition.device != scores.device:
|
| 371 |
-
raise ValueError("lattice partition must align with preferred paths")
|
| 372 |
-
|
| 373 |
-
incoming = preferred.sum(dim=1)
|
| 374 |
-
outgoing = preferred.sum(dim=2)
|
| 375 |
-
rows = torch.arange(batch, device=scores.device)
|
| 376 |
-
positions = torch.arange(boundaries, device=scores.device).unsqueeze(0)
|
| 377 |
-
internal = (positions > 0) & (positions < checked_lengths.unsqueeze(1))
|
| 378 |
-
internal_flow = (~internal) | (
|
| 379 |
-
(incoming == outgoing)
|
| 380 |
-
& (incoming <= 1)
|
| 381 |
-
& (outgoing <= 1)
|
| 382 |
-
)
|
| 383 |
-
complete = (
|
| 384 |
-
(outgoing[:, 0] == 1)
|
| 385 |
-
& (incoming[rows, checked_lengths] == 1)
|
| 386 |
-
& internal_flow.all(dim=1)
|
| 387 |
-
)
|
| 388 |
-
if not bool(complete.all()):
|
| 389 |
-
raise ValueError("preferred edges must form one complete path per sentence")
|
| 390 |
-
|
| 391 |
-
token_counts = preferred.sum(dim=(1, 2))
|
| 392 |
-
path_scores = (scores * preferred.to(scores.dtype)).sum(dim=(1, 2))
|
| 393 |
-
nll = (lattice.log_partition - path_scores) / token_counts.to(scores.dtype)
|
| 394 |
-
if not bool(torch.isfinite(nll).all()):
|
| 395 |
-
raise FloatingPointError("nonfinite preferred-path NLL")
|
| 396 |
-
return nll
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_model.py
DELETED
|
@@ -1,1196 +0,0 @@
|
|
| 1 |
-
"""Differentiable open-vocabulary static sentence encoder."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections import deque
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
import hashlib
|
| 8 |
-
import math
|
| 9 |
-
from collections.abc import Sequence
|
| 10 |
-
|
| 11 |
-
import torch
|
| 12 |
-
from torch import nn
|
| 13 |
-
from torch.nn import functional as F
|
| 14 |
-
|
| 15 |
-
from starse.openlex_dictionary import (
|
| 16 |
-
dictionary_expected_numerator,
|
| 17 |
-
dictionary_ordered_map_numerator,
|
| 18 |
-
dictionary_span_values,
|
| 19 |
-
)
|
| 20 |
-
from starse.openlex_lattice import (
|
| 21 |
-
LatticeResult,
|
| 22 |
-
expected_span_sum,
|
| 23 |
-
factored_expected_sum,
|
| 24 |
-
factored_map_sum,
|
| 25 |
-
ordered_map_span_sum,
|
| 26 |
-
preferred_boundary_spans,
|
| 27 |
-
preferred_path_nll,
|
| 28 |
-
solve_lattice,
|
| 29 |
-
)
|
| 30 |
-
from starse.openlex_spans import (
|
| 31 |
-
ContextFreeSpanGenerator,
|
| 32 |
-
SpanKeys,
|
| 33 |
-
exact_span_keys as exact_span_keys,
|
| 34 |
-
)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
@dataclass(frozen=True)
|
| 38 |
-
class OpenLexOutput:
|
| 39 |
-
embeddings: torch.Tensor
|
| 40 |
-
relaxed_embeddings: torch.Tensor
|
| 41 |
-
map_embeddings: torch.Tensor
|
| 42 |
-
soft_numerators: torch.Tensor
|
| 43 |
-
map_numerators: torch.Tensor
|
| 44 |
-
lattice: LatticeResult
|
| 45 |
-
span_codes: torch.Tensor | None
|
| 46 |
-
preferred_path_nll: torch.Tensor
|
| 47 |
-
teacher_span_distortion: torch.Tensor | None
|
| 48 |
-
map_keys: tuple[tuple[tuple[int, ...], ...], ...] | None
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
@dataclass(frozen=True)
|
| 52 |
-
class OpenLexStepMetrics:
|
| 53 |
-
loss: float
|
| 54 |
-
residual_gradient_norm: float
|
| 55 |
-
score_gradient_norm: float
|
| 56 |
-
expected_token_count: float
|
| 57 |
-
map_token_count: float
|
| 58 |
-
map_keys_before: tuple[tuple[tuple[int, ...], ...], ...]
|
| 59 |
-
map_keys_after: tuple[tuple[tuple[int, ...], ...], ...]
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
@dataclass(frozen=True)
|
| 63 |
-
class DocumentFolds:
|
| 64 |
-
support: tuple[int, ...]
|
| 65 |
-
query: tuple[int, ...]
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
@dataclass(frozen=True)
|
| 69 |
-
class OpenLexCrossfitObjective:
|
| 70 |
-
total: torch.Tensor
|
| 71 |
-
support_loss: torch.Tensor
|
| 72 |
-
query_loss: torch.Tensor
|
| 73 |
-
mdl: torch.Tensor
|
| 74 |
-
gap: torch.Tensor
|
| 75 |
-
original_path_nll: torch.Tensor
|
| 76 |
-
teacher_span_distortion: torch.Tensor
|
| 77 |
-
expected_token_count: torch.Tensor
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def split_document_folds(document_ids: Sequence[str], *, seed: int) -> DocumentFolds:
|
| 81 |
-
"""Assign every view of one document to one deterministic cross-fit fold."""
|
| 82 |
-
|
| 83 |
-
if len(document_ids) < 2 or any(not isinstance(value, str) or not value for value in document_ids):
|
| 84 |
-
raise ValueError("cross-fit needs at least two nonempty document ids")
|
| 85 |
-
decisions: dict[str, int] = {}
|
| 86 |
-
hashes: dict[str, int] = {}
|
| 87 |
-
for document_id in document_ids:
|
| 88 |
-
digest = hashlib.blake2b(digest_size=8, person=b"openlex")
|
| 89 |
-
digest.update(int(seed).to_bytes(8, byteorder="little", signed=True))
|
| 90 |
-
digest.update(document_id.encode("utf-8"))
|
| 91 |
-
value = int.from_bytes(digest.digest(), byteorder="little")
|
| 92 |
-
hashes[document_id] = value
|
| 93 |
-
decisions[document_id] = value & 1
|
| 94 |
-
if len(set(decisions.values())) < 2:
|
| 95 |
-
unique = sorted(decisions, key=lambda value: (hashes[value], value))
|
| 96 |
-
midpoint = max(1, len(unique) // 2)
|
| 97 |
-
decisions = {document_id: int(position >= midpoint) for position, document_id in enumerate(unique)}
|
| 98 |
-
support = tuple(index for index, document_id in enumerate(document_ids) if decisions[document_id] == 0)
|
| 99 |
-
query = tuple(index for index, document_id in enumerate(document_ids) if decisions[document_id] == 1)
|
| 100 |
-
if not support or not query:
|
| 101 |
-
raise ValueError("cross-fit document identities cannot form two nonempty folds")
|
| 102 |
-
return DocumentFolds(support, query)
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def type_presence_description_length(
|
| 106 |
-
marginals: torch.Tensor,
|
| 107 |
-
keys: SpanKeys,
|
| 108 |
-
*,
|
| 109 |
-
atom_vocab_size: int,
|
| 110 |
-
residual_rank: int,
|
| 111 |
-
) -> torch.Tensor:
|
| 112 |
-
"""Charge each exact multi-atom type by its smooth batch presence."""
|
| 113 |
-
|
| 114 |
-
if marginals.ndim != 3 or not marginals.is_floating_point():
|
| 115 |
-
raise ValueError("description length requires floating rank-3 marginals")
|
| 116 |
-
if atom_vocab_size <= 1 or residual_rank <= 0:
|
| 117 |
-
raise ValueError("description length dimensions must be positive")
|
| 118 |
-
if len(keys) != marginals.shape[0]:
|
| 119 |
-
raise ValueError("description length keys and marginals disagree on batch size")
|
| 120 |
-
occurrences: dict[tuple[int, ...], list[torch.Tensor]] = {}
|
| 121 |
-
for sentence, sentence_keys in enumerate(keys):
|
| 122 |
-
if len(sentence_keys) != marginals.shape[1]:
|
| 123 |
-
raise ValueError("description length keys and marginals disagree on boundaries")
|
| 124 |
-
for start, row in enumerate(sentence_keys):
|
| 125 |
-
if len(row) != marginals.shape[2]:
|
| 126 |
-
raise ValueError("description length key row has the wrong boundary count")
|
| 127 |
-
for end, key in enumerate(row):
|
| 128 |
-
if key is not None and len(key) > 1:
|
| 129 |
-
occurrences.setdefault(key, []).append(marginals[sentence, start, end])
|
| 130 |
-
if not occurrences:
|
| 131 |
-
return marginals.sum() * 0.0
|
| 132 |
-
total = marginals.sum() * 0.0
|
| 133 |
-
spelling_bits = math.log2(atom_vocab_size)
|
| 134 |
-
for key, values in occurrences.items():
|
| 135 |
-
probability = 1.0 - torch.prod(1.0 - torch.stack(values).clamp(0.0, 1.0))
|
| 136 |
-
cost = len(key) * spelling_bits + residual_rank * 16.0
|
| 137 |
-
total = total + probability * cost
|
| 138 |
-
return total
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
def expected_occurrence_description_length(
|
| 142 |
-
marginals: torch.Tensor,
|
| 143 |
-
*,
|
| 144 |
-
atom_vocab_size: int,
|
| 145 |
-
residual_rank: int,
|
| 146 |
-
) -> torch.Tensor:
|
| 147 |
-
"""Tensor-only expected corpus code length for multi-atom emissions."""
|
| 148 |
-
|
| 149 |
-
if marginals.ndim != 3 or marginals.shape[1] != marginals.shape[2]:
|
| 150 |
-
raise ValueError("occurrence description length needs square span marginals")
|
| 151 |
-
if not marginals.is_floating_point():
|
| 152 |
-
raise TypeError("occurrence description length marginals must be floating point")
|
| 153 |
-
if atom_vocab_size <= 1 or residual_rank <= 0:
|
| 154 |
-
raise ValueError("occurrence description length dimensions must be positive")
|
| 155 |
-
boundaries = marginals.shape[1]
|
| 156 |
-
positions = torch.arange(boundaries, device=marginals.device)
|
| 157 |
-
span_lengths = positions.unsqueeze(0) - positions.unsqueeze(1)
|
| 158 |
-
costs = torch.where(
|
| 159 |
-
span_lengths > 1,
|
| 160 |
-
span_lengths.float() * math.log2(atom_vocab_size) + residual_rank * 16.0,
|
| 161 |
-
torch.zeros_like(span_lengths, dtype=torch.float32),
|
| 162 |
-
)
|
| 163 |
-
return (marginals * costs.to(marginals.dtype).unsqueeze(0)).sum() / marginals.shape[0]
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def _selected_keys(atom_ids: torch.Tensor, map_edges: torch.Tensor) -> tuple[tuple[tuple[int, ...], ...], ...]:
|
| 167 |
-
selected_sentences: list[tuple[tuple[int, ...], ...]] = []
|
| 168 |
-
atoms = atom_ids.detach().to(device="cpu", dtype=torch.long)
|
| 169 |
-
for sentence, sentence_edges in enumerate(map_edges.detach().to(device="cpu")):
|
| 170 |
-
ordered = sorted(
|
| 171 |
-
(
|
| 172 |
-
(
|
| 173 |
-
int(start),
|
| 174 |
-
tuple(int(value) for value in atoms[sentence, int(start) : int(end)]),
|
| 175 |
-
)
|
| 176 |
-
for start, end in sentence_edges.nonzero(as_tuple=False).tolist()
|
| 177 |
-
),
|
| 178 |
-
key=lambda item: item[0],
|
| 179 |
-
)
|
| 180 |
-
selected_sentences.append(tuple(key for _, key in ordered))
|
| 181 |
-
return tuple(selected_sentences)
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
def _override_correction(
|
| 185 |
-
atom_values: torch.Tensor,
|
| 186 |
-
embedding: nn.Embedding,
|
| 187 |
-
residual_codes: torch.Tensor,
|
| 188 |
-
projection: torch.Tensor,
|
| 189 |
-
override_ids: torch.Tensor,
|
| 190 |
-
edge_weights: torch.Tensor,
|
| 191 |
-
*,
|
| 192 |
-
detach_embedding: bool,
|
| 193 |
-
) -> torch.Tensor:
|
| 194 |
-
"""Replace canonical atom sums by exact existing-token rows sparsely."""
|
| 195 |
-
|
| 196 |
-
batch, atom_count, dimension = atom_values.shape
|
| 197 |
-
boundaries = atom_count + 1
|
| 198 |
-
if override_ids.shape != (batch, boundaries, boundaries):
|
| 199 |
-
raise ValueError("override ids must align with canonical atom boundaries")
|
| 200 |
-
if override_ids.dtype not in (torch.int32, torch.int64):
|
| 201 |
-
raise TypeError("override ids must be integer token rows")
|
| 202 |
-
if override_ids.device != atom_values.device or edge_weights.device != atom_values.device:
|
| 203 |
-
raise ValueError("override tensors must share the atom device")
|
| 204 |
-
registered = override_ids >= 0
|
| 205 |
-
if bool((override_ids[registered] >= embedding.num_embeddings).any()):
|
| 206 |
-
raise ValueError("override id lies outside the existing embedding table")
|
| 207 |
-
locations = registered.nonzero(as_tuple=False)
|
| 208 |
-
result = torch.zeros(batch, dimension, dtype=torch.float32, device=atom_values.device)
|
| 209 |
-
if not locations.numel():
|
| 210 |
-
return result
|
| 211 |
-
sentence, start, end = locations.unbind(dim=1)
|
| 212 |
-
prefix = torch.cat(
|
| 213 |
-
(
|
| 214 |
-
torch.zeros(batch, 1, dimension, dtype=torch.float32, device=atom_values.device),
|
| 215 |
-
atom_values.float().cumsum(dim=1),
|
| 216 |
-
),
|
| 217 |
-
dim=1,
|
| 218 |
-
)
|
| 219 |
-
canonical = prefix[sentence, end] - prefix[sentence, start]
|
| 220 |
-
exact = embedding(override_ids[sentence, start, end].long()).float()
|
| 221 |
-
if detach_embedding:
|
| 222 |
-
exact = exact.detach()
|
| 223 |
-
residual = residual_codes[sentence, start, end].float() @ projection.float().T
|
| 224 |
-
weights = edge_weights[sentence, start, end].float().unsqueeze(1)
|
| 225 |
-
result.index_add_(0, sentence, (exact - canonical - residual) * weights)
|
| 226 |
-
return result
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
def _map_sum_with_overrides(
|
| 230 |
-
atom_values: torch.Tensor,
|
| 231 |
-
embedding: nn.Embedding,
|
| 232 |
-
residual_codes: torch.Tensor,
|
| 233 |
-
projection: torch.Tensor,
|
| 234 |
-
map_edges: torch.Tensor,
|
| 235 |
-
override_ids: torch.Tensor,
|
| 236 |
-
*,
|
| 237 |
-
detach_embedding: bool,
|
| 238 |
-
) -> torch.Tensor:
|
| 239 |
-
"""Materialize MAP tokens in path order, including exact base-row overrides."""
|
| 240 |
-
|
| 241 |
-
sentences: list[torch.Tensor] = []
|
| 242 |
-
for sentence in range(atom_values.shape[0]):
|
| 243 |
-
numerator = torch.zeros(atom_values.shape[2], dtype=torch.float32, device=atom_values.device)
|
| 244 |
-
for start, end in map_edges[sentence].nonzero(as_tuple=False).tolist():
|
| 245 |
-
row = int(override_ids[sentence, start, end])
|
| 246 |
-
if row >= 0:
|
| 247 |
-
base = embedding.weight[row].float()
|
| 248 |
-
if detach_embedding:
|
| 249 |
-
base = base.detach()
|
| 250 |
-
residual = torch.zeros_like(base)
|
| 251 |
-
else:
|
| 252 |
-
base = atom_values[sentence, start:end].float().sum(dim=0)
|
| 253 |
-
residual = projection.float() @ residual_codes[sentence, start, end].float()
|
| 254 |
-
token_vector = base + residual
|
| 255 |
-
numerator = numerator + token_vector
|
| 256 |
-
sentences.append(numerator)
|
| 257 |
-
result = torch.stack(sentences)
|
| 258 |
-
if not bool(torch.isfinite(result).all()):
|
| 259 |
-
raise FloatingPointError("nonfinite overridden OpenLex MAP sum")
|
| 260 |
-
return result
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
def teacher_span_distortion(
|
| 264 |
-
atom_values: torch.Tensor,
|
| 265 |
-
embedding: nn.Embedding,
|
| 266 |
-
residual_codes: torch.Tensor,
|
| 267 |
-
projection: torch.Tensor,
|
| 268 |
-
override_ids: torch.Tensor,
|
| 269 |
-
preferred_edges: torch.Tensor,
|
| 270 |
-
span_mask: torch.Tensor,
|
| 271 |
-
marginals: torch.Tensor,
|
| 272 |
-
*,
|
| 273 |
-
teacher_embedding_weight: torch.Tensor | None = None,
|
| 274 |
-
candidate_span_values: torch.Tensor | None = None,
|
| 275 |
-
detach_embedding: bool = False,
|
| 276 |
-
detach_marginals: bool = False,
|
| 277 |
-
require_registered_preferred: bool = False,
|
| 278 |
-
) -> torch.Tensor:
|
| 279 |
-
"""Distil candidate contributions toward the original-tokenizer geometry."""
|
| 280 |
-
|
| 281 |
-
if atom_values.ndim != 3 or not atom_values.is_floating_point():
|
| 282 |
-
raise ValueError("teacher span distortion needs floating rank-3 atom values")
|
| 283 |
-
batch, atom_count, dimension = atom_values.shape
|
| 284 |
-
boundaries = atom_count + 1
|
| 285 |
-
edge_shape = (batch, boundaries, boundaries)
|
| 286 |
-
if residual_codes.shape[:3] != edge_shape or residual_codes.ndim != 4:
|
| 287 |
-
raise ValueError("teacher span residual codes must align with atom boundaries")
|
| 288 |
-
if projection.shape != (dimension, residual_codes.shape[3]):
|
| 289 |
-
raise ValueError("teacher span projection and residual rank disagree")
|
| 290 |
-
if override_ids.shape != edge_shape or override_ids.dtype not in (torch.int32, torch.int64):
|
| 291 |
-
raise ValueError("teacher span override ids must align with atom boundaries")
|
| 292 |
-
if preferred_edges.shape != edge_shape or preferred_edges.dtype is not torch.bool:
|
| 293 |
-
raise ValueError("teacher span preferred edges must align with atom boundaries")
|
| 294 |
-
if span_mask.shape != edge_shape or span_mask.dtype is not torch.bool:
|
| 295 |
-
raise ValueError("teacher span mask must align with atom boundaries")
|
| 296 |
-
if marginals.shape != edge_shape or not marginals.is_floating_point():
|
| 297 |
-
raise ValueError("teacher span marginals must align with atom boundaries")
|
| 298 |
-
tensors = (residual_codes, projection, override_ids, preferred_edges, span_mask, marginals)
|
| 299 |
-
if any(tensor.device != atom_values.device for tensor in tensors):
|
| 300 |
-
raise ValueError("teacher span tensors must share one device")
|
| 301 |
-
if candidate_span_values is not None:
|
| 302 |
-
if (
|
| 303 |
-
candidate_span_values.shape != (*edge_shape, dimension)
|
| 304 |
-
or not candidate_span_values.is_floating_point()
|
| 305 |
-
):
|
| 306 |
-
raise ValueError("teacher candidate span values must align with atom boundaries")
|
| 307 |
-
if candidate_span_values.device != atom_values.device:
|
| 308 |
-
raise ValueError("teacher candidate span values must share the atom device")
|
| 309 |
-
if not bool(torch.isfinite(candidate_span_values).all()):
|
| 310 |
-
raise ValueError("teacher candidate span values must be finite")
|
| 311 |
-
if bool((preferred_edges & ~span_mask).any()):
|
| 312 |
-
raise ValueError("teacher preferred edges must be legal spans")
|
| 313 |
-
if bool(((override_ids >= embedding.num_embeddings) & span_mask).any()):
|
| 314 |
-
raise ValueError("teacher span override id lies outside the embedding table")
|
| 315 |
-
if teacher_embedding_weight is None:
|
| 316 |
-
teacher_weight = embedding.weight
|
| 317 |
-
else:
|
| 318 |
-
if teacher_embedding_weight.shape != embedding.weight.shape:
|
| 319 |
-
raise ValueError("teacher embedding snapshot must match the candidate table")
|
| 320 |
-
if teacher_embedding_weight.device != atom_values.device:
|
| 321 |
-
raise ValueError("teacher embedding snapshot must share the atom device")
|
| 322 |
-
teacher_weight = teacher_embedding_weight
|
| 323 |
-
|
| 324 |
-
atom_work = atom_values.float()
|
| 325 |
-
teacher_atoms = torch.zeros(
|
| 326 |
-
batch,
|
| 327 |
-
atom_count,
|
| 328 |
-
dimension,
|
| 329 |
-
dtype=torch.float32,
|
| 330 |
-
device=atom_values.device,
|
| 331 |
-
)
|
| 332 |
-
coverage = torch.zeros(batch, atom_count, dtype=torch.int32, device=atom_values.device)
|
| 333 |
-
for sentence, start, end in preferred_edges.nonzero(as_tuple=False).tolist():
|
| 334 |
-
row = int(override_ids[sentence, start, end])
|
| 335 |
-
if row < 0:
|
| 336 |
-
if require_registered_preferred:
|
| 337 |
-
raise ValueError("canonical preferred edge must name an existing token row")
|
| 338 |
-
contribution = atom_work[sentence, start:end].sum(dim=0).detach()
|
| 339 |
-
else:
|
| 340 |
-
contribution = teacher_weight[row].float().detach()
|
| 341 |
-
teacher_atoms[sentence, start:end] += contribution / float(end - start)
|
| 342 |
-
coverage[sentence, start:end] += 1
|
| 343 |
-
active_atoms = span_mask[:, :-1, 1:].diagonal(dim1=1, dim2=2)
|
| 344 |
-
if not bool(torch.equal(coverage == 1, active_atoms)):
|
| 345 |
-
raise ValueError("preferred edges must cover every active atom exactly once")
|
| 346 |
-
|
| 347 |
-
zero = torch.zeros(batch, 1, dimension, dtype=torch.float32, device=atom_values.device)
|
| 348 |
-
teacher_prefix = torch.cat((zero, teacher_atoms.cumsum(dim=1)), dim=1)
|
| 349 |
-
teacher_values = teacher_prefix.unsqueeze(1) - teacher_prefix.unsqueeze(2)
|
| 350 |
-
if candidate_span_values is None:
|
| 351 |
-
atom_prefix = torch.cat((zero, atom_work.cumsum(dim=1)), dim=1)
|
| 352 |
-
candidate_values = atom_prefix.unsqueeze(1) - atom_prefix.unsqueeze(2)
|
| 353 |
-
residual_values = residual_codes.float() @ projection.float().T
|
| 354 |
-
candidate_values = candidate_values + residual_values
|
| 355 |
-
|
| 356 |
-
registered = override_ids >= 0
|
| 357 |
-
exact_values = embedding(override_ids.clamp_min(0).long()).float()
|
| 358 |
-
if detach_embedding:
|
| 359 |
-
exact_values = exact_values.detach()
|
| 360 |
-
candidate_values = torch.where(registered.unsqueeze(3), exact_values, candidate_values)
|
| 361 |
-
else:
|
| 362 |
-
candidate_values = candidate_span_values.float()
|
| 363 |
-
relative_error = (candidate_values - teacher_values).square().sum(dim=3) / teacher_values.square().sum(
|
| 364 |
-
dim=3
|
| 365 |
-
).clamp_min(1e-6)
|
| 366 |
-
marginal_weights = marginals.detach() if detach_marginals else marginals
|
| 367 |
-
weights = torch.where(
|
| 368 |
-
span_mask,
|
| 369 |
-
marginal_weights.float(),
|
| 370 |
-
torch.zeros_like(marginals, dtype=torch.float32),
|
| 371 |
-
)
|
| 372 |
-
normalizer = weights.sum(dim=(1, 2)).clamp_min(1e-6)
|
| 373 |
-
distortion = (relative_error * weights).sum(dim=(1, 2)) / normalizer
|
| 374 |
-
result = distortion.mean()
|
| 375 |
-
if not bool(torch.isfinite(result)):
|
| 376 |
-
raise FloatingPointError("nonfinite teacher span distortion")
|
| 377 |
-
return result
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
class OpenLexEncoder(nn.Module):
|
| 381 |
-
"""Use exact span marginals during training and a deterministic MAP path."""
|
| 382 |
-
|
| 383 |
-
def __init__(
|
| 384 |
-
self,
|
| 385 |
-
atom_vocab_size: int,
|
| 386 |
-
dimension: int,
|
| 387 |
-
generator_atom_dim: int,
|
| 388 |
-
generator_hidden_dim: int,
|
| 389 |
-
residual_rank: int,
|
| 390 |
-
*,
|
| 391 |
-
initial_non_atomic_score: float = -2.0,
|
| 392 |
-
existing_token_length_bonus: float = 0.0,
|
| 393 |
-
detach_base_for_openlex: bool = False,
|
| 394 |
-
straight_through_map: bool = False,
|
| 395 |
-
disable_candidate_residuals: bool = False,
|
| 396 |
-
dictionary_composition: bool = False,
|
| 397 |
-
boundary_misalignment_penalty: float = 0.0,
|
| 398 |
-
initial_temperature: float = 1.0,
|
| 399 |
-
minimum_temperature: float = 0.05,
|
| 400 |
-
maximum_temperature: float = 5.0,
|
| 401 |
-
) -> None:
|
| 402 |
-
super().__init__()
|
| 403 |
-
if dimension < residual_rank:
|
| 404 |
-
raise ValueError("OpenLex dimension must be at least the residual rank")
|
| 405 |
-
if not math.isfinite(existing_token_length_bonus) or existing_token_length_bonus < 0:
|
| 406 |
-
raise ValueError("existing-token length bonus must be finite and nonnegative")
|
| 407 |
-
if not math.isfinite(boundary_misalignment_penalty) or boundary_misalignment_penalty < 0:
|
| 408 |
-
raise ValueError("boundary misalignment penalty must be finite and nonnegative")
|
| 409 |
-
if not 0 < minimum_temperature < initial_temperature < maximum_temperature:
|
| 410 |
-
raise ValueError("initial temperature must lie strictly inside its bounds")
|
| 411 |
-
self.atom_vocab_size = int(atom_vocab_size)
|
| 412 |
-
self.dimension = int(dimension)
|
| 413 |
-
self.residual_rank = int(residual_rank)
|
| 414 |
-
self.existing_token_length_bonus = float(existing_token_length_bonus)
|
| 415 |
-
self.detach_base_for_openlex = bool(detach_base_for_openlex)
|
| 416 |
-
self.straight_through_map = bool(straight_through_map)
|
| 417 |
-
self.disable_candidate_residuals = bool(disable_candidate_residuals)
|
| 418 |
-
self.dictionary_composition = bool(dictionary_composition)
|
| 419 |
-
self.boundary_misalignment_penalty = float(boundary_misalignment_penalty)
|
| 420 |
-
self.minimum_temperature = float(minimum_temperature)
|
| 421 |
-
self.maximum_temperature = float(maximum_temperature)
|
| 422 |
-
|
| 423 |
-
self.atom_embedding = nn.Embedding(self.atom_vocab_size, self.dimension)
|
| 424 |
-
self.span_generator = ContextFreeSpanGenerator(
|
| 425 |
-
self.atom_vocab_size,
|
| 426 |
-
generator_atom_dim,
|
| 427 |
-
generator_hidden_dim,
|
| 428 |
-
self.residual_rank,
|
| 429 |
-
initial_non_atomic_score=initial_non_atomic_score,
|
| 430 |
-
)
|
| 431 |
-
projection = torch.zeros(self.dimension, self.residual_rank)
|
| 432 |
-
projection[: self.residual_rank] = torch.eye(self.residual_rank)
|
| 433 |
-
self.residual_projection = nn.Parameter(projection)
|
| 434 |
-
fraction = (initial_temperature - minimum_temperature) / (maximum_temperature - minimum_temperature)
|
| 435 |
-
self.raw_temperature = nn.Parameter(torch.tensor(math.log(fraction / (1.0 - fraction)), dtype=torch.float32))
|
| 436 |
-
self.register_buffer("teacher_embedding_weight", torch.empty(0), persistent=False)
|
| 437 |
-
self.candidate_atom_embedding: nn.Embedding | None = None
|
| 438 |
-
self._cache_audit_active = False
|
| 439 |
-
self._cache_phase: str | None = None
|
| 440 |
-
self._cache_records: deque[tuple[torch.Tensor, torch.Tensor, torch.Tensor]] = deque()
|
| 441 |
-
|
| 442 |
-
@property
|
| 443 |
-
def temperature(self) -> torch.Tensor:
|
| 444 |
-
fraction = self.raw_temperature.sigmoid()
|
| 445 |
-
return self.minimum_temperature + (self.maximum_temperature - self.minimum_temperature) * fraction
|
| 446 |
-
|
| 447 |
-
def set_teacher_embedding_weight(self, weight: torch.Tensor) -> None:
|
| 448 |
-
"""Freeze a non-persistent copy of the original static table for distillation."""
|
| 449 |
-
|
| 450 |
-
if weight.shape != self.atom_embedding.weight.shape or not weight.is_floating_point():
|
| 451 |
-
raise ValueError("teacher embedding snapshot must match the OpenLex table")
|
| 452 |
-
self.teacher_embedding_weight = weight.detach().to(
|
| 453 |
-
device=self.atom_embedding.weight.device,
|
| 454 |
-
dtype=self.atom_embedding.weight.dtype,
|
| 455 |
-
).clone()
|
| 456 |
-
|
| 457 |
-
def set_candidate_atom_weight(self, weight: torch.Tensor, *, trainable: bool) -> None:
|
| 458 |
-
"""Install a separate compositional atom table for novel spans."""
|
| 459 |
-
|
| 460 |
-
if weight.shape != self.atom_embedding.weight.shape or not weight.is_floating_point():
|
| 461 |
-
raise ValueError("candidate atom table must match the OpenLex table")
|
| 462 |
-
candidate = weight.detach().to(
|
| 463 |
-
device=self.atom_embedding.weight.device,
|
| 464 |
-
dtype=self.atom_embedding.weight.dtype,
|
| 465 |
-
).clone()
|
| 466 |
-
self.candidate_atom_embedding = nn.Embedding.from_pretrained(
|
| 467 |
-
candidate,
|
| 468 |
-
freeze=not trainable,
|
| 469 |
-
)
|
| 470 |
-
|
| 471 |
-
def start_cache_audit(self) -> None:
|
| 472 |
-
if self._cache_phase is not None or self._cache_records:
|
| 473 |
-
raise RuntimeError("previous OpenLex cache audit is incomplete")
|
| 474 |
-
self._cache_audit_active = True
|
| 475 |
-
|
| 476 |
-
def begin_cache_record(self) -> None:
|
| 477 |
-
if self._cache_audit_active:
|
| 478 |
-
if self._cache_phase is not None:
|
| 479 |
-
raise RuntimeError("OpenLex cache phase is already active")
|
| 480 |
-
self._cache_phase = "record"
|
| 481 |
-
|
| 482 |
-
def begin_cache_replay(self) -> None:
|
| 483 |
-
if self._cache_audit_active:
|
| 484 |
-
if self._cache_phase is not None:
|
| 485 |
-
raise RuntimeError("OpenLex cache phase is already active")
|
| 486 |
-
self._cache_phase = "replay"
|
| 487 |
-
|
| 488 |
-
def end_cache_phase(self) -> None:
|
| 489 |
-
self._cache_phase = None
|
| 490 |
-
|
| 491 |
-
def abort_cache_audit(self) -> None:
|
| 492 |
-
self._cache_records.clear()
|
| 493 |
-
self._cache_phase = None
|
| 494 |
-
self._cache_audit_active = False
|
| 495 |
-
|
| 496 |
-
def assert_cache_audit_empty(self) -> None:
|
| 497 |
-
if self._cache_phase is not None or self._cache_records:
|
| 498 |
-
raise RuntimeError("OpenLex cache audit did not replay every record")
|
| 499 |
-
self._cache_audit_active = False
|
| 500 |
-
|
| 501 |
-
def _audit_map(
|
| 502 |
-
self,
|
| 503 |
-
atom_ids: torch.Tensor,
|
| 504 |
-
lengths: torch.Tensor,
|
| 505 |
-
map_edges: torch.Tensor,
|
| 506 |
-
) -> None:
|
| 507 |
-
if not self._cache_audit_active or self._cache_phase is None:
|
| 508 |
-
return
|
| 509 |
-
values = tuple(tensor.detach().clone() for tensor in (atom_ids, lengths, map_edges))
|
| 510 |
-
if self._cache_phase == "record":
|
| 511 |
-
self._cache_records.append(values)
|
| 512 |
-
return
|
| 513 |
-
if self._cache_phase != "replay" or not self._cache_records:
|
| 514 |
-
self.abort_cache_audit()
|
| 515 |
-
raise RuntimeError("cache/replay MAP mismatch")
|
| 516 |
-
expected = self._cache_records.popleft()
|
| 517 |
-
if not all(
|
| 518 |
-
observed.shape == wanted.shape and torch.equal(observed, wanted)
|
| 519 |
-
for observed, wanted in zip(values, expected, strict=True)
|
| 520 |
-
):
|
| 521 |
-
self.abort_cache_audit()
|
| 522 |
-
raise RuntimeError("cache/replay MAP mismatch")
|
| 523 |
-
|
| 524 |
-
def forward(
|
| 525 |
-
self,
|
| 526 |
-
atom_ids: torch.Tensor,
|
| 527 |
-
lengths: torch.Tensor,
|
| 528 |
-
*,
|
| 529 |
-
preferred_edges: torch.Tensor | None = None,
|
| 530 |
-
override_ids: torch.Tensor | None = None,
|
| 531 |
-
include_keys: bool = False,
|
| 532 |
-
include_teacher_distillation: bool = False,
|
| 533 |
-
detach_teacher_routing: bool = False,
|
| 534 |
-
candidate_support: torch.Tensor | None = None,
|
| 535 |
-
) -> OpenLexOutput:
|
| 536 |
-
skip_residual_codes = (
|
| 537 |
-
self.disable_candidate_residuals
|
| 538 |
-
and self.dictionary_composition
|
| 539 |
-
and not include_teacher_distillation
|
| 540 |
-
)
|
| 541 |
-
spans = self.span_generator(
|
| 542 |
-
atom_ids,
|
| 543 |
-
lengths,
|
| 544 |
-
include_keys=False,
|
| 545 |
-
emit_residual_codes=not skip_residual_codes,
|
| 546 |
-
residual_mask=(
|
| 547 |
-
candidate_support
|
| 548 |
-
if candidate_support is not None and not include_teacher_distillation
|
| 549 |
-
else None
|
| 550 |
-
),
|
| 551 |
-
)
|
| 552 |
-
atom_count = atom_ids.shape[1]
|
| 553 |
-
boundaries = atom_count + 1
|
| 554 |
-
if preferred_edges is None:
|
| 555 |
-
preferred = torch.zeros_like(spans.mask)
|
| 556 |
-
positions = torch.arange(atom_count, device=atom_ids.device)
|
| 557 |
-
preferred[:, positions, positions + 1] = spans.mask[:, positions, positions + 1]
|
| 558 |
-
else:
|
| 559 |
-
if preferred_edges.shape != spans.mask.shape or preferred_edges.dtype is not torch.bool:
|
| 560 |
-
raise ValueError("preferred edges must be boolean and align with spans")
|
| 561 |
-
preferred = preferred_edges
|
| 562 |
-
if override_ids is None:
|
| 563 |
-
overrides = torch.full(
|
| 564 |
-
(atom_ids.shape[0], boundaries, boundaries),
|
| 565 |
-
-1,
|
| 566 |
-
dtype=torch.long,
|
| 567 |
-
device=atom_ids.device,
|
| 568 |
-
)
|
| 569 |
-
scores = spans.scores
|
| 570 |
-
else:
|
| 571 |
-
if override_ids.shape != spans.mask.shape or override_ids.dtype not in (
|
| 572 |
-
torch.int32,
|
| 573 |
-
torch.int64,
|
| 574 |
-
):
|
| 575 |
-
raise ValueError("override ids must be integer and align with spans")
|
| 576 |
-
if override_ids.device != atom_ids.device:
|
| 577 |
-
raise ValueError("override ids must share the atom device")
|
| 578 |
-
if bool(((override_ids >= 0) & ~spans.mask).any()):
|
| 579 |
-
raise ValueError("override ids may name only valid spans")
|
| 580 |
-
overrides = override_ids
|
| 581 |
-
positions = torch.arange(boundaries, device=atom_ids.device)
|
| 582 |
-
span_lengths = positions.unsqueeze(0) - positions.unsqueeze(1)
|
| 583 |
-
existing_multi = (overrides >= 0) & (span_lengths.unsqueeze(0) > 1)
|
| 584 |
-
scores = spans.scores - existing_multi.float() * float(self.span_generator.initial_non_atomic_score)
|
| 585 |
-
scores = (
|
| 586 |
-
scores
|
| 587 |
-
+ existing_multi.float() * span_lengths.unsqueeze(0).sub(1).float() * self.existing_token_length_bonus
|
| 588 |
-
)
|
| 589 |
-
if self.boundary_misalignment_penalty > 0:
|
| 590 |
-
aligned = preferred_boundary_spans(preferred, spans.mask)
|
| 591 |
-
scores = scores - (~aligned & spans.mask).float() * self.boundary_misalignment_penalty
|
| 592 |
-
lattice_scores = scores / self.temperature.float()
|
| 593 |
-
lattice = solve_lattice(
|
| 594 |
-
lattice_scores,
|
| 595 |
-
spans.mask,
|
| 596 |
-
lengths,
|
| 597 |
-
preferred_edges=preferred,
|
| 598 |
-
)
|
| 599 |
-
path_nll = preferred_path_nll(
|
| 600 |
-
lattice,
|
| 601 |
-
lattice_scores,
|
| 602 |
-
spans.mask,
|
| 603 |
-
lengths,
|
| 604 |
-
preferred,
|
| 605 |
-
)
|
| 606 |
-
candidate_embedding = (
|
| 607 |
-
self.atom_embedding if self.candidate_atom_embedding is None else self.candidate_atom_embedding
|
| 608 |
-
)
|
| 609 |
-
atoms = candidate_embedding(atom_ids.to(torch.long))
|
| 610 |
-
if self.detach_base_for_openlex:
|
| 611 |
-
atoms = atoms.detach()
|
| 612 |
-
residual_codes = spans.residual_codes
|
| 613 |
-
if residual_codes is not None and self.disable_candidate_residuals:
|
| 614 |
-
residual_codes = residual_codes * 0.0
|
| 615 |
-
if self.dictionary_composition and override_ids is None:
|
| 616 |
-
raise ValueError("dictionary composition requires canonical override ids")
|
| 617 |
-
novel = spans.mask & (overrides < 0)
|
| 618 |
-
if candidate_support is None:
|
| 619 |
-
supported_novel = novel
|
| 620 |
-
else:
|
| 621 |
-
if (
|
| 622 |
-
candidate_support.shape != spans.mask.shape
|
| 623 |
-
or candidate_support.dtype is not torch.bool
|
| 624 |
-
or candidate_support.device != spans.mask.device
|
| 625 |
-
):
|
| 626 |
-
raise ValueError(
|
| 627 |
-
"candidate support must be boolean and align with the span lattice"
|
| 628 |
-
)
|
| 629 |
-
if bool((candidate_support & ~novel).any()):
|
| 630 |
-
raise ValueError("candidate support may enable only legal novel spans")
|
| 631 |
-
supported_novel = candidate_support
|
| 632 |
-
dictionary_values = None
|
| 633 |
-
needs_dense_dictionary_values = (
|
| 634 |
-
self.dictionary_composition and include_teacher_distillation
|
| 635 |
-
)
|
| 636 |
-
if needs_dense_dictionary_values:
|
| 637 |
-
if residual_codes is None:
|
| 638 |
-
raise RuntimeError("dense dictionary values require residual codes")
|
| 639 |
-
base_span_values = dictionary_span_values(
|
| 640 |
-
self.atom_embedding,
|
| 641 |
-
overrides,
|
| 642 |
-
spans.mask,
|
| 643 |
-
detach_embedding=self.detach_base_for_openlex,
|
| 644 |
-
)
|
| 645 |
-
residual_values = residual_codes.float() @ self.residual_projection.float().T
|
| 646 |
-
dictionary_values = (
|
| 647 |
-
base_span_values + residual_values * supported_novel.unsqueeze(3)
|
| 648 |
-
)
|
| 649 |
-
teacher_distortion = (
|
| 650 |
-
teacher_span_distortion(
|
| 651 |
-
atoms,
|
| 652 |
-
self.atom_embedding,
|
| 653 |
-
residual_codes,
|
| 654 |
-
self.residual_projection,
|
| 655 |
-
overrides,
|
| 656 |
-
preferred,
|
| 657 |
-
spans.mask,
|
| 658 |
-
lattice.marginals,
|
| 659 |
-
teacher_embedding_weight=(
|
| 660 |
-
self.teacher_embedding_weight if self.teacher_embedding_weight.numel() else None
|
| 661 |
-
),
|
| 662 |
-
candidate_span_values=dictionary_values,
|
| 663 |
-
detach_embedding=self.detach_base_for_openlex,
|
| 664 |
-
detach_marginals=detach_teacher_routing,
|
| 665 |
-
require_registered_preferred=override_ids is not None,
|
| 666 |
-
)
|
| 667 |
-
if include_teacher_distillation
|
| 668 |
-
else None
|
| 669 |
-
)
|
| 670 |
-
if self.dictionary_composition:
|
| 671 |
-
if dictionary_values is None:
|
| 672 |
-
soft_numerators = dictionary_expected_numerator(
|
| 673 |
-
self.atom_embedding,
|
| 674 |
-
overrides,
|
| 675 |
-
spans.mask,
|
| 676 |
-
lattice.marginals,
|
| 677 |
-
detach_embedding=self.detach_base_for_openlex,
|
| 678 |
-
)
|
| 679 |
-
map_numerators = dictionary_ordered_map_numerator(
|
| 680 |
-
self.atom_embedding,
|
| 681 |
-
overrides,
|
| 682 |
-
spans.mask,
|
| 683 |
-
lattice.map_edges,
|
| 684 |
-
detach_embedding=self.detach_base_for_openlex,
|
| 685 |
-
)
|
| 686 |
-
if residual_codes is not None and not self.disable_candidate_residuals:
|
| 687 |
-
supported_codes = (
|
| 688 |
-
residual_codes.float() * supported_novel.unsqueeze(3)
|
| 689 |
-
)
|
| 690 |
-
soft_residual = expected_span_sum(
|
| 691 |
-
supported_codes,
|
| 692 |
-
lattice.marginals,
|
| 693 |
-
) @ self.residual_projection.float().T
|
| 694 |
-
map_residual = ordered_map_span_sum(
|
| 695 |
-
supported_codes,
|
| 696 |
-
lattice.map_edges,
|
| 697 |
-
) @ self.residual_projection.float().T
|
| 698 |
-
soft_numerators = soft_numerators + soft_residual
|
| 699 |
-
map_numerators = map_numerators + map_residual
|
| 700 |
-
else:
|
| 701 |
-
soft_numerators = expected_span_sum(dictionary_values, lattice.marginals)
|
| 702 |
-
map_numerators = ordered_map_span_sum(dictionary_values, lattice.map_edges)
|
| 703 |
-
else:
|
| 704 |
-
if residual_codes is None:
|
| 705 |
-
raise RuntimeError("factored residual pooling requires residual codes")
|
| 706 |
-
soft_numerators = factored_expected_sum(
|
| 707 |
-
atoms,
|
| 708 |
-
residual_codes,
|
| 709 |
-
self.residual_projection,
|
| 710 |
-
lattice.marginals,
|
| 711 |
-
lengths,
|
| 712 |
-
)
|
| 713 |
-
soft_numerators = soft_numerators + _override_correction(
|
| 714 |
-
atoms,
|
| 715 |
-
self.atom_embedding,
|
| 716 |
-
residual_codes,
|
| 717 |
-
self.residual_projection,
|
| 718 |
-
overrides,
|
| 719 |
-
lattice.marginals,
|
| 720 |
-
detach_embedding=self.detach_base_for_openlex,
|
| 721 |
-
)
|
| 722 |
-
if override_ids is None:
|
| 723 |
-
map_numerators = factored_map_sum(
|
| 724 |
-
atoms,
|
| 725 |
-
residual_codes,
|
| 726 |
-
self.residual_projection,
|
| 727 |
-
lattice.map_edges,
|
| 728 |
-
lengths,
|
| 729 |
-
)
|
| 730 |
-
else:
|
| 731 |
-
map_numerators = _map_sum_with_overrides(
|
| 732 |
-
atoms,
|
| 733 |
-
self.atom_embedding,
|
| 734 |
-
residual_codes,
|
| 735 |
-
self.residual_projection,
|
| 736 |
-
lattice.map_edges,
|
| 737 |
-
overrides,
|
| 738 |
-
detach_embedding=self.detach_base_for_openlex,
|
| 739 |
-
)
|
| 740 |
-
relaxed_embeddings = F.normalize(soft_numerators, dim=1)
|
| 741 |
-
map_embeddings = F.normalize(map_numerators, dim=1)
|
| 742 |
-
embeddings = (
|
| 743 |
-
relaxed_embeddings + (map_embeddings - relaxed_embeddings).detach()
|
| 744 |
-
if self.straight_through_map
|
| 745 |
-
else relaxed_embeddings
|
| 746 |
-
)
|
| 747 |
-
if not bool(torch.isfinite(embeddings).all()) or not bool(torch.isfinite(map_embeddings).all()):
|
| 748 |
-
raise FloatingPointError("nonfinite OpenLex sentence embedding")
|
| 749 |
-
map_keys = _selected_keys(atom_ids, lattice.map_edges) if include_keys else None
|
| 750 |
-
self._audit_map(atom_ids, lengths, lattice.map_edges)
|
| 751 |
-
return OpenLexOutput(
|
| 752 |
-
embeddings,
|
| 753 |
-
relaxed_embeddings,
|
| 754 |
-
map_embeddings,
|
| 755 |
-
soft_numerators,
|
| 756 |
-
map_numerators,
|
| 757 |
-
lattice,
|
| 758 |
-
residual_codes,
|
| 759 |
-
path_nll,
|
| 760 |
-
teacher_distortion,
|
| 761 |
-
map_keys,
|
| 762 |
-
)
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
def static_sentence_embeddings(
|
| 766 |
-
model: OpenLexEncoder,
|
| 767 |
-
token_ids: torch.Tensor,
|
| 768 |
-
lengths: torch.Tensor,
|
| 769 |
-
) -> torch.Tensor:
|
| 770 |
-
"""Ordinary table lookup, sum pooling and normalization for the base path."""
|
| 771 |
-
|
| 772 |
-
if token_ids.ndim != 2 or token_ids.dtype not in (torch.int32, torch.int64):
|
| 773 |
-
raise ValueError("static token ids must be a rank-2 integer tensor")
|
| 774 |
-
batch, token_count = token_ids.shape
|
| 775 |
-
if lengths.shape != (batch,) or lengths.dtype not in (torch.int32, torch.int64):
|
| 776 |
-
raise ValueError("static lengths must contain one integer per sentence")
|
| 777 |
-
if token_ids.device != lengths.device:
|
| 778 |
-
raise ValueError("static token ids and lengths must share a device")
|
| 779 |
-
if bool(((lengths < 1) | (lengths > token_count)).any()):
|
| 780 |
-
raise ValueError("every static sentence must be nonempty")
|
| 781 |
-
if bool(((token_ids < 0) | (token_ids >= model.atom_vocab_size)).any()):
|
| 782 |
-
raise ValueError("static token id lies outside the base table")
|
| 783 |
-
positions = torch.arange(token_count, device=token_ids.device)
|
| 784 |
-
mask = positions.unsqueeze(0) < lengths.unsqueeze(1)
|
| 785 |
-
values = model.atom_embedding(token_ids.long()).float() * mask.unsqueeze(-1)
|
| 786 |
-
embeddings = F.normalize(values.sum(dim=1), dim=1)
|
| 787 |
-
if not bool(torch.isfinite(embeddings).all()):
|
| 788 |
-
raise FloatingPointError("nonfinite static sentence embedding")
|
| 789 |
-
return embeddings
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
def _symmetric_info_nce(anchors: torch.Tensor, positives: torch.Tensor, scale: float) -> torch.Tensor:
|
| 793 |
-
if anchors.ndim != 2 or positives.shape != anchors.shape or not anchors.shape[0]:
|
| 794 |
-
raise ValueError("OpenLex InfoNCE requires aligned nonempty embedding batches")
|
| 795 |
-
if not math.isfinite(scale) or scale <= 0:
|
| 796 |
-
raise ValueError("OpenLex InfoNCE scale must be finite and positive")
|
| 797 |
-
logits = anchors @ positives.T * float(scale)
|
| 798 |
-
targets = torch.arange(logits.shape[0], device=logits.device)
|
| 799 |
-
return 0.5 * (F.cross_entropy(logits, targets) + F.cross_entropy(logits.T, targets))
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
def _gradient_norm(parameters: tuple[nn.Parameter, ...]) -> float:
|
| 803 |
-
squares = [parameter.grad.detach().float().square().sum() for parameter in parameters if parameter.grad is not None]
|
| 804 |
-
if not squares:
|
| 805 |
-
return 0.0
|
| 806 |
-
return float(torch.stack(squares).sum().sqrt().item())
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
def openlex_contrastive_step(
|
| 810 |
-
model: OpenLexEncoder,
|
| 811 |
-
optimizer: torch.optim.Optimizer,
|
| 812 |
-
anchor_ids: torch.Tensor,
|
| 813 |
-
anchor_lengths: torch.Tensor,
|
| 814 |
-
positive_ids: torch.Tensor,
|
| 815 |
-
positive_lengths: torch.Tensor,
|
| 816 |
-
*,
|
| 817 |
-
scale: float,
|
| 818 |
-
anchor_preferred_edges: torch.Tensor | None = None,
|
| 819 |
-
anchor_override_ids: torch.Tensor | None = None,
|
| 820 |
-
positive_preferred_edges: torch.Tensor | None = None,
|
| 821 |
-
positive_override_ids: torch.Tensor | None = None,
|
| 822 |
-
) -> OpenLexStepMetrics:
|
| 823 |
-
"""Run one complete contrastive update and expose vocabulary mutation."""
|
| 824 |
-
|
| 825 |
-
if anchor_ids.shape[0] != positive_ids.shape[0]:
|
| 826 |
-
raise ValueError("anchor and positive batches must contain the same number of texts")
|
| 827 |
-
model.train()
|
| 828 |
-
optimizer.zero_grad(set_to_none=True)
|
| 829 |
-
anchor_output = model(
|
| 830 |
-
anchor_ids,
|
| 831 |
-
anchor_lengths,
|
| 832 |
-
preferred_edges=anchor_preferred_edges,
|
| 833 |
-
override_ids=anchor_override_ids,
|
| 834 |
-
include_keys=True,
|
| 835 |
-
)
|
| 836 |
-
positive_output = model(
|
| 837 |
-
positive_ids,
|
| 838 |
-
positive_lengths,
|
| 839 |
-
preferred_edges=positive_preferred_edges,
|
| 840 |
-
override_ids=positive_override_ids,
|
| 841 |
-
include_keys=True,
|
| 842 |
-
)
|
| 843 |
-
if anchor_output.map_keys is None or positive_output.map_keys is None:
|
| 844 |
-
raise RuntimeError("OpenLex training step failed to materialize MAP keys")
|
| 845 |
-
keys_before = anchor_output.map_keys + positive_output.map_keys
|
| 846 |
-
loss = _symmetric_info_nce(anchor_output.embeddings, positive_output.embeddings, scale)
|
| 847 |
-
if not bool(torch.isfinite(loss)):
|
| 848 |
-
raise FloatingPointError("nonfinite OpenLex contrastive loss")
|
| 849 |
-
loss.backward()
|
| 850 |
-
for parameter in model.parameters():
|
| 851 |
-
if parameter.grad is not None and not bool(torch.isfinite(parameter.grad).all()):
|
| 852 |
-
raise FloatingPointError("nonfinite OpenLex parameter gradient")
|
| 853 |
-
residual_gradient_norm = _gradient_norm(
|
| 854 |
-
(
|
| 855 |
-
model.span_generator.residual_head.weight,
|
| 856 |
-
model.span_generator.residual_head.bias,
|
| 857 |
-
)
|
| 858 |
-
)
|
| 859 |
-
score_gradient_norm = _gradient_norm(
|
| 860 |
-
(
|
| 861 |
-
model.span_generator.score_head.weight,
|
| 862 |
-
model.span_generator.score_head.bias,
|
| 863 |
-
model.raw_temperature,
|
| 864 |
-
)
|
| 865 |
-
)
|
| 866 |
-
expected_token_count = float(
|
| 867 |
-
torch.cat(
|
| 868 |
-
(
|
| 869 |
-
anchor_output.lattice.marginals.flatten(1).sum(1),
|
| 870 |
-
positive_output.lattice.marginals.flatten(1).sum(1),
|
| 871 |
-
)
|
| 872 |
-
)
|
| 873 |
-
.mean()
|
| 874 |
-
.item()
|
| 875 |
-
)
|
| 876 |
-
map_token_count = float(
|
| 877 |
-
torch.cat(
|
| 878 |
-
(
|
| 879 |
-
anchor_output.lattice.map_edges.flatten(1).sum(1),
|
| 880 |
-
positive_output.lattice.map_edges.flatten(1).sum(1),
|
| 881 |
-
)
|
| 882 |
-
)
|
| 883 |
-
.float()
|
| 884 |
-
.mean()
|
| 885 |
-
.item()
|
| 886 |
-
)
|
| 887 |
-
optimizer.step()
|
| 888 |
-
with torch.no_grad():
|
| 889 |
-
anchor_after = model(
|
| 890 |
-
anchor_ids,
|
| 891 |
-
anchor_lengths,
|
| 892 |
-
preferred_edges=anchor_preferred_edges,
|
| 893 |
-
override_ids=anchor_override_ids,
|
| 894 |
-
include_keys=True,
|
| 895 |
-
)
|
| 896 |
-
positive_after = model(
|
| 897 |
-
positive_ids,
|
| 898 |
-
positive_lengths,
|
| 899 |
-
preferred_edges=positive_preferred_edges,
|
| 900 |
-
override_ids=positive_override_ids,
|
| 901 |
-
include_keys=True,
|
| 902 |
-
)
|
| 903 |
-
if anchor_after.map_keys is None or positive_after.map_keys is None:
|
| 904 |
-
raise RuntimeError("OpenLex post-step forward failed to materialize MAP keys")
|
| 905 |
-
return OpenLexStepMetrics(
|
| 906 |
-
loss=float(loss.detach().item()),
|
| 907 |
-
residual_gradient_norm=residual_gradient_norm,
|
| 908 |
-
score_gradient_norm=score_gradient_norm,
|
| 909 |
-
expected_token_count=expected_token_count,
|
| 910 |
-
map_token_count=map_token_count,
|
| 911 |
-
map_keys_before=keys_before,
|
| 912 |
-
map_keys_after=anchor_after.map_keys + positive_after.map_keys,
|
| 913 |
-
)
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
def openlex_crossfit_objective(
|
| 917 |
-
model: OpenLexEncoder,
|
| 918 |
-
support_anchor_ids: torch.Tensor,
|
| 919 |
-
support_anchor_lengths: torch.Tensor,
|
| 920 |
-
support_positive_ids: torch.Tensor,
|
| 921 |
-
support_positive_lengths: torch.Tensor,
|
| 922 |
-
query_anchor_ids: torch.Tensor,
|
| 923 |
-
query_anchor_lengths: torch.Tensor,
|
| 924 |
-
query_positive_ids: torch.Tensor,
|
| 925 |
-
query_positive_lengths: torch.Tensor,
|
| 926 |
-
*,
|
| 927 |
-
scale: float,
|
| 928 |
-
inner_lr: float,
|
| 929 |
-
mdl_weight: float,
|
| 930 |
-
gap_weight: float,
|
| 931 |
-
original_path_weight: float = 0.0,
|
| 932 |
-
teacher_distillation_weight: float = 0.0,
|
| 933 |
-
compression_weight: float = 0.0,
|
| 934 |
-
support_anchor_preferred_edges: torch.Tensor | None = None,
|
| 935 |
-
support_anchor_override_ids: torch.Tensor | None = None,
|
| 936 |
-
support_positive_preferred_edges: torch.Tensor | None = None,
|
| 937 |
-
support_positive_override_ids: torch.Tensor | None = None,
|
| 938 |
-
query_anchor_preferred_edges: torch.Tensor | None = None,
|
| 939 |
-
query_anchor_override_ids: torch.Tensor | None = None,
|
| 940 |
-
query_positive_preferred_edges: torch.Tensor | None = None,
|
| 941 |
-
query_positive_override_ids: torch.Tensor | None = None,
|
| 942 |
-
) -> OpenLexCrossfitObjective:
|
| 943 |
-
"""Differentiate query quality through one support residual update."""
|
| 944 |
-
|
| 945 |
-
if not math.isfinite(inner_lr) or inner_lr <= 0:
|
| 946 |
-
raise ValueError("OpenLex cross-fit inner learning rate must be positive")
|
| 947 |
-
if any(
|
| 948 |
-
not math.isfinite(value) or value < 0
|
| 949 |
-
for value in (
|
| 950 |
-
mdl_weight,
|
| 951 |
-
gap_weight,
|
| 952 |
-
original_path_weight,
|
| 953 |
-
teacher_distillation_weight,
|
| 954 |
-
compression_weight,
|
| 955 |
-
)
|
| 956 |
-
):
|
| 957 |
-
raise ValueError("OpenLex auxiliary weights must be finite and nonnegative")
|
| 958 |
-
support_anchor = model(
|
| 959 |
-
support_anchor_ids,
|
| 960 |
-
support_anchor_lengths,
|
| 961 |
-
preferred_edges=support_anchor_preferred_edges,
|
| 962 |
-
override_ids=support_anchor_override_ids,
|
| 963 |
-
include_teacher_distillation=teacher_distillation_weight > 0,
|
| 964 |
-
)
|
| 965 |
-
support_positive = model(
|
| 966 |
-
support_positive_ids,
|
| 967 |
-
support_positive_lengths,
|
| 968 |
-
preferred_edges=support_positive_preferred_edges,
|
| 969 |
-
override_ids=support_positive_override_ids,
|
| 970 |
-
include_teacher_distillation=teacher_distillation_weight > 0,
|
| 971 |
-
)
|
| 972 |
-
support_loss = _symmetric_info_nce(support_anchor.embeddings, support_positive.embeddings, scale)
|
| 973 |
-
named_parameters = dict(model.named_parameters())
|
| 974 |
-
inner_names = (
|
| 975 |
-
"span_generator.residual_head.weight",
|
| 976 |
-
"span_generator.residual_head.bias",
|
| 977 |
-
"residual_projection",
|
| 978 |
-
)
|
| 979 |
-
inner_parameters = tuple(named_parameters[name] for name in inner_names)
|
| 980 |
-
inner_gradients = torch.autograd.grad(
|
| 981 |
-
support_loss,
|
| 982 |
-
inner_parameters,
|
| 983 |
-
create_graph=True,
|
| 984 |
-
allow_unused=True,
|
| 985 |
-
)
|
| 986 |
-
updated: dict[str, torch.Tensor] = {}
|
| 987 |
-
for name, parameter, gradient in zip(inner_names, inner_parameters, inner_gradients, strict=True):
|
| 988 |
-
updated[name] = parameter if gradient is None else parameter - inner_lr * gradient
|
| 989 |
-
|
| 990 |
-
query_anchor = torch.func.functional_call(
|
| 991 |
-
model,
|
| 992 |
-
updated,
|
| 993 |
-
(query_anchor_ids, query_anchor_lengths),
|
| 994 |
-
{
|
| 995 |
-
"preferred_edges": query_anchor_preferred_edges,
|
| 996 |
-
"override_ids": query_anchor_override_ids,
|
| 997 |
-
"include_teacher_distillation": teacher_distillation_weight > 0,
|
| 998 |
-
},
|
| 999 |
-
strict=False,
|
| 1000 |
-
)
|
| 1001 |
-
query_positive = torch.func.functional_call(
|
| 1002 |
-
model,
|
| 1003 |
-
updated,
|
| 1004 |
-
(query_positive_ids, query_positive_lengths),
|
| 1005 |
-
{
|
| 1006 |
-
"preferred_edges": query_positive_preferred_edges,
|
| 1007 |
-
"override_ids": query_positive_override_ids,
|
| 1008 |
-
"include_teacher_distillation": teacher_distillation_weight > 0,
|
| 1009 |
-
},
|
| 1010 |
-
strict=False,
|
| 1011 |
-
)
|
| 1012 |
-
query_loss = _symmetric_info_nce(query_anchor.embeddings, query_positive.embeddings, scale)
|
| 1013 |
-
anchor_mdl = expected_occurrence_description_length(
|
| 1014 |
-
query_anchor.lattice.marginals,
|
| 1015 |
-
atom_vocab_size=model.atom_vocab_size,
|
| 1016 |
-
residual_rank=model.residual_rank,
|
| 1017 |
-
)
|
| 1018 |
-
positive_mdl = expected_occurrence_description_length(
|
| 1019 |
-
query_positive.lattice.marginals,
|
| 1020 |
-
atom_vocab_size=model.atom_vocab_size,
|
| 1021 |
-
residual_rank=model.residual_rank,
|
| 1022 |
-
)
|
| 1023 |
-
mdl = 0.5 * (anchor_mdl + positive_mdl)
|
| 1024 |
-
gap = torch.cat(
|
| 1025 |
-
(
|
| 1026 |
-
1.0 - (query_anchor.relaxed_embeddings * query_anchor.map_embeddings.detach()).sum(dim=1),
|
| 1027 |
-
1.0 - (query_positive.relaxed_embeddings * query_positive.map_embeddings.detach()).sum(dim=1),
|
| 1028 |
-
)
|
| 1029 |
-
).mean()
|
| 1030 |
-
original_path_nll = torch.cat(
|
| 1031 |
-
(
|
| 1032 |
-
support_anchor.preferred_path_nll,
|
| 1033 |
-
support_positive.preferred_path_nll,
|
| 1034 |
-
query_anchor.preferred_path_nll,
|
| 1035 |
-
query_positive.preferred_path_nll,
|
| 1036 |
-
)
|
| 1037 |
-
).mean()
|
| 1038 |
-
if teacher_distillation_weight > 0:
|
| 1039 |
-
teacher_terms = (
|
| 1040 |
-
support_anchor.teacher_span_distortion,
|
| 1041 |
-
support_positive.teacher_span_distortion,
|
| 1042 |
-
query_anchor.teacher_span_distortion,
|
| 1043 |
-
query_positive.teacher_span_distortion,
|
| 1044 |
-
)
|
| 1045 |
-
if any(value is None for value in teacher_terms):
|
| 1046 |
-
raise RuntimeError("requested teacher span distortion was not materialized")
|
| 1047 |
-
teacher_distillation = torch.stack(
|
| 1048 |
-
tuple(value for value in teacher_terms if value is not None)
|
| 1049 |
-
).mean()
|
| 1050 |
-
else:
|
| 1051 |
-
teacher_distillation = support_loss.new_zeros(())
|
| 1052 |
-
expected_token_count = torch.cat(
|
| 1053 |
-
tuple(
|
| 1054 |
-
output.lattice.marginals.sum(dim=(1, 2))
|
| 1055 |
-
for output in (
|
| 1056 |
-
support_anchor,
|
| 1057 |
-
support_positive,
|
| 1058 |
-
query_anchor,
|
| 1059 |
-
query_positive,
|
| 1060 |
-
)
|
| 1061 |
-
)
|
| 1062 |
-
).mean()
|
| 1063 |
-
total = (
|
| 1064 |
-
support_loss
|
| 1065 |
-
+ query_loss
|
| 1066 |
-
+ mdl_weight * mdl
|
| 1067 |
-
+ gap_weight * gap
|
| 1068 |
-
+ original_path_weight * original_path_nll
|
| 1069 |
-
+ teacher_distillation_weight * teacher_distillation
|
| 1070 |
-
+ compression_weight * expected_token_count
|
| 1071 |
-
)
|
| 1072 |
-
if not bool(
|
| 1073 |
-
torch.isfinite(
|
| 1074 |
-
torch.stack(
|
| 1075 |
-
(
|
| 1076 |
-
total,
|
| 1077 |
-
support_loss,
|
| 1078 |
-
query_loss,
|
| 1079 |
-
mdl,
|
| 1080 |
-
gap,
|
| 1081 |
-
original_path_nll,
|
| 1082 |
-
teacher_distillation,
|
| 1083 |
-
expected_token_count,
|
| 1084 |
-
)
|
| 1085 |
-
)
|
| 1086 |
-
).all()
|
| 1087 |
-
):
|
| 1088 |
-
raise FloatingPointError("nonfinite OpenLex cross-fit objective")
|
| 1089 |
-
return OpenLexCrossfitObjective(
|
| 1090 |
-
total,
|
| 1091 |
-
support_loss,
|
| 1092 |
-
query_loss,
|
| 1093 |
-
mdl,
|
| 1094 |
-
gap,
|
| 1095 |
-
original_path_nll,
|
| 1096 |
-
teacher_distillation,
|
| 1097 |
-
expected_token_count,
|
| 1098 |
-
)
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
def cached_openlex_backward(
|
| 1102 |
-
model: OpenLexEncoder,
|
| 1103 |
-
anchor_ids: torch.Tensor,
|
| 1104 |
-
anchor_lengths: torch.Tensor,
|
| 1105 |
-
positive_ids: torch.Tensor,
|
| 1106 |
-
positive_lengths: torch.Tensor,
|
| 1107 |
-
*,
|
| 1108 |
-
scale: float,
|
| 1109 |
-
mini_batch_size: int,
|
| 1110 |
-
anchor_preferred_edges: torch.Tensor | None = None,
|
| 1111 |
-
anchor_override_ids: torch.Tensor | None = None,
|
| 1112 |
-
positive_preferred_edges: torch.Tensor | None = None,
|
| 1113 |
-
positive_override_ids: torch.Tensor | None = None,
|
| 1114 |
-
) -> float:
|
| 1115 |
-
"""Backpropagate full-batch InfoNCE through deterministic replay chunks."""
|
| 1116 |
-
|
| 1117 |
-
if anchor_ids.shape[0] != positive_ids.shape[0] or not anchor_ids.shape[0]:
|
| 1118 |
-
raise ValueError("cached OpenLex needs aligned nonempty pair batches")
|
| 1119 |
-
if mini_batch_size <= 0:
|
| 1120 |
-
raise ValueError("cached OpenLex mini_batch_size must be positive")
|
| 1121 |
-
batch = anchor_ids.shape[0]
|
| 1122 |
-
slices = [slice(start, min(start + mini_batch_size, batch)) for start in range(0, batch, mini_batch_size)]
|
| 1123 |
-
|
| 1124 |
-
def encode(
|
| 1125 |
-
ids: torch.Tensor,
|
| 1126 |
-
sequence_lengths: torch.Tensor,
|
| 1127 |
-
item: slice,
|
| 1128 |
-
preferred_edges: torch.Tensor | None,
|
| 1129 |
-
override_ids: torch.Tensor | None,
|
| 1130 |
-
) -> torch.Tensor:
|
| 1131 |
-
return model(
|
| 1132 |
-
ids[item],
|
| 1133 |
-
sequence_lengths[item],
|
| 1134 |
-
preferred_edges=(None if preferred_edges is None else preferred_edges[item]),
|
| 1135 |
-
override_ids=None if override_ids is None else override_ids[item],
|
| 1136 |
-
).embeddings
|
| 1137 |
-
|
| 1138 |
-
model.start_cache_audit()
|
| 1139 |
-
try:
|
| 1140 |
-
model.begin_cache_record()
|
| 1141 |
-
with torch.no_grad():
|
| 1142 |
-
anchor_cache = torch.cat(
|
| 1143 |
-
[
|
| 1144 |
-
encode(
|
| 1145 |
-
anchor_ids,
|
| 1146 |
-
anchor_lengths,
|
| 1147 |
-
item,
|
| 1148 |
-
anchor_preferred_edges,
|
| 1149 |
-
anchor_override_ids,
|
| 1150 |
-
)
|
| 1151 |
-
for item in slices
|
| 1152 |
-
]
|
| 1153 |
-
)
|
| 1154 |
-
positive_cache = torch.cat(
|
| 1155 |
-
[
|
| 1156 |
-
encode(
|
| 1157 |
-
positive_ids,
|
| 1158 |
-
positive_lengths,
|
| 1159 |
-
item,
|
| 1160 |
-
positive_preferred_edges,
|
| 1161 |
-
positive_override_ids,
|
| 1162 |
-
)
|
| 1163 |
-
for item in slices
|
| 1164 |
-
]
|
| 1165 |
-
)
|
| 1166 |
-
model.end_cache_phase()
|
| 1167 |
-
anchor_leaf = anchor_cache.detach().requires_grad_()
|
| 1168 |
-
positive_leaf = positive_cache.detach().requires_grad_()
|
| 1169 |
-
loss = _symmetric_info_nce(anchor_leaf, positive_leaf, scale)
|
| 1170 |
-
anchor_gradient, positive_gradient = torch.autograd.grad(loss, (anchor_leaf, positive_leaf))
|
| 1171 |
-
|
| 1172 |
-
model.begin_cache_replay()
|
| 1173 |
-
for item in slices:
|
| 1174 |
-
replay = encode(
|
| 1175 |
-
anchor_ids,
|
| 1176 |
-
anchor_lengths,
|
| 1177 |
-
item,
|
| 1178 |
-
anchor_preferred_edges,
|
| 1179 |
-
anchor_override_ids,
|
| 1180 |
-
)
|
| 1181 |
-
(replay * anchor_gradient[item]).sum().backward()
|
| 1182 |
-
for item in slices:
|
| 1183 |
-
replay = encode(
|
| 1184 |
-
positive_ids,
|
| 1185 |
-
positive_lengths,
|
| 1186 |
-
item,
|
| 1187 |
-
positive_preferred_edges,
|
| 1188 |
-
positive_override_ids,
|
| 1189 |
-
)
|
| 1190 |
-
(replay * positive_gradient[item]).sum().backward()
|
| 1191 |
-
model.end_cache_phase()
|
| 1192 |
-
model.assert_cache_audit_empty()
|
| 1193 |
-
except Exception:
|
| 1194 |
-
model.abort_cache_audit()
|
| 1195 |
-
raise
|
| 1196 |
-
return float(loss.detach().item())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_replay.py
DELETED
|
@@ -1,604 +0,0 @@
|
|
| 1 |
-
"""Control-variate OpenLex replay against a complete static embedding cache."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
import math
|
| 7 |
-
import os
|
| 8 |
-
from typing import Any
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
|
| 12 |
-
from starse.cached_loss import StaticCompatibleCachedMultipleNegativesRankingLoss
|
| 13 |
-
from starse.openlex_model import OpenLexEncoder
|
| 14 |
-
from starse.openlex_reuse import (
|
| 15 |
-
canonical_atom_rows,
|
| 16 |
-
cross_fitted_supported_novel_span_mask,
|
| 17 |
-
exact_candidate_document_frequency,
|
| 18 |
-
exact_document_frequency,
|
| 19 |
-
map_novel_reuse_counts,
|
| 20 |
-
reuse_description_edge_costs,
|
| 21 |
-
)
|
| 22 |
-
from starse.openlex_tokenizer import CanonicalVocabulary
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
@dataclass(frozen=True)
|
| 26 |
-
class OpenLexReplaySettings:
|
| 27 |
-
sample_pairs: int
|
| 28 |
-
sample_seed: int
|
| 29 |
-
compression_weight: float = 1.0
|
| 30 |
-
squared_boundary_budget: int = 1_000_000
|
| 31 |
-
structural_objective: str = "expected_tokens"
|
| 32 |
-
recurrent_residuals: bool = False
|
| 33 |
-
reuse_support_scope: str = "sample"
|
| 34 |
-
|
| 35 |
-
def __post_init__(self) -> None:
|
| 36 |
-
if isinstance(self.sample_pairs, bool) or self.sample_pairs <= 0:
|
| 37 |
-
raise ValueError("OpenLex replay sample_pairs must be positive")
|
| 38 |
-
if isinstance(self.sample_seed, bool) or not isinstance(self.sample_seed, int):
|
| 39 |
-
raise TypeError("OpenLex replay sample_seed must be an integer")
|
| 40 |
-
if not math.isfinite(self.compression_weight) or self.compression_weight < 0:
|
| 41 |
-
raise ValueError("OpenLex replay compression weight must be finite and nonnegative")
|
| 42 |
-
if isinstance(self.squared_boundary_budget, bool) or self.squared_boundary_budget <= 0:
|
| 43 |
-
raise ValueError("OpenLex replay squared-boundary budget must be positive")
|
| 44 |
-
if self.structural_objective not in {"expected_tokens", "reuse_mdl"}:
|
| 45 |
-
raise ValueError(
|
| 46 |
-
"OpenLex replay structural objective must be expected_tokens or reuse_mdl"
|
| 47 |
-
)
|
| 48 |
-
if not isinstance(self.recurrent_residuals, bool):
|
| 49 |
-
raise TypeError("OpenLex recurrent_residuals must be boolean")
|
| 50 |
-
if self.reuse_support_scope not in {"sample", "batch"}:
|
| 51 |
-
raise ValueError("OpenLex reuse support scope must be sample or batch")
|
| 52 |
-
if self.recurrent_residuals and (
|
| 53 |
-
self.structural_objective != "reuse_mdl" or self.sample_pairs < 2
|
| 54 |
-
):
|
| 55 |
-
raise ValueError(
|
| 56 |
-
"OpenLex recurrent residuals require reuse_mdl and at least two pairs"
|
| 57 |
-
)
|
| 58 |
-
if self.reuse_support_scope == "batch" and not self.recurrent_residuals:
|
| 59 |
-
raise ValueError("OpenLex full-batch reuse support requires recurrent residuals")
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
class UniformPairReplaySampler:
|
| 63 |
-
"""Uniform aligned pair samples with an exact checkpointable CPU RNG."""
|
| 64 |
-
|
| 65 |
-
def __init__(self, *, sample_pairs: int, seed: int) -> None:
|
| 66 |
-
if isinstance(sample_pairs, bool) or sample_pairs <= 0:
|
| 67 |
-
raise ValueError("OpenLex replay sample_pairs must be positive")
|
| 68 |
-
if isinstance(seed, bool) or not isinstance(seed, int):
|
| 69 |
-
raise TypeError("OpenLex replay seed must be an integer")
|
| 70 |
-
self.sample_pairs = int(sample_pairs)
|
| 71 |
-
self._generator = torch.Generator(device="cpu").manual_seed(seed)
|
| 72 |
-
self.completed_forwards = 0
|
| 73 |
-
|
| 74 |
-
def select(self, batch_size: int) -> torch.Tensor:
|
| 75 |
-
if isinstance(batch_size, bool) or batch_size <= 0:
|
| 76 |
-
raise ValueError("OpenLex replay batch size must be positive")
|
| 77 |
-
if self.sample_pairs > batch_size:
|
| 78 |
-
raise ValueError("OpenLex replay sample exceeds the pair batch")
|
| 79 |
-
selected = torch.randperm(batch_size, generator=self._generator)[: self.sample_pairs]
|
| 80 |
-
self.completed_forwards += 1
|
| 81 |
-
return selected
|
| 82 |
-
|
| 83 |
-
def state_payload(self) -> dict[str, object]:
|
| 84 |
-
return {
|
| 85 |
-
"sample_pairs": self.sample_pairs,
|
| 86 |
-
"completed_forwards": self.completed_forwards,
|
| 87 |
-
"generator_state": self._generator.get_state().clone(),
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
def load_state_payload(self, payload: dict[str, object]) -> None:
|
| 91 |
-
if set(payload) != {"sample_pairs", "completed_forwards", "generator_state"}:
|
| 92 |
-
raise ValueError("OpenLex replay sampler state has the wrong schema")
|
| 93 |
-
if payload["sample_pairs"] != self.sample_pairs:
|
| 94 |
-
raise ValueError("OpenLex replay sampler size changed across resume")
|
| 95 |
-
completed = payload["completed_forwards"]
|
| 96 |
-
state = payload["generator_state"]
|
| 97 |
-
if isinstance(completed, bool) or not isinstance(completed, int) or completed < 0:
|
| 98 |
-
raise ValueError("OpenLex replay completed-forward count is invalid")
|
| 99 |
-
if not isinstance(state, torch.Tensor) or state.device.type != "cpu" or state.dtype != torch.uint8:
|
| 100 |
-
raise ValueError("OpenLex replay generator state is invalid")
|
| 101 |
-
self._generator.set_state(state.clone())
|
| 102 |
-
self.completed_forwards = completed
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def _static_row_bounds(features: dict[str, Any]) -> tuple[torch.Tensor, torch.Tensor]:
|
| 106 |
-
input_ids = features.get("input_ids")
|
| 107 |
-
offsets = features.get("offsets")
|
| 108 |
-
if (
|
| 109 |
-
not isinstance(input_ids, torch.Tensor)
|
| 110 |
-
or input_ids.ndim != 1
|
| 111 |
-
or input_ids.dtype not in (torch.int32, torch.int64)
|
| 112 |
-
or not isinstance(offsets, torch.Tensor)
|
| 113 |
-
or offsets.ndim != 1
|
| 114 |
-
or offsets.dtype not in (torch.int32, torch.int64)
|
| 115 |
-
or not offsets.numel()
|
| 116 |
-
):
|
| 117 |
-
raise ValueError("OpenLex replay requires flattened integer input_ids and offsets")
|
| 118 |
-
if input_ids.device != offsets.device:
|
| 119 |
-
raise ValueError("OpenLex replay ids and offsets must share a device")
|
| 120 |
-
if int(offsets[0].item()) != 0 or bool((offsets[1:] <= offsets[:-1]).any()):
|
| 121 |
-
raise ValueError("OpenLex replay offsets must start at zero and increase")
|
| 122 |
-
if int(offsets[-1].item()) >= input_ids.numel():
|
| 123 |
-
raise ValueError("OpenLex replay final sentence must be nonempty")
|
| 124 |
-
ends = torch.cat((offsets[1:], offsets.new_tensor([input_ids.numel()])))
|
| 125 |
-
return offsets, ends
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
def select_static_features(
|
| 129 |
-
features: dict[str, Any], indices: torch.Tensor
|
| 130 |
-
) -> dict[str, Any]:
|
| 131 |
-
"""Gather arbitrary sentence rows from a flattened static feature batch."""
|
| 132 |
-
|
| 133 |
-
starts, ends = _static_row_bounds(features)
|
| 134 |
-
if indices.ndim != 1 or indices.dtype not in (torch.int32, torch.int64) or not indices.numel():
|
| 135 |
-
raise ValueError("OpenLex replay indices must be a nonempty integer vector")
|
| 136 |
-
local_indices = indices.to(device=starts.device, dtype=torch.long)
|
| 137 |
-
if bool(((local_indices < 0) | (local_indices >= starts.numel())).any()):
|
| 138 |
-
raise ValueError("OpenLex replay index lies outside the pair batch")
|
| 139 |
-
if local_indices.unique().numel() != local_indices.numel():
|
| 140 |
-
raise ValueError("OpenLex replay indices must be unique")
|
| 141 |
-
lengths = ends - starts
|
| 142 |
-
selected_lengths = lengths.index_select(0, local_indices)
|
| 143 |
-
selected_offsets = torch.cat(
|
| 144 |
-
(selected_lengths.new_zeros(1), selected_lengths.cumsum(0)[:-1])
|
| 145 |
-
)
|
| 146 |
-
token_parts = [
|
| 147 |
-
features["input_ids"][int(starts[index].item()) : int(ends[index].item())]
|
| 148 |
-
for index in local_indices.tolist()
|
| 149 |
-
]
|
| 150 |
-
result: dict[str, Any] = {}
|
| 151 |
-
for name, value in features.items():
|
| 152 |
-
if name == "input_ids":
|
| 153 |
-
result[name] = torch.cat(token_parts)
|
| 154 |
-
elif name == "offsets":
|
| 155 |
-
result[name] = selected_offsets
|
| 156 |
-
elif isinstance(value, torch.Tensor) and value.ndim and value.shape[0] == starts.numel():
|
| 157 |
-
result[name] = value.index_select(0, local_indices)
|
| 158 |
-
else:
|
| 159 |
-
result[name] = value
|
| 160 |
-
return result
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def flattened_to_padded(
|
| 164 |
-
features: dict[str, Any],
|
| 165 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 166 |
-
"""Convert a flattened static token stream into padded rows and lengths."""
|
| 167 |
-
|
| 168 |
-
starts, ends = _static_row_bounds(features)
|
| 169 |
-
lengths = (ends - starts).long()
|
| 170 |
-
maximum = int(lengths.max().item())
|
| 171 |
-
padded = torch.zeros(
|
| 172 |
-
starts.numel(),
|
| 173 |
-
maximum,
|
| 174 |
-
dtype=features["input_ids"].dtype,
|
| 175 |
-
device=features["input_ids"].device,
|
| 176 |
-
)
|
| 177 |
-
for row, (start, end) in enumerate(zip(starts.tolist(), ends.tolist(), strict=True)):
|
| 178 |
-
padded[row, : end - start] = features["input_ids"][start:end]
|
| 179 |
-
return padded, lengths
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
def replay_pair_chunks(
|
| 183 |
-
sentence_features: list[dict[str, Any]],
|
| 184 |
-
selected_indices: torch.Tensor,
|
| 185 |
-
*,
|
| 186 |
-
squared_boundary_budget: int,
|
| 187 |
-
) -> tuple[torch.Tensor, ...]:
|
| 188 |
-
"""Sort selected pairs by two-sided length and pack padded-square chunks."""
|
| 189 |
-
|
| 190 |
-
if len(sentence_features) != 2:
|
| 191 |
-
raise ValueError("OpenLex replay chunking requires exactly two sentence sides")
|
| 192 |
-
if (
|
| 193 |
-
selected_indices.ndim != 1
|
| 194 |
-
or selected_indices.dtype not in (torch.int32, torch.int64)
|
| 195 |
-
or not selected_indices.numel()
|
| 196 |
-
or selected_indices.unique().numel() != selected_indices.numel()
|
| 197 |
-
):
|
| 198 |
-
raise ValueError("OpenLex replay chunk indices must be unique and nonempty")
|
| 199 |
-
if isinstance(squared_boundary_budget, bool) or squared_boundary_budget <= 0:
|
| 200 |
-
raise ValueError("OpenLex replay squared-boundary budget must be positive")
|
| 201 |
-
|
| 202 |
-
side_lengths: list[torch.Tensor] = []
|
| 203 |
-
pair_count: int | None = None
|
| 204 |
-
for features in sentence_features:
|
| 205 |
-
starts, ends = _static_row_bounds(features)
|
| 206 |
-
if pair_count is None:
|
| 207 |
-
pair_count = int(starts.numel())
|
| 208 |
-
elif starts.numel() != pair_count:
|
| 209 |
-
raise ValueError("OpenLex replay sentence sides have different pair counts")
|
| 210 |
-
local_indices = selected_indices.to(device=starts.device, dtype=torch.long)
|
| 211 |
-
if bool(((local_indices < 0) | (local_indices >= starts.numel())).any()):
|
| 212 |
-
raise ValueError("OpenLex replay chunk index lies outside the pair batch")
|
| 213 |
-
side_lengths.append((ends - starts).index_select(0, local_indices).cpu())
|
| 214 |
-
maximum_lengths = torch.maximum(side_lengths[0], side_lengths[1])
|
| 215 |
-
order = sorted(
|
| 216 |
-
range(selected_indices.numel()),
|
| 217 |
-
key=lambda position: int(maximum_lengths[position].item()),
|
| 218 |
-
)
|
| 219 |
-
sorted_indices = selected_indices.index_select(
|
| 220 |
-
0,
|
| 221 |
-
torch.tensor(order, device=selected_indices.device, dtype=torch.long),
|
| 222 |
-
)
|
| 223 |
-
sorted_lengths = maximum_lengths.index_select(0, torch.tensor(order))
|
| 224 |
-
|
| 225 |
-
chunks: list[torch.Tensor] = []
|
| 226 |
-
start = 0
|
| 227 |
-
while start < sorted_indices.numel():
|
| 228 |
-
stop = start
|
| 229 |
-
maximum = 0
|
| 230 |
-
while stop < sorted_indices.numel():
|
| 231 |
-
maximum = max(maximum, int(sorted_lengths[stop].item()))
|
| 232 |
-
proposed_size = stop - start + 1
|
| 233 |
-
proposed_cost = proposed_size * (maximum + 1) ** 2
|
| 234 |
-
if proposed_cost > squared_boundary_budget:
|
| 235 |
-
if stop == start:
|
| 236 |
-
raise ValueError(
|
| 237 |
-
"one OpenLex replay pair exceeds the squared-boundary budget"
|
| 238 |
-
)
|
| 239 |
-
break
|
| 240 |
-
stop += 1
|
| 241 |
-
chunks.append(sorted_indices[start:stop])
|
| 242 |
-
start = stop
|
| 243 |
-
return tuple(chunks)
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
def scaled_cached_jvp(
|
| 247 |
-
embeddings: torch.Tensor,
|
| 248 |
-
cached_gradients: torch.Tensor,
|
| 249 |
-
*,
|
| 250 |
-
batch_size: int,
|
| 251 |
-
sample_size: int,
|
| 252 |
-
) -> torch.Tensor:
|
| 253 |
-
"""Return the uniform-sample estimate of a full-cache embedding JVP."""
|
| 254 |
-
|
| 255 |
-
if embeddings.ndim != 2 or cached_gradients.shape != embeddings.shape:
|
| 256 |
-
raise ValueError("OpenLex replay embeddings and cached gradients must align")
|
| 257 |
-
if not embeddings.is_floating_point() or not cached_gradients.is_floating_point():
|
| 258 |
-
raise TypeError("OpenLex replay JVP tensors must be floating point")
|
| 259 |
-
if isinstance(batch_size, bool) or isinstance(sample_size, bool) or not 0 < sample_size <= batch_size:
|
| 260 |
-
raise ValueError("OpenLex replay sample size must lie inside the pair batch")
|
| 261 |
-
return (
|
| 262 |
-
embeddings.float()
|
| 263 |
-
* cached_gradients.detach().to(device=embeddings.device, dtype=torch.float32)
|
| 264 |
-
).sum() * (float(batch_size) / float(sample_size))
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
class OpenLexControlVariateLoss(StaticCompatibleCachedMultipleNegativesRankingLoss):
|
| 268 |
-
"""Add sampled OpenLex policy gradients to the unchanged cached Q15 loss."""
|
| 269 |
-
|
| 270 |
-
def __init__(
|
| 271 |
-
self,
|
| 272 |
-
model: Any,
|
| 273 |
-
*,
|
| 274 |
-
openlex: OpenLexEncoder,
|
| 275 |
-
vocabulary: CanonicalVocabulary,
|
| 276 |
-
settings: OpenLexReplaySettings,
|
| 277 |
-
**kwargs: Any,
|
| 278 |
-
) -> None:
|
| 279 |
-
super().__init__(model=model, **kwargs)
|
| 280 |
-
if not openlex.detach_base_for_openlex:
|
| 281 |
-
raise ValueError("OpenLex control-variate replay must detach the base table")
|
| 282 |
-
if not openlex.dictionary_composition:
|
| 283 |
-
raise ValueError("OpenLex replay requires dictionary composition")
|
| 284 |
-
if openlex.disable_candidate_residuals == settings.recurrent_residuals:
|
| 285 |
-
raise ValueError(
|
| 286 |
-
"OpenLex residual mode and recurrent-residual setting disagree"
|
| 287 |
-
)
|
| 288 |
-
self.openlex = openlex
|
| 289 |
-
self.vocabulary = vocabulary
|
| 290 |
-
self.registered_types = frozenset(
|
| 291 |
-
tuple(int(atom) for atom in spelling)
|
| 292 |
-
for spelling in self.vocabulary.spellings
|
| 293 |
-
)
|
| 294 |
-
self.settings = settings
|
| 295 |
-
self.sampler = UniformPairReplaySampler(
|
| 296 |
-
sample_pairs=settings.sample_pairs,
|
| 297 |
-
seed=settings.sample_seed,
|
| 298 |
-
)
|
| 299 |
-
# Temperature is redundant with the unrestricted score scale and its
|
| 300 |
-
# lattice derivative is the numerically fragile replay direction.
|
| 301 |
-
# Keep the registered value of 1.0 fixed and learn relative span scores.
|
| 302 |
-
self.openlex.raw_temperature.requires_grad_(False)
|
| 303 |
-
# Full forward context remains unrestricted; only the backward chain is
|
| 304 |
-
# truncated so a rare long sample cannot overflow or retain O(L) GRU graphs.
|
| 305 |
-
self.openlex.span_generator.recurrent_gradient_horizon = 8
|
| 306 |
-
if self.openlex.disable_candidate_residuals:
|
| 307 |
-
for parameter in self.openlex.span_generator.residual_head.parameters():
|
| 308 |
-
parameter.requires_grad_(False)
|
| 309 |
-
self.openlex.residual_projection.requires_grad_(False)
|
| 310 |
-
self._latest_metrics: dict[str, float] = {}
|
| 311 |
-
self._anomaly_enabled = False
|
| 312 |
-
|
| 313 |
-
def _maybe_enable_anomaly_detection(self) -> None:
|
| 314 |
-
raw = os.environ.get("STARSE_OPENLEX_ANOMALY_AFTER")
|
| 315 |
-
if raw is None or self._anomaly_enabled:
|
| 316 |
-
return
|
| 317 |
-
try:
|
| 318 |
-
threshold = int(raw)
|
| 319 |
-
except ValueError as error:
|
| 320 |
-
raise ValueError("STARSE_OPENLEX_ANOMALY_AFTER must be a positive integer") from error
|
| 321 |
-
if threshold <= 0:
|
| 322 |
-
raise ValueError("STARSE_OPENLEX_ANOMALY_AFTER must be a positive integer")
|
| 323 |
-
if self.sampler.completed_forwards >= threshold:
|
| 324 |
-
torch.autograd.set_detect_anomaly(True, check_nan=True)
|
| 325 |
-
self._anomaly_enabled = True
|
| 326 |
-
print(
|
| 327 |
-
"OpenLex autograd anomaly detection enabled at forward "
|
| 328 |
-
f"{self.sampler.completed_forwards}",
|
| 329 |
-
flush=True,
|
| 330 |
-
)
|
| 331 |
-
|
| 332 |
-
def policy_parameters(self) -> tuple[torch.nn.Parameter, ...]:
|
| 333 |
-
return tuple(parameter for _, parameter in self.policy_named_parameters())
|
| 334 |
-
|
| 335 |
-
def policy_named_parameters(self) -> tuple[tuple[str, torch.nn.Parameter], ...]:
|
| 336 |
-
parameters: list[torch.nn.Parameter] = []
|
| 337 |
-
named_parameters: list[tuple[str, torch.nn.Parameter]] = []
|
| 338 |
-
for name, parameter in self.openlex.named_parameters():
|
| 339 |
-
if (
|
| 340 |
-
name == "atom_embedding.weight"
|
| 341 |
-
or (
|
| 342 |
-
self.openlex.disable_candidate_residuals
|
| 343 |
-
and (name == "residual_projection" or "residual_head" in name)
|
| 344 |
-
)
|
| 345 |
-
or not parameter.requires_grad
|
| 346 |
-
):
|
| 347 |
-
continue
|
| 348 |
-
parameters.append(parameter)
|
| 349 |
-
named_parameters.append((name, parameter))
|
| 350 |
-
if not parameters or len({id(parameter) for parameter in parameters}) != len(parameters):
|
| 351 |
-
raise RuntimeError("OpenLex replay policy parameter set is invalid")
|
| 352 |
-
return tuple(named_parameters)
|
| 353 |
-
|
| 354 |
-
def consume_metrics(self) -> dict[str, float]:
|
| 355 |
-
metrics = dict(self._latest_metrics)
|
| 356 |
-
self._latest_metrics.clear()
|
| 357 |
-
return metrics
|
| 358 |
-
|
| 359 |
-
def _cached_gradients(self) -> list[torch.Tensor]:
|
| 360 |
-
if len(self.cache) != 2:
|
| 361 |
-
raise RuntimeError("OpenLex replay requires exactly two cached sentence sides")
|
| 362 |
-
gradients: list[torch.Tensor] = []
|
| 363 |
-
for side in self.cache:
|
| 364 |
-
if not side or any(value is None for value in side):
|
| 365 |
-
raise RuntimeError("OpenLex replay cache is missing embedding gradients")
|
| 366 |
-
gradients.append(torch.cat(tuple(value for value in side if value is not None)))
|
| 367 |
-
return gradients
|
| 368 |
-
|
| 369 |
-
def forward(
|
| 370 |
-
self,
|
| 371 |
-
sentence_features: Any,
|
| 372 |
-
labels: torch.Tensor | None,
|
| 373 |
-
) -> torch.Tensor:
|
| 374 |
-
features = list(sentence_features)
|
| 375 |
-
base_loss = super().forward(features, labels)
|
| 376 |
-
if not torch.is_grad_enabled():
|
| 377 |
-
return base_loss
|
| 378 |
-
if len(features) != 2:
|
| 379 |
-
raise ValueError("OpenLex replay requires exactly two aligned sentence sides")
|
| 380 |
-
pair_count = int(features[0]["offsets"].numel())
|
| 381 |
-
if int(features[1]["offsets"].numel()) != pair_count:
|
| 382 |
-
raise ValueError("OpenLex replay sentence sides have different pair counts")
|
| 383 |
-
selected_cpu = self.sampler.select(pair_count)
|
| 384 |
-
self._maybe_enable_anomaly_detection()
|
| 385 |
-
selected_chunks = replay_pair_chunks(
|
| 386 |
-
features,
|
| 387 |
-
selected_cpu,
|
| 388 |
-
squared_boundary_budget=self.settings.squared_boundary_budget,
|
| 389 |
-
)
|
| 390 |
-
cached_gradients = self._cached_gradients()
|
| 391 |
-
reuse_supports: list[dict[tuple[int, ...], int]] = []
|
| 392 |
-
reuse_rows: list[tuple[tuple[int, ...], ...]] = []
|
| 393 |
-
crossfit_supports: list[
|
| 394 |
-
tuple[dict[tuple[int, ...], int], dict[tuple[int, ...], int]]
|
| 395 |
-
] = []
|
| 396 |
-
selected_folds = {
|
| 397 |
-
int(index): (
|
| 398 |
-
int(index) % 2
|
| 399 |
-
if self.settings.reuse_support_scope == "batch"
|
| 400 |
-
else position % 2
|
| 401 |
-
)
|
| 402 |
-
for position, index in enumerate(selected_cpu.tolist())
|
| 403 |
-
}
|
| 404 |
-
selected_positions = {
|
| 405 |
-
int(index): position
|
| 406 |
-
for position, index in enumerate(selected_cpu.tolist())
|
| 407 |
-
}
|
| 408 |
-
if self.settings.structural_objective == "reuse_mdl":
|
| 409 |
-
for side_features in features:
|
| 410 |
-
all_selected = select_static_features(side_features, selected_cpu)
|
| 411 |
-
all_token_ids, all_token_lengths = flattened_to_padded(all_selected)
|
| 412 |
-
rows = canonical_atom_rows(
|
| 413 |
-
all_token_ids,
|
| 414 |
-
all_token_lengths,
|
| 415 |
-
self.vocabulary.spellings,
|
| 416 |
-
)
|
| 417 |
-
reuse_rows.append(rows)
|
| 418 |
-
sample_support = exact_document_frequency(rows)
|
| 419 |
-
if self.settings.reuse_support_scope == "batch":
|
| 420 |
-
batch_token_ids, batch_token_lengths = flattened_to_padded(
|
| 421 |
-
side_features
|
| 422 |
-
)
|
| 423 |
-
batch_rows = canonical_atom_rows(
|
| 424 |
-
batch_token_ids,
|
| 425 |
-
batch_token_lengths,
|
| 426 |
-
self.vocabulary.spellings,
|
| 427 |
-
)
|
| 428 |
-
candidates = sample_support.keys()
|
| 429 |
-
reuse_supports.append(
|
| 430 |
-
exact_candidate_document_frequency(batch_rows, candidates)
|
| 431 |
-
)
|
| 432 |
-
else:
|
| 433 |
-
batch_rows = rows
|
| 434 |
-
reuse_supports.append(sample_support)
|
| 435 |
-
if self.settings.recurrent_residuals:
|
| 436 |
-
fold_zero = tuple(
|
| 437 |
-
row for position, row in enumerate(batch_rows) if position % 2 == 0
|
| 438 |
-
)
|
| 439 |
-
fold_one = tuple(
|
| 440 |
-
row for position, row in enumerate(batch_rows) if position % 2 == 1
|
| 441 |
-
)
|
| 442 |
-
if not fold_zero or not fold_one:
|
| 443 |
-
raise ValueError("OpenLex recurrent residual folds must be nonempty")
|
| 444 |
-
if self.settings.reuse_support_scope == "batch":
|
| 445 |
-
crossfit_supports.append(
|
| 446 |
-
(
|
| 447 |
-
exact_candidate_document_frequency(
|
| 448 |
-
fold_one, sample_support.keys()
|
| 449 |
-
),
|
| 450 |
-
exact_candidate_document_frequency(
|
| 451 |
-
fold_zero, sample_support.keys()
|
| 452 |
-
),
|
| 453 |
-
)
|
| 454 |
-
)
|
| 455 |
-
else:
|
| 456 |
-
crossfit_supports.append(
|
| 457 |
-
(
|
| 458 |
-
exact_document_frequency(fold_one),
|
| 459 |
-
exact_document_frequency(fold_zero),
|
| 460 |
-
)
|
| 461 |
-
)
|
| 462 |
-
lexical = base_loss.new_zeros(())
|
| 463 |
-
expected_counts: list[torch.Tensor] = []
|
| 464 |
-
reuse_rates: list[torch.Tensor] = []
|
| 465 |
-
map_tokens = 0
|
| 466 |
-
changed_paths = 0
|
| 467 |
-
novel_map_edges = 0
|
| 468 |
-
singleton_novel_map_edges = 0
|
| 469 |
-
recurrent_novel_map_edges = 0
|
| 470 |
-
crossfit_supported_edges = 0
|
| 471 |
-
terminal_atoms = 0
|
| 472 |
-
legal_spans = 0
|
| 473 |
-
for selected_chunk in selected_chunks:
|
| 474 |
-
for side_index, side_features in enumerate(features):
|
| 475 |
-
selected = select_static_features(side_features, selected_chunk)
|
| 476 |
-
token_ids, token_lengths = flattened_to_padded(selected)
|
| 477 |
-
canonical = self.vocabulary.canonicalize(token_ids, token_lengths)
|
| 478 |
-
candidate_support = None
|
| 479 |
-
if crossfit_supports:
|
| 480 |
-
chunk_indices = tuple(int(index) for index in selected_chunk.tolist())
|
| 481 |
-
chunk_rows = tuple(
|
| 482 |
-
reuse_rows[side_index][selected_positions[index]]
|
| 483 |
-
for index in chunk_indices
|
| 484 |
-
)
|
| 485 |
-
candidate_support = cross_fitted_supported_novel_span_mask(
|
| 486 |
-
chunk_rows,
|
| 487 |
-
folds=tuple(selected_folds[index] for index in chunk_indices),
|
| 488 |
-
opposite_fold_frequencies=crossfit_supports[side_index],
|
| 489 |
-
registered_types=self.registered_types,
|
| 490 |
-
width=int(canonical.atom_ids.shape[1]),
|
| 491 |
-
device=canonical.atom_ids.device,
|
| 492 |
-
)
|
| 493 |
-
crossfit_supported_edges += int(candidate_support.sum().item())
|
| 494 |
-
output = self.openlex(
|
| 495 |
-
canonical.atom_ids,
|
| 496 |
-
canonical.lengths,
|
| 497 |
-
preferred_edges=canonical.preferred_edges,
|
| 498 |
-
override_ids=canonical.override_ids,
|
| 499 |
-
candidate_support=candidate_support,
|
| 500 |
-
)
|
| 501 |
-
selected_device = selected_chunk.to(
|
| 502 |
-
device=cached_gradients[side_index].device
|
| 503 |
-
)
|
| 504 |
-
lexical_term = scaled_cached_jvp(
|
| 505 |
-
output.embeddings,
|
| 506 |
-
cached_gradients[side_index].index_select(0, selected_device),
|
| 507 |
-
batch_size=pair_count,
|
| 508 |
-
sample_size=self.settings.sample_pairs,
|
| 509 |
-
)
|
| 510 |
-
expected_count = output.lattice.marginals.sum(dim=(1, 2))
|
| 511 |
-
expected_counts.append(expected_count.detach())
|
| 512 |
-
reuse_rate = None
|
| 513 |
-
if reuse_supports:
|
| 514 |
-
reuse_costs = reuse_description_edge_costs(
|
| 515 |
-
canonical.atom_ids,
|
| 516 |
-
canonical.lengths,
|
| 517 |
-
output.lattice.marginals.ne(0),
|
| 518 |
-
canonical.override_ids,
|
| 519 |
-
reuse_supports[side_index],
|
| 520 |
-
)
|
| 521 |
-
reuse_rate = (
|
| 522 |
-
output.lattice.marginals
|
| 523 |
-
* reuse_costs.to(dtype=output.lattice.marginals.dtype)
|
| 524 |
-
).sum(dim=(1, 2))
|
| 525 |
-
reuse_rates.append(reuse_rate.detach())
|
| 526 |
-
singleton, recurrent = map_novel_reuse_counts(
|
| 527 |
-
canonical.atom_ids,
|
| 528 |
-
canonical.lengths,
|
| 529 |
-
output.lattice.map_edges,
|
| 530 |
-
canonical.override_ids,
|
| 531 |
-
reuse_supports[side_index],
|
| 532 |
-
)
|
| 533 |
-
singleton_novel_map_edges += singleton
|
| 534 |
-
recurrent_novel_map_edges += recurrent
|
| 535 |
-
structural_values = (
|
| 536 |
-
reuse_rate
|
| 537 |
-
if self.settings.structural_objective == "reuse_mdl"
|
| 538 |
-
else expected_count
|
| 539 |
-
)
|
| 540 |
-
chunk_auxiliary = lexical_term + (
|
| 541 |
-
self.settings.compression_weight
|
| 542 |
-
* structural_values.sum()
|
| 543 |
-
/ float(2 * self.settings.sample_pairs)
|
| 544 |
-
)
|
| 545 |
-
chunk_auxiliary.backward()
|
| 546 |
-
lexical = lexical + lexical_term.detach()
|
| 547 |
-
map_tokens += int(output.lattice.map_edges.sum().item())
|
| 548 |
-
changed_paths += int(
|
| 549 |
-
(output.lattice.map_edges != canonical.preferred_edges)
|
| 550 |
-
.flatten(1)
|
| 551 |
-
.any(dim=1)
|
| 552 |
-
.sum()
|
| 553 |
-
.item()
|
| 554 |
-
)
|
| 555 |
-
novel_map_edges += int(
|
| 556 |
-
(output.lattice.map_edges & (canonical.override_ids < 0)).sum().item()
|
| 557 |
-
)
|
| 558 |
-
terminal_atoms += int(canonical.lengths.sum().item())
|
| 559 |
-
legal_spans += int(output.lattice.marginals.ne(0).sum().item())
|
| 560 |
-
expected_token_count = torch.cat(expected_counts).mean()
|
| 561 |
-
reuse_mdl_rate = (
|
| 562 |
-
torch.cat(reuse_rates).mean()
|
| 563 |
-
if reuse_rates
|
| 564 |
-
else expected_token_count.new_zeros(())
|
| 565 |
-
)
|
| 566 |
-
structural = (
|
| 567 |
-
reuse_mdl_rate
|
| 568 |
-
if self.settings.structural_objective == "reuse_mdl"
|
| 569 |
-
else expected_token_count
|
| 570 |
-
)
|
| 571 |
-
compression = self.settings.compression_weight * structural
|
| 572 |
-
repeated_span_types = sum(
|
| 573 |
-
1
|
| 574 |
-
for support in reuse_supports
|
| 575 |
-
for frequency in support.values()
|
| 576 |
-
if frequency > 1
|
| 577 |
-
)
|
| 578 |
-
self._latest_metrics = {
|
| 579 |
-
"sample_pairs": float(self.settings.sample_pairs),
|
| 580 |
-
"reuse_support_rows": float(
|
| 581 |
-
pair_count
|
| 582 |
-
if self.settings.reuse_support_scope == "batch"
|
| 583 |
-
else self.settings.sample_pairs
|
| 584 |
-
),
|
| 585 |
-
"terminal_atoms": float(terminal_atoms),
|
| 586 |
-
"legal_spans": float(legal_spans),
|
| 587 |
-
"expected_token_count": float(expected_token_count.detach().item()),
|
| 588 |
-
"map_token_count": float(map_tokens),
|
| 589 |
-
"changed_path_rate": float(changed_paths) / float(2 * self.settings.sample_pairs),
|
| 590 |
-
"novel_map_edges": float(novel_map_edges),
|
| 591 |
-
"singleton_novel_map_edges": float(singleton_novel_map_edges),
|
| 592 |
-
"recurrent_novel_map_edges": float(recurrent_novel_map_edges),
|
| 593 |
-
"repeated_span_types": float(repeated_span_types),
|
| 594 |
-
"crossfit_supported_edges": float(crossfit_supported_edges),
|
| 595 |
-
"reuse_mdl_rate": float(reuse_mdl_rate.detach().item()),
|
| 596 |
-
"lexical_jvp": float(lexical.detach().item()),
|
| 597 |
-
"compression": float(compression.detach().item()),
|
| 598 |
-
"completed_forwards": float(self.sampler.completed_forwards),
|
| 599 |
-
}
|
| 600 |
-
# The table is detached from OpenLex, so each sampled policy graph is
|
| 601 |
-
# released above before the next quadratic lattice is materialized.
|
| 602 |
-
# Policy gradients accumulate until GradCache replays the exact table
|
| 603 |
-
# loss; trainer-level gradient accumulation is forbidden by protocol.
|
| 604 |
-
return base_loss
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_replay_checkpoint.py
DELETED
|
@@ -1,168 +0,0 @@
|
|
| 1 |
-
"""Atomic OpenLex replay sidecars and Trainer metric injection."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import math
|
| 6 |
-
import os
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Any, Mapping
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
from transformers import TrainerCallback
|
| 12 |
-
|
| 13 |
-
from starse.gradient_safety import stable_clip_gradients
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
_PROTOCOL = "openlex-cvr-sidecar-v1"
|
| 17 |
-
_SIDECAR = "openlex_replay.pt"
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def _fsync_directory(path: Path) -> None:
|
| 21 |
-
descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
|
| 22 |
-
try:
|
| 23 |
-
os.fsync(descriptor)
|
| 24 |
-
finally:
|
| 25 |
-
os.close(descriptor)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _policy_parameters(replay: Any) -> dict[str, torch.nn.Parameter]:
|
| 29 |
-
named = dict(replay.policy_named_parameters())
|
| 30 |
-
if not named or len({id(parameter) for parameter in named.values()}) != len(named):
|
| 31 |
-
raise RuntimeError("OpenLex replay policy parameter inventory is invalid")
|
| 32 |
-
if not all(isinstance(parameter, torch.nn.Parameter) for parameter in named.values()):
|
| 33 |
-
raise TypeError("OpenLex replay policy inventory contains a non-parameter")
|
| 34 |
-
return named
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def _state_payload(replay: Any, *, identity: Mapping[str, object], step: int) -> dict[str, object]:
|
| 38 |
-
if isinstance(step, bool) or step < 0:
|
| 39 |
-
raise ValueError("OpenLex replay checkpoint step must be nonnegative")
|
| 40 |
-
return {
|
| 41 |
-
"protocol": _PROTOCOL,
|
| 42 |
-
"step": int(step),
|
| 43 |
-
"identity": dict(identity),
|
| 44 |
-
"policy": {
|
| 45 |
-
name: parameter.detach().cpu().clone()
|
| 46 |
-
for name, parameter in _policy_parameters(replay).items()
|
| 47 |
-
},
|
| 48 |
-
"sampler": replay.sampler.state_payload(),
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
class OpenLexReplayCheckpointCallback(TrainerCallback):
|
| 53 |
-
"""Persist replay policy and RNG after every complete Trainer checkpoint."""
|
| 54 |
-
|
| 55 |
-
def __init__(
|
| 56 |
-
self,
|
| 57 |
-
*,
|
| 58 |
-
replay: Any,
|
| 59 |
-
output_dir: Path,
|
| 60 |
-
identity: Mapping[str, object],
|
| 61 |
-
) -> None:
|
| 62 |
-
self.replay = replay
|
| 63 |
-
self.output_dir = Path(output_dir).expanduser().resolve()
|
| 64 |
-
self.identity = dict(identity)
|
| 65 |
-
self.completed_saves = 0
|
| 66 |
-
|
| 67 |
-
def on_save(self, args, state, control, **kwargs):
|
| 68 |
-
del kwargs
|
| 69 |
-
if Path(args.output_dir).expanduser().resolve() != self.output_dir:
|
| 70 |
-
raise ValueError("OpenLex replay output directory differs from Trainer")
|
| 71 |
-
step = int(state.global_step)
|
| 72 |
-
checkpoint = self.output_dir / f"checkpoint-{step}"
|
| 73 |
-
if not checkpoint.is_dir():
|
| 74 |
-
raise FileNotFoundError(checkpoint)
|
| 75 |
-
destination = checkpoint / _SIDECAR
|
| 76 |
-
temporary = checkpoint / f"{_SIDECAR}.tmp"
|
| 77 |
-
if destination.exists() or temporary.exists():
|
| 78 |
-
raise FileExistsError(destination)
|
| 79 |
-
try:
|
| 80 |
-
torch.save(
|
| 81 |
-
_state_payload(self.replay, identity=self.identity, step=step),
|
| 82 |
-
temporary,
|
| 83 |
-
)
|
| 84 |
-
with temporary.open("rb") as handle:
|
| 85 |
-
os.fsync(handle.fileno())
|
| 86 |
-
os.replace(temporary, destination)
|
| 87 |
-
_fsync_directory(checkpoint)
|
| 88 |
-
except BaseException:
|
| 89 |
-
if temporary.exists():
|
| 90 |
-
temporary.unlink()
|
| 91 |
-
raise
|
| 92 |
-
self.completed_saves += 1
|
| 93 |
-
return control
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def load_openlex_replay_checkpoint(
|
| 97 |
-
checkpoint: Path,
|
| 98 |
-
*,
|
| 99 |
-
replay: Any,
|
| 100 |
-
expected_identity: Mapping[str, object],
|
| 101 |
-
expected_step: int,
|
| 102 |
-
) -> None:
|
| 103 |
-
"""Fail closed, then restore policy tensors and the exact next RNG draw."""
|
| 104 |
-
|
| 105 |
-
sidecar = Path(checkpoint).expanduser().resolve() / _SIDECAR
|
| 106 |
-
if not sidecar.is_file():
|
| 107 |
-
raise FileNotFoundError(f"OpenLex replay resume requires sidecar: {sidecar}")
|
| 108 |
-
payload = torch.load(sidecar, map_location="cpu", weights_only=True)
|
| 109 |
-
if not isinstance(payload, dict) or payload.get("protocol") != _PROTOCOL:
|
| 110 |
-
raise ValueError("OpenLex replay sidecar protocol mismatch")
|
| 111 |
-
if payload.get("identity") != dict(expected_identity):
|
| 112 |
-
raise ValueError("OpenLex replay sidecar identity mismatch")
|
| 113 |
-
if payload.get("step") != int(expected_step):
|
| 114 |
-
raise ValueError("OpenLex replay sidecar step mismatch")
|
| 115 |
-
observed = payload.get("policy")
|
| 116 |
-
parameters = _policy_parameters(replay)
|
| 117 |
-
if not isinstance(observed, dict) or set(observed) != set(parameters):
|
| 118 |
-
raise ValueError("OpenLex replay policy inventory mismatch")
|
| 119 |
-
for name, parameter in parameters.items():
|
| 120 |
-
value = observed[name]
|
| 121 |
-
if (
|
| 122 |
-
not isinstance(value, torch.Tensor)
|
| 123 |
-
or value.shape != parameter.shape
|
| 124 |
-
or value.dtype != parameter.dtype
|
| 125 |
-
or (value.is_floating_point() and not bool(torch.isfinite(value).all()))
|
| 126 |
-
):
|
| 127 |
-
raise ValueError(f"OpenLex replay policy tensor mismatch: {name}")
|
| 128 |
-
sampler = payload.get("sampler")
|
| 129 |
-
if not isinstance(sampler, dict):
|
| 130 |
-
raise ValueError("OpenLex replay sampler state is missing")
|
| 131 |
-
|
| 132 |
-
with torch.no_grad():
|
| 133 |
-
for name, parameter in parameters.items():
|
| 134 |
-
parameter.copy_(observed[name].to(device=parameter.device))
|
| 135 |
-
replay.sampler.load_state_payload(sampler)
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
class OpenLexReplayLoggingCallback(TrainerCallback):
|
| 139 |
-
"""Safely clip policy gradients before AdamW and inject replay diagnostics."""
|
| 140 |
-
|
| 141 |
-
def __init__(self, replay: Any, *, max_grad_norm: float) -> None:
|
| 142 |
-
self.replay = replay
|
| 143 |
-
self.max_grad_norm = float(max_grad_norm)
|
| 144 |
-
self._gradient_metrics: dict[str, float] = {}
|
| 145 |
-
|
| 146 |
-
def on_pre_optimizer_step(self, args, state, control, **kwargs):
|
| 147 |
-
del args, state, control, kwargs
|
| 148 |
-
report = stable_clip_gradients(
|
| 149 |
-
(
|
| 150 |
-
(f"policy.{name}", parameter)
|
| 151 |
-
for name, parameter in _policy_parameters(self.replay).items()
|
| 152 |
-
),
|
| 153 |
-
max_norm=self.max_grad_norm,
|
| 154 |
-
group_name=lambda _name: "policy",
|
| 155 |
-
)
|
| 156 |
-
self._gradient_metrics = {
|
| 157 |
-
"policy_gradient_norm": report.total_norm,
|
| 158 |
-
"policy_gradient_clip_coefficient": report.clip_coefficient,
|
| 159 |
-
"policy_gradient_finite": 1.0,
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
def on_log(self, args, state, control, logs=None, **kwargs):
|
| 163 |
-
del args, state, control, kwargs
|
| 164 |
-
if logs is None:
|
| 165 |
-
return
|
| 166 |
-
metrics = self.replay.consume_metrics()
|
| 167 |
-
metrics.update(self._gradient_metrics)
|
| 168 |
-
logs.update({f"openlex/{name}": float(value) for name, value in metrics.items()})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_reuse.py
DELETED
|
@@ -1,338 +0,0 @@
|
|
| 1 |
-
"""Exact within-sample reuse accounting for unrestricted OpenLex spans."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from collections.abc import Collection, Mapping, Sequence
|
| 6 |
-
|
| 7 |
-
import torch
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
SpanKey = tuple[int, ...]
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def canonical_atom_rows(
|
| 14 |
-
token_ids: torch.Tensor,
|
| 15 |
-
lengths: torch.Tensor,
|
| 16 |
-
spellings: Sequence[Sequence[int]],
|
| 17 |
-
) -> tuple[SpanKey, ...]:
|
| 18 |
-
"""Expand old token rows without materializing a square canonical lattice."""
|
| 19 |
-
|
| 20 |
-
if token_ids.ndim != 2 or token_ids.dtype not in (torch.int32, torch.int64):
|
| 21 |
-
raise ValueError("reuse token ids must be a rank-2 integer tensor")
|
| 22 |
-
batch, width = token_ids.shape
|
| 23 |
-
if lengths.shape != (batch,) or lengths.dtype not in (torch.int32, torch.int64):
|
| 24 |
-
raise ValueError("reuse token lengths must contain one integer per sentence")
|
| 25 |
-
if token_ids.device != lengths.device:
|
| 26 |
-
raise ValueError("reuse token ids and lengths must share one device")
|
| 27 |
-
cpu_tokens = token_ids.detach().to(device="cpu", dtype=torch.long)
|
| 28 |
-
cpu_lengths = lengths.detach().to(device="cpu", dtype=torch.long)
|
| 29 |
-
if bool(((cpu_lengths < 1) | (cpu_lengths > width)).any()):
|
| 30 |
-
raise ValueError("reuse token length lies outside the padded width")
|
| 31 |
-
rows: list[SpanKey] = []
|
| 32 |
-
for sentence in range(batch):
|
| 33 |
-
atoms: list[int] = []
|
| 34 |
-
for raw_token in cpu_tokens[sentence, : int(cpu_lengths[sentence])].tolist():
|
| 35 |
-
token = int(raw_token)
|
| 36 |
-
if token < 0 or token >= len(spellings):
|
| 37 |
-
raise ValueError("reuse token id lies outside the canonical vocabulary")
|
| 38 |
-
atoms.extend(int(atom) for atom in spellings[token])
|
| 39 |
-
if not atoms:
|
| 40 |
-
raise ValueError("reuse canonical expansion produced an empty sentence")
|
| 41 |
-
rows.append(tuple(atoms))
|
| 42 |
-
return tuple(rows)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def exact_document_frequency(rows: Sequence[Sequence[int]]) -> dict[SpanKey, int]:
|
| 46 |
-
"""Count a span once per sentence using exact terminal-id tuples."""
|
| 47 |
-
|
| 48 |
-
frequencies: dict[SpanKey, int] = {}
|
| 49 |
-
for raw_row in rows:
|
| 50 |
-
row = tuple(int(value) for value in raw_row)
|
| 51 |
-
if not row:
|
| 52 |
-
raise ValueError("reuse accounting requires nonempty sentences")
|
| 53 |
-
sentence_types = {
|
| 54 |
-
row[start:end]
|
| 55 |
-
for start in range(len(row))
|
| 56 |
-
for end in range(start + 1, len(row) + 1)
|
| 57 |
-
}
|
| 58 |
-
for key in sentence_types:
|
| 59 |
-
frequencies[key] = frequencies.get(key, 0) + 1
|
| 60 |
-
if not frequencies:
|
| 61 |
-
raise ValueError("reuse accounting requires at least one sentence")
|
| 62 |
-
return frequencies
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def exact_candidate_document_frequency(
|
| 66 |
-
rows: Sequence[Sequence[int]],
|
| 67 |
-
candidates: Collection[SpanKey],
|
| 68 |
-
) -> dict[SpanKey, int]:
|
| 69 |
-
"""Count only queried exact span types across a larger sentence sample."""
|
| 70 |
-
|
| 71 |
-
normalized_candidates = {
|
| 72 |
-
tuple(int(value) for value in candidate) for candidate in candidates
|
| 73 |
-
}
|
| 74 |
-
if not normalized_candidates or any(
|
| 75 |
-
not candidate or min(candidate) < 0 for candidate in normalized_candidates
|
| 76 |
-
):
|
| 77 |
-
raise ValueError("candidate reuse accounting requires nonempty terminal tuples")
|
| 78 |
-
|
| 79 |
-
terminal = object()
|
| 80 |
-
trie: dict[object, object] = {}
|
| 81 |
-
for candidate in normalized_candidates:
|
| 82 |
-
node = trie
|
| 83 |
-
for atom in candidate:
|
| 84 |
-
child = node.setdefault(atom, {})
|
| 85 |
-
if not isinstance(child, dict):
|
| 86 |
-
raise RuntimeError("candidate trie is malformed")
|
| 87 |
-
node = child
|
| 88 |
-
node[terminal] = candidate
|
| 89 |
-
|
| 90 |
-
frequencies = {candidate: 0 for candidate in normalized_candidates}
|
| 91 |
-
if not rows:
|
| 92 |
-
raise ValueError("candidate reuse accounting requires at least one sentence")
|
| 93 |
-
for raw_row in rows:
|
| 94 |
-
row = tuple(int(value) for value in raw_row)
|
| 95 |
-
if not row or min(row) < 0:
|
| 96 |
-
raise ValueError("candidate reuse rows must contain terminal ids")
|
| 97 |
-
observed: set[SpanKey] = set()
|
| 98 |
-
for start in range(len(row)):
|
| 99 |
-
node = trie
|
| 100 |
-
for atom in row[start:]:
|
| 101 |
-
child = node.get(atom)
|
| 102 |
-
if not isinstance(child, dict):
|
| 103 |
-
break
|
| 104 |
-
node = child
|
| 105 |
-
candidate = node.get(terminal)
|
| 106 |
-
if isinstance(candidate, tuple):
|
| 107 |
-
observed.add(candidate)
|
| 108 |
-
for candidate in observed:
|
| 109 |
-
frequencies[candidate] += 1
|
| 110 |
-
return frequencies
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def map_novel_reuse_counts(
|
| 114 |
-
atom_ids: torch.Tensor,
|
| 115 |
-
lengths: torch.Tensor,
|
| 116 |
-
map_edges: torch.Tensor,
|
| 117 |
-
override_ids: torch.Tensor,
|
| 118 |
-
document_frequency: Mapping[SpanKey, int],
|
| 119 |
-
) -> tuple[int, int]:
|
| 120 |
-
"""Count singleton versus independently supported novel MAP edges."""
|
| 121 |
-
|
| 122 |
-
if atom_ids.ndim != 2 or atom_ids.dtype not in (torch.int32, torch.int64):
|
| 123 |
-
raise ValueError("reuse atom ids must be a rank-2 integer tensor")
|
| 124 |
-
batch, width = atom_ids.shape
|
| 125 |
-
expected_shape = (batch, width + 1, width + 1)
|
| 126 |
-
if lengths.shape != (batch,) or lengths.dtype not in (torch.int32, torch.int64):
|
| 127 |
-
raise ValueError("reuse lengths must contain one integer per sentence")
|
| 128 |
-
if map_edges.shape != expected_shape or map_edges.dtype is not torch.bool:
|
| 129 |
-
raise ValueError("reuse MAP edges must be a square boolean boundary lattice")
|
| 130 |
-
if override_ids.shape != expected_shape or override_ids.dtype not in (
|
| 131 |
-
torch.int32,
|
| 132 |
-
torch.int64,
|
| 133 |
-
):
|
| 134 |
-
raise ValueError("reuse override ids must align with MAP edges")
|
| 135 |
-
if len({atom_ids.device, lengths.device, map_edges.device, override_ids.device}) != 1:
|
| 136 |
-
raise ValueError("reuse MAP tensors must share one device")
|
| 137 |
-
if bool(((lengths < 1) | (lengths > width)).any()):
|
| 138 |
-
raise ValueError("reuse sentence length lies outside the padded atom width")
|
| 139 |
-
cpu_atoms = atom_ids.detach().to(device="cpu", dtype=torch.long)
|
| 140 |
-
cpu_lengths = lengths.detach().to(device="cpu", dtype=torch.long)
|
| 141 |
-
cpu_map = map_edges.detach().to(device="cpu")
|
| 142 |
-
cpu_overrides = override_ids.detach().to(device="cpu")
|
| 143 |
-
singleton = 0
|
| 144 |
-
recurrent = 0
|
| 145 |
-
for sentence in range(batch):
|
| 146 |
-
length = int(cpu_lengths[sentence].item())
|
| 147 |
-
row = tuple(int(value) for value in cpu_atoms[sentence, :length].tolist())
|
| 148 |
-
for start, end in cpu_map[sentence].nonzero(as_tuple=False).tolist():
|
| 149 |
-
if start >= end or int(cpu_overrides[sentence, start, end].item()) >= 0:
|
| 150 |
-
continue
|
| 151 |
-
if int(document_frequency.get(row[start:end], 1)) > 1:
|
| 152 |
-
recurrent += 1
|
| 153 |
-
else:
|
| 154 |
-
singleton += 1
|
| 155 |
-
return singleton, recurrent
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def _validate_lattice_inputs(
|
| 159 |
-
atom_ids: torch.Tensor,
|
| 160 |
-
lengths: torch.Tensor,
|
| 161 |
-
span_mask: torch.Tensor,
|
| 162 |
-
override_ids: torch.Tensor,
|
| 163 |
-
) -> tuple[int, int]:
|
| 164 |
-
if atom_ids.ndim != 2 or atom_ids.dtype not in (torch.int32, torch.int64):
|
| 165 |
-
raise ValueError("reuse atom ids must be a rank-2 integer tensor")
|
| 166 |
-
batch, width = atom_ids.shape
|
| 167 |
-
if lengths.shape != (batch,) or lengths.dtype not in (torch.int32, torch.int64):
|
| 168 |
-
raise ValueError("reuse lengths must contain one integer per sentence")
|
| 169 |
-
expected_shape = (batch, width + 1, width + 1)
|
| 170 |
-
if span_mask.shape != expected_shape or span_mask.dtype is not torch.bool:
|
| 171 |
-
raise ValueError("reuse span mask must be a square boolean boundary lattice")
|
| 172 |
-
if override_ids.shape != expected_shape or override_ids.dtype not in (
|
| 173 |
-
torch.int32,
|
| 174 |
-
torch.int64,
|
| 175 |
-
):
|
| 176 |
-
raise ValueError("reuse override ids must align with the boundary lattice")
|
| 177 |
-
devices = {atom_ids.device, lengths.device, span_mask.device, override_ids.device}
|
| 178 |
-
if len(devices) != 1:
|
| 179 |
-
raise ValueError("reuse lattice tensors must share one device")
|
| 180 |
-
if bool(((lengths < 1) | (lengths > width)).any()):
|
| 181 |
-
raise ValueError("reuse sentence length lies outside the padded atom width")
|
| 182 |
-
if bool(((override_ids >= 0) & ~span_mask).any()):
|
| 183 |
-
raise ValueError("reuse override ids may name only legal spans")
|
| 184 |
-
return batch, width
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def reuse_description_edge_costs(
|
| 188 |
-
atom_ids: torch.Tensor,
|
| 189 |
-
lengths: torch.Tensor,
|
| 190 |
-
span_mask: torch.Tensor,
|
| 191 |
-
override_ids: torch.Tensor,
|
| 192 |
-
document_frequency: Mapping[SpanKey, int],
|
| 193 |
-
) -> torch.Tensor:
|
| 194 |
-
"""Return lookup plus amortized exact-spelling cost for every legal edge."""
|
| 195 |
-
|
| 196 |
-
batch, _width = _validate_lattice_inputs(
|
| 197 |
-
atom_ids,
|
| 198 |
-
lengths,
|
| 199 |
-
span_mask,
|
| 200 |
-
override_ids,
|
| 201 |
-
)
|
| 202 |
-
costs = torch.zeros(span_mask.shape, dtype=torch.float32, device=atom_ids.device)
|
| 203 |
-
costs.masked_fill_(span_mask, 1.0)
|
| 204 |
-
cpu_atoms = atom_ids.detach().to(device="cpu", dtype=torch.long)
|
| 205 |
-
cpu_lengths = lengths.detach().to(device="cpu", dtype=torch.long)
|
| 206 |
-
cpu_mask = span_mask.detach().to(device="cpu")
|
| 207 |
-
cpu_overrides = override_ids.detach().to(device="cpu")
|
| 208 |
-
novel_costs: list[tuple[int, int, int, float]] = []
|
| 209 |
-
for sentence in range(batch):
|
| 210 |
-
length = int(cpu_lengths[sentence].item())
|
| 211 |
-
row = tuple(int(value) for value in cpu_atoms[sentence, :length].tolist())
|
| 212 |
-
for start in range(length):
|
| 213 |
-
for end in range(start + 1, length + 1):
|
| 214 |
-
if not bool(cpu_mask[sentence, start, end]):
|
| 215 |
-
continue
|
| 216 |
-
if int(cpu_overrides[sentence, start, end].item()) >= 0:
|
| 217 |
-
continue
|
| 218 |
-
key = row[start:end]
|
| 219 |
-
frequency = int(document_frequency.get(key, 1))
|
| 220 |
-
if frequency <= 0:
|
| 221 |
-
raise ValueError("reuse document frequencies must be positive")
|
| 222 |
-
description = 1 + len(key)
|
| 223 |
-
novel_costs.append(
|
| 224 |
-
(sentence, start, end, 1.0 + float(description) / float(frequency))
|
| 225 |
-
)
|
| 226 |
-
for sentence, start, end, value in novel_costs:
|
| 227 |
-
costs[sentence, start, end] = value
|
| 228 |
-
return costs
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
def supported_novel_span_mask(
|
| 232 |
-
atom_ids: torch.Tensor,
|
| 233 |
-
lengths: torch.Tensor,
|
| 234 |
-
span_mask: torch.Tensor,
|
| 235 |
-
override_ids: torch.Tensor,
|
| 236 |
-
opposite_fold_frequency: Mapping[SpanKey, int],
|
| 237 |
-
) -> torch.Tensor:
|
| 238 |
-
"""Enable a novel residual only when its exact type occurs in the other fold."""
|
| 239 |
-
|
| 240 |
-
batch, _width = _validate_lattice_inputs(
|
| 241 |
-
atom_ids,
|
| 242 |
-
lengths,
|
| 243 |
-
span_mask,
|
| 244 |
-
override_ids,
|
| 245 |
-
)
|
| 246 |
-
supported = torch.zeros_like(span_mask)
|
| 247 |
-
cpu_atoms = atom_ids.detach().to(device="cpu", dtype=torch.long)
|
| 248 |
-
cpu_lengths = lengths.detach().to(device="cpu", dtype=torch.long)
|
| 249 |
-
cpu_mask = span_mask.detach().to(device="cpu")
|
| 250 |
-
cpu_overrides = override_ids.detach().to(device="cpu")
|
| 251 |
-
selected: list[tuple[int, int, int]] = []
|
| 252 |
-
for sentence in range(batch):
|
| 253 |
-
length = int(cpu_lengths[sentence].item())
|
| 254 |
-
row = tuple(int(value) for value in cpu_atoms[sentence, :length].tolist())
|
| 255 |
-
for start in range(length):
|
| 256 |
-
for end in range(start + 1, length + 1):
|
| 257 |
-
if (
|
| 258 |
-
bool(cpu_mask[sentence, start, end])
|
| 259 |
-
and int(cpu_overrides[sentence, start, end].item()) < 0
|
| 260 |
-
and int(opposite_fold_frequency.get(row[start:end], 0)) > 0
|
| 261 |
-
):
|
| 262 |
-
selected.append((sentence, start, end))
|
| 263 |
-
for sentence, start, end in selected:
|
| 264 |
-
supported[sentence, start, end] = True
|
| 265 |
-
return supported
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
def cross_fitted_supported_novel_span_mask(
|
| 269 |
-
rows: Sequence[Sequence[int]],
|
| 270 |
-
*,
|
| 271 |
-
folds: Sequence[int],
|
| 272 |
-
opposite_fold_frequencies: tuple[
|
| 273 |
-
Mapping[SpanKey, int], Mapping[SpanKey, int]
|
| 274 |
-
],
|
| 275 |
-
registered_types: Collection[SpanKey],
|
| 276 |
-
width: int,
|
| 277 |
-
device: torch.device,
|
| 278 |
-
) -> torch.Tensor:
|
| 279 |
-
"""Build exact cross-fit support in one CPU tuple scan per sampled row."""
|
| 280 |
-
|
| 281 |
-
if not rows or len(rows) != len(folds):
|
| 282 |
-
raise ValueError("cross-fit rows and folds must be nonempty and aligned")
|
| 283 |
-
if isinstance(width, bool) or width <= 0:
|
| 284 |
-
raise ValueError("cross-fit lattice width must be positive")
|
| 285 |
-
normalized_rows = tuple(tuple(int(value) for value in row) for row in rows)
|
| 286 |
-
if any(not row or min(row) < 0 or len(row) > width for row in normalized_rows):
|
| 287 |
-
raise ValueError("cross-fit rows must contain valid terminal ids within width")
|
| 288 |
-
if any(fold not in (0, 1) for fold in folds):
|
| 289 |
-
raise ValueError("cross-fit folds must be zero or one")
|
| 290 |
-
|
| 291 |
-
registered = registered_types
|
| 292 |
-
sentence_indices: list[int] = []
|
| 293 |
-
starts: list[int] = []
|
| 294 |
-
ends: list[int] = []
|
| 295 |
-
for sentence, (row, fold) in enumerate(zip(normalized_rows, folds, strict=True)):
|
| 296 |
-
opposite = opposite_fold_frequencies[int(fold)]
|
| 297 |
-
for start in range(len(row)):
|
| 298 |
-
for end in range(start + 2, len(row) + 1):
|
| 299 |
-
key = row[start:end]
|
| 300 |
-
if int(opposite.get(key, 0)) > 0 and key not in registered:
|
| 301 |
-
sentence_indices.append(sentence)
|
| 302 |
-
starts.append(start)
|
| 303 |
-
ends.append(end)
|
| 304 |
-
|
| 305 |
-
supported = torch.zeros(
|
| 306 |
-
len(rows), width + 1, width + 1, dtype=torch.bool, device=device
|
| 307 |
-
)
|
| 308 |
-
if sentence_indices:
|
| 309 |
-
supported[
|
| 310 |
-
torch.tensor(sentence_indices, dtype=torch.long, device=device),
|
| 311 |
-
torch.tensor(starts, dtype=torch.long, device=device),
|
| 312 |
-
torch.tensor(ends, dtype=torch.long, device=device),
|
| 313 |
-
] = True
|
| 314 |
-
return supported
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
def reuse_description_rate(
|
| 318 |
-
atom_ids: torch.Tensor,
|
| 319 |
-
lengths: torch.Tensor,
|
| 320 |
-
span_mask: torch.Tensor,
|
| 321 |
-
override_ids: torch.Tensor,
|
| 322 |
-
marginals: torch.Tensor,
|
| 323 |
-
document_frequency: Mapping[SpanKey, int],
|
| 324 |
-
) -> torch.Tensor:
|
| 325 |
-
"""Mean differentiable description rate under exact lattice marginals."""
|
| 326 |
-
|
| 327 |
-
if marginals.shape != span_mask.shape or not marginals.is_floating_point():
|
| 328 |
-
raise ValueError("reuse marginals must be floating point and align with spans")
|
| 329 |
-
if marginals.device != atom_ids.device:
|
| 330 |
-
raise ValueError("reuse marginals must share the atom device")
|
| 331 |
-
costs = reuse_description_edge_costs(
|
| 332 |
-
atom_ids,
|
| 333 |
-
lengths,
|
| 334 |
-
span_mask,
|
| 335 |
-
override_ids,
|
| 336 |
-
document_frequency,
|
| 337 |
-
)
|
| 338 |
-
return (marginals * costs.to(dtype=marginals.dtype)).sum(dim=(1, 2)).mean()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_spans.py
DELETED
|
@@ -1,215 +0,0 @@
|
|
| 1 |
-
"""Context-independent parameters for every span in a finite input batch."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
|
| 7 |
-
import torch
|
| 8 |
-
from torch import nn
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
SpanKeys = tuple[
|
| 12 |
-
tuple[tuple[tuple[int, ...] | None, ...], ...], ...
|
| 13 |
-
]
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
@dataclass(frozen=True)
|
| 17 |
-
class SpanGeneratorOutput:
|
| 18 |
-
scores: torch.Tensor
|
| 19 |
-
residual_codes: torch.Tensor | None
|
| 20 |
-
mask: torch.Tensor
|
| 21 |
-
keys: SpanKeys | None
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def _validate_atoms(
|
| 25 |
-
atom_ids: torch.Tensor, lengths: torch.Tensor
|
| 26 |
-
) -> tuple[int, int]:
|
| 27 |
-
if atom_ids.ndim != 2 or atom_ids.dtype not in (torch.int32, torch.int64):
|
| 28 |
-
raise ValueError("atom_ids must be a rank-2 integer tensor")
|
| 29 |
-
batch, atom_count = atom_ids.shape
|
| 30 |
-
if lengths.shape != (batch,) or lengths.dtype not in (torch.int32, torch.int64):
|
| 31 |
-
raise ValueError("lengths must contain one integer length per sentence")
|
| 32 |
-
if atom_ids.device != lengths.device:
|
| 33 |
-
raise ValueError("atom_ids and lengths must use the same device")
|
| 34 |
-
if bool(((lengths < 1) | (lengths > atom_count)).any()):
|
| 35 |
-
raise ValueError("every sentence needs between one and atom_count atoms")
|
| 36 |
-
if bool((atom_ids < 0).any()):
|
| 37 |
-
raise ValueError("atom ids must be nonnegative")
|
| 38 |
-
return batch, atom_count
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def exact_span_keys(atom_ids: torch.Tensor, lengths: torch.Tensor) -> SpanKeys:
|
| 42 |
-
"""Return the exact atom tuple for every valid span and ``None`` elsewhere."""
|
| 43 |
-
|
| 44 |
-
batch, atom_count = _validate_atoms(atom_ids, lengths)
|
| 45 |
-
atoms = atom_ids.detach().to(device="cpu", dtype=torch.long)
|
| 46 |
-
cpu_lengths = lengths.detach().to(device="cpu", dtype=torch.long)
|
| 47 |
-
boundaries = atom_count + 1
|
| 48 |
-
sentences: list[tuple[tuple[tuple[int, ...] | None, ...], ...]] = []
|
| 49 |
-
for sentence in range(batch):
|
| 50 |
-
length = int(cpu_lengths[sentence])
|
| 51 |
-
rows: list[tuple[tuple[int, ...] | None, ...]] = []
|
| 52 |
-
for start in range(boundaries):
|
| 53 |
-
columns: list[tuple[int, ...] | None] = []
|
| 54 |
-
for end in range(boundaries):
|
| 55 |
-
if start < end <= length:
|
| 56 |
-
columns.append(tuple(int(value) for value in atoms[sentence, start:end]))
|
| 57 |
-
else:
|
| 58 |
-
columns.append(None)
|
| 59 |
-
rows.append(tuple(columns))
|
| 60 |
-
sentences.append(tuple(rows))
|
| 61 |
-
return tuple(sentences)
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
class ContextFreeSpanGenerator(nn.Module):
|
| 65 |
-
"""Score and encode all contiguous atom tuples with shared parameters."""
|
| 66 |
-
|
| 67 |
-
def __init__(
|
| 68 |
-
self,
|
| 69 |
-
atom_vocab_size: int,
|
| 70 |
-
atom_dim: int,
|
| 71 |
-
hidden_dim: int,
|
| 72 |
-
residual_rank: int,
|
| 73 |
-
*,
|
| 74 |
-
initial_non_atomic_score: float = -2.0,
|
| 75 |
-
recurrent_gradient_horizon: int | None = None,
|
| 76 |
-
) -> None:
|
| 77 |
-
super().__init__()
|
| 78 |
-
if min(atom_vocab_size, atom_dim, hidden_dim, residual_rank) <= 0:
|
| 79 |
-
raise ValueError("span generator dimensions must be positive")
|
| 80 |
-
if not torch.isfinite(torch.tensor(initial_non_atomic_score)):
|
| 81 |
-
raise ValueError("initial non-atomic score must be finite")
|
| 82 |
-
if recurrent_gradient_horizon is not None and (
|
| 83 |
-
isinstance(recurrent_gradient_horizon, bool)
|
| 84 |
-
or recurrent_gradient_horizon <= 0
|
| 85 |
-
):
|
| 86 |
-
raise ValueError("recurrent gradient horizon must be positive")
|
| 87 |
-
self.atom_vocab_size = int(atom_vocab_size)
|
| 88 |
-
self.atom_dim = int(atom_dim)
|
| 89 |
-
self.hidden_dim = int(hidden_dim)
|
| 90 |
-
self.residual_rank = int(residual_rank)
|
| 91 |
-
self.initial_non_atomic_score = float(initial_non_atomic_score)
|
| 92 |
-
self.recurrent_gradient_horizon = recurrent_gradient_horizon
|
| 93 |
-
|
| 94 |
-
self.atom_embedding = nn.Embedding(self.atom_vocab_size, self.atom_dim)
|
| 95 |
-
self.recurrence = nn.GRUCell(self.atom_dim, self.hidden_dim)
|
| 96 |
-
self.score_head = nn.Linear(self.hidden_dim, 1)
|
| 97 |
-
self.residual_head = nn.Linear(self.hidden_dim, self.residual_rank)
|
| 98 |
-
nn.init.zeros_(self.score_head.weight)
|
| 99 |
-
nn.init.zeros_(self.score_head.bias)
|
| 100 |
-
nn.init.zeros_(self.residual_head.weight)
|
| 101 |
-
nn.init.zeros_(self.residual_head.bias)
|
| 102 |
-
|
| 103 |
-
def encode_non_atomic_sequences(
|
| 104 |
-
self,
|
| 105 |
-
atom_ids: torch.Tensor,
|
| 106 |
-
lengths: torch.Tensor,
|
| 107 |
-
) -> torch.Tensor:
|
| 108 |
-
"""Encode each complete multi-atom tuple without materializing subspans."""
|
| 109 |
-
|
| 110 |
-
batch, atom_count = _validate_atoms(atom_ids, lengths)
|
| 111 |
-
if bool((lengths < 2).any()):
|
| 112 |
-
raise ValueError("direct span encoding requires non-atomic sequences")
|
| 113 |
-
if atom_ids.numel() and int(atom_ids.max()) >= self.atom_vocab_size:
|
| 114 |
-
raise ValueError("atom id is outside the span generator vocabulary")
|
| 115 |
-
embedded = self.atom_embedding(atom_ids.to(torch.long))
|
| 116 |
-
hidden = embedded.new_zeros(batch, self.hidden_dim)
|
| 117 |
-
for position in range(atom_count):
|
| 118 |
-
updated = self.recurrence(embedded[:, position], hidden)
|
| 119 |
-
active = position < lengths
|
| 120 |
-
hidden = torch.where(active.unsqueeze(1), updated, hidden)
|
| 121 |
-
codes = self.residual_head(hidden)
|
| 122 |
-
if not bool(torch.isfinite(codes).all()):
|
| 123 |
-
raise FloatingPointError("nonfinite direct open-lexicon residual code")
|
| 124 |
-
return codes
|
| 125 |
-
|
| 126 |
-
def forward(
|
| 127 |
-
self,
|
| 128 |
-
atom_ids: torch.Tensor,
|
| 129 |
-
lengths: torch.Tensor,
|
| 130 |
-
*,
|
| 131 |
-
include_keys: bool = False,
|
| 132 |
-
emit_residual_codes: bool = True,
|
| 133 |
-
residual_mask: torch.Tensor | None = None,
|
| 134 |
-
) -> SpanGeneratorOutput:
|
| 135 |
-
batch, atom_count = _validate_atoms(atom_ids, lengths)
|
| 136 |
-
if atom_ids.numel() and int(atom_ids.max()) >= self.atom_vocab_size:
|
| 137 |
-
raise ValueError("atom id is outside the span generator vocabulary")
|
| 138 |
-
boundaries = atom_count + 1
|
| 139 |
-
if residual_mask is not None:
|
| 140 |
-
if not emit_residual_codes:
|
| 141 |
-
raise ValueError("residual mask requires emitted residual codes")
|
| 142 |
-
if (
|
| 143 |
-
residual_mask.shape != (batch, boundaries, boundaries)
|
| 144 |
-
or residual_mask.dtype is not torch.bool
|
| 145 |
-
or residual_mask.device != atom_ids.device
|
| 146 |
-
):
|
| 147 |
-
raise ValueError(
|
| 148 |
-
"residual mask must be boolean and align with the span lattice"
|
| 149 |
-
)
|
| 150 |
-
positions = torch.arange(atom_count, device=atom_ids.device)
|
| 151 |
-
if bool(residual_mask[:, positions, positions + 1].any()):
|
| 152 |
-
raise ValueError("residual mask may not enable atomic fallback edges")
|
| 153 |
-
embedded = self.atom_embedding(atom_ids.to(torch.long))
|
| 154 |
-
scores = torch.zeros(
|
| 155 |
-
batch, boundaries, boundaries, dtype=torch.float32, device=atom_ids.device
|
| 156 |
-
)
|
| 157 |
-
codes = (
|
| 158 |
-
embedded.new_zeros(batch, boundaries, boundaries, self.residual_rank)
|
| 159 |
-
if emit_residual_codes
|
| 160 |
-
else None
|
| 161 |
-
)
|
| 162 |
-
mask = torch.zeros(
|
| 163 |
-
batch, boundaries, boundaries, dtype=torch.bool, device=atom_ids.device
|
| 164 |
-
)
|
| 165 |
-
|
| 166 |
-
previous: torch.Tensor | None = None
|
| 167 |
-
for span_length in range(1, atom_count + 1):
|
| 168 |
-
span_count = atom_count - span_length + 1
|
| 169 |
-
if previous is None:
|
| 170 |
-
prior = embedded.new_zeros(batch, span_count, self.hidden_dim)
|
| 171 |
-
else:
|
| 172 |
-
prior = previous[:, :span_count]
|
| 173 |
-
if (
|
| 174 |
-
self.recurrent_gradient_horizon is not None
|
| 175 |
-
and (span_length - 1) % self.recurrent_gradient_horizon == 0
|
| 176 |
-
):
|
| 177 |
-
prior = prior.detach()
|
| 178 |
-
next_atoms = embedded[:, span_length - 1 :]
|
| 179 |
-
hidden = self.recurrence(
|
| 180 |
-
next_atoms.reshape(batch * span_count, self.atom_dim),
|
| 181 |
-
prior.reshape(batch * span_count, self.hidden_dim),
|
| 182 |
-
).reshape(batch, span_count, self.hidden_dim)
|
| 183 |
-
previous = hidden
|
| 184 |
-
starts = torch.arange(span_count, device=atom_ids.device)
|
| 185 |
-
ends = starts + span_length
|
| 186 |
-
valid = ends.unsqueeze(0) <= lengths.unsqueeze(1)
|
| 187 |
-
learned_score = self.score_head(hidden).squeeze(-1).float()
|
| 188 |
-
if span_length > 1:
|
| 189 |
-
learned_score = learned_score + self.initial_non_atomic_score
|
| 190 |
-
scores[:, starts, ends] = torch.where(
|
| 191 |
-
valid, learned_score, torch.zeros_like(learned_score)
|
| 192 |
-
)
|
| 193 |
-
if codes is not None:
|
| 194 |
-
residual = hidden.new_zeros(
|
| 195 |
-
batch, span_count, self.residual_rank
|
| 196 |
-
)
|
| 197 |
-
if span_length > 1:
|
| 198 |
-
if residual_mask is None:
|
| 199 |
-
residual = self.residual_head(hidden)
|
| 200 |
-
else:
|
| 201 |
-
supported = residual_mask[:, starts, ends] & valid
|
| 202 |
-
residual[supported] = self.residual_head(hidden[supported])
|
| 203 |
-
codes[:, starts, ends] = torch.where(
|
| 204 |
-
valid.unsqueeze(2), residual, torch.zeros_like(residual)
|
| 205 |
-
)
|
| 206 |
-
mask[:, starts, ends] = valid
|
| 207 |
-
|
| 208 |
-
if residual_mask is not None and bool((residual_mask & ~mask).any()):
|
| 209 |
-
raise ValueError("residual mask may enable only legal spans")
|
| 210 |
-
if not bool(torch.isfinite(scores).all()):
|
| 211 |
-
raise FloatingPointError("nonfinite open-lexicon span score")
|
| 212 |
-
if codes is not None and not bool(torch.isfinite(codes).all()):
|
| 213 |
-
raise FloatingPointError("nonfinite open-lexicon residual code")
|
| 214 |
-
keys = exact_span_keys(atom_ids, lengths) if include_keys else None
|
| 215 |
-
return SpanGeneratorOutput(scores, codes, mask, keys)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/openlex_tokenizer.py
DELETED
|
@@ -1,180 +0,0 @@
|
|
| 1 |
-
"""Canonical terminal decomposition for BPE-backed OpenLex lattices."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass, field
|
| 6 |
-
from typing import Mapping, Sequence
|
| 7 |
-
|
| 8 |
-
import torch
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
_TOKEN_ROW = -1
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
@dataclass(frozen=True)
|
| 15 |
-
class CanonicalBatch:
|
| 16 |
-
"""One old-token batch expanded into exact terminal atoms."""
|
| 17 |
-
|
| 18 |
-
atom_ids: torch.Tensor
|
| 19 |
-
lengths: torch.Tensor
|
| 20 |
-
preferred_edges: torch.Tensor
|
| 21 |
-
override_ids: torch.Tensor
|
| 22 |
-
|
| 23 |
-
def index_select(self, indices: Sequence[int] | torch.Tensor) -> "CanonicalBatch":
|
| 24 |
-
if isinstance(indices, torch.Tensor):
|
| 25 |
-
rows = indices.to(device=self.atom_ids.device, dtype=torch.long)
|
| 26 |
-
else:
|
| 27 |
-
rows = torch.tensor(tuple(indices), device=self.atom_ids.device, dtype=torch.long)
|
| 28 |
-
return CanonicalBatch(
|
| 29 |
-
self.atom_ids.index_select(0, rows),
|
| 30 |
-
self.lengths.index_select(0, rows),
|
| 31 |
-
self.preferred_edges.index_select(0, rows),
|
| 32 |
-
self.override_ids.index_select(0, rows),
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
@dataclass(frozen=True)
|
| 37 |
-
class CanonicalVocabulary:
|
| 38 |
-
"""Exact BPE-token spellings over recursively undecomposable terminals."""
|
| 39 |
-
|
| 40 |
-
spellings: tuple[tuple[int, ...], ...]
|
| 41 |
-
atomic_ids: tuple[int, ...]
|
| 42 |
-
_trie: dict[int, object] = field(repr=False, compare=False)
|
| 43 |
-
|
| 44 |
-
@classmethod
|
| 45 |
-
def from_model_json(
|
| 46 |
-
cls,
|
| 47 |
-
model_json: Mapping[str, object],
|
| 48 |
-
*,
|
| 49 |
-
vocabulary_size: int,
|
| 50 |
-
) -> "CanonicalVocabulary":
|
| 51 |
-
raw_vocab = model_json.get("vocab")
|
| 52 |
-
raw_merges = model_json.get("merges")
|
| 53 |
-
if not isinstance(raw_vocab, Mapping) or not isinstance(raw_merges, Sequence):
|
| 54 |
-
raise ValueError("canonical vocabulary requires BPE vocab and merges")
|
| 55 |
-
vocab: dict[str, int] = {}
|
| 56 |
-
for token, raw_id in raw_vocab.items():
|
| 57 |
-
if not isinstance(token, str) or not isinstance(raw_id, int):
|
| 58 |
-
raise ValueError("BPE vocabulary entries must map strings to integers")
|
| 59 |
-
vocab[token] = raw_id
|
| 60 |
-
if not vocab or min(vocab.values()) < 0 or max(vocab.values()) >= vocabulary_size:
|
| 61 |
-
raise ValueError("BPE vocabulary ids lie outside the embedding table")
|
| 62 |
-
|
| 63 |
-
children: dict[int, tuple[int, int]] = {}
|
| 64 |
-
alternatives: list[tuple[int, tuple[int, int]]] = []
|
| 65 |
-
for raw_merge in raw_merges:
|
| 66 |
-
if (
|
| 67 |
-
not isinstance(raw_merge, Sequence)
|
| 68 |
-
or isinstance(raw_merge, (str, bytes))
|
| 69 |
-
or len(raw_merge) != 2
|
| 70 |
-
or not all(isinstance(part, str) for part in raw_merge)
|
| 71 |
-
):
|
| 72 |
-
raise ValueError("every BPE merge must contain two token strings")
|
| 73 |
-
left, right = raw_merge
|
| 74 |
-
result = left + right
|
| 75 |
-
if left not in vocab or right not in vocab or result not in vocab:
|
| 76 |
-
continue
|
| 77 |
-
row = vocab[result]
|
| 78 |
-
pair = (vocab[left], vocab[right])
|
| 79 |
-
previous = children.setdefault(row, pair)
|
| 80 |
-
if previous != pair:
|
| 81 |
-
alternatives.append((row, pair))
|
| 82 |
-
|
| 83 |
-
memo: dict[int, tuple[int, ...]] = {}
|
| 84 |
-
visiting: set[int] = set()
|
| 85 |
-
|
| 86 |
-
def expand(row: int) -> tuple[int, ...]:
|
| 87 |
-
if row in memo:
|
| 88 |
-
return memo[row]
|
| 89 |
-
if row in visiting:
|
| 90 |
-
raise ValueError("BPE merge graph contains a cycle")
|
| 91 |
-
pair = children.get(row)
|
| 92 |
-
if pair is None:
|
| 93 |
-
result = (row,)
|
| 94 |
-
else:
|
| 95 |
-
visiting.add(row)
|
| 96 |
-
result = expand(pair[0]) + expand(pair[1])
|
| 97 |
-
visiting.remove(row)
|
| 98 |
-
memo[row] = result
|
| 99 |
-
return result
|
| 100 |
-
|
| 101 |
-
spellings = tuple(expand(row) for row in range(vocabulary_size))
|
| 102 |
-
for row, pair in alternatives:
|
| 103 |
-
if expand(pair[0]) + expand(pair[1]) != spellings[row]:
|
| 104 |
-
raise ValueError("BPE token has inconsistent terminal spellings")
|
| 105 |
-
atomic_ids = tuple(row for row, spelling in enumerate(spellings) if spelling == (row,))
|
| 106 |
-
trie: dict[int, object] = {}
|
| 107 |
-
for row, spelling in enumerate(spellings):
|
| 108 |
-
node = trie
|
| 109 |
-
for atom in spelling:
|
| 110 |
-
child = node.setdefault(atom, {})
|
| 111 |
-
if not isinstance(child, dict):
|
| 112 |
-
raise RuntimeError("invalid canonical vocabulary trie")
|
| 113 |
-
node = child
|
| 114 |
-
previous = node.setdefault(_TOKEN_ROW, row)
|
| 115 |
-
if previous != row:
|
| 116 |
-
raise ValueError("two embedding rows have the same terminal spelling")
|
| 117 |
-
return cls(spellings, atomic_ids, trie)
|
| 118 |
-
|
| 119 |
-
def canonicalize(
|
| 120 |
-
self,
|
| 121 |
-
token_ids: torch.Tensor,
|
| 122 |
-
lengths: torch.Tensor,
|
| 123 |
-
) -> CanonicalBatch:
|
| 124 |
-
"""Expand a padded token batch and register every known exact span."""
|
| 125 |
-
|
| 126 |
-
if token_ids.ndim != 2 or token_ids.dtype not in (torch.int32, torch.int64):
|
| 127 |
-
raise ValueError("token_ids must be a rank-2 integer tensor")
|
| 128 |
-
batch, token_count = token_ids.shape
|
| 129 |
-
if lengths.shape != (batch,) or lengths.dtype not in (torch.int32, torch.int64):
|
| 130 |
-
raise ValueError("lengths must contain one integer per token sequence")
|
| 131 |
-
if token_ids.device != lengths.device:
|
| 132 |
-
raise ValueError("token_ids and lengths must use the same device")
|
| 133 |
-
if bool(((lengths < 1) | (lengths > token_count)).any()):
|
| 134 |
-
raise ValueError("every old-token sequence must be nonempty")
|
| 135 |
-
if bool(((token_ids < 0) | (token_ids >= len(self.spellings))).any()):
|
| 136 |
-
raise ValueError("old token id lies outside the canonical vocabulary")
|
| 137 |
-
|
| 138 |
-
cpu_tokens = token_ids.detach().to(device="cpu", dtype=torch.long)
|
| 139 |
-
cpu_lengths = lengths.detach().to(device="cpu", dtype=torch.long)
|
| 140 |
-
sentences: list[list[int]] = []
|
| 141 |
-
old_edges: list[list[tuple[int, int]]] = []
|
| 142 |
-
maximum = 0
|
| 143 |
-
for sentence in range(batch):
|
| 144 |
-
atoms: list[int] = []
|
| 145 |
-
edges: list[tuple[int, int]] = []
|
| 146 |
-
for token in cpu_tokens[sentence, : int(cpu_lengths[sentence])].tolist():
|
| 147 |
-
start = len(atoms)
|
| 148 |
-
atoms.extend(self.spellings[int(token)])
|
| 149 |
-
edges.append((start, len(atoms)))
|
| 150 |
-
sentences.append(atoms)
|
| 151 |
-
old_edges.append(edges)
|
| 152 |
-
maximum = max(maximum, len(atoms))
|
| 153 |
-
if maximum == 0:
|
| 154 |
-
raise ValueError("canonical expansion produced an empty sentence")
|
| 155 |
-
|
| 156 |
-
atom_ids = torch.zeros(batch, maximum, dtype=torch.long)
|
| 157 |
-
atom_lengths = torch.tensor([len(row) for row in sentences], dtype=torch.long)
|
| 158 |
-
preferred = torch.zeros(batch, maximum + 1, maximum + 1, dtype=torch.bool)
|
| 159 |
-
overrides = torch.full((batch, maximum + 1, maximum + 1), -1, dtype=torch.long)
|
| 160 |
-
for sentence, atoms in enumerate(sentences):
|
| 161 |
-
atom_ids[sentence, : len(atoms)] = torch.tensor(atoms, dtype=torch.long)
|
| 162 |
-
for start, end in old_edges[sentence]:
|
| 163 |
-
preferred[sentence, start, end] = True
|
| 164 |
-
for start in range(len(atoms)):
|
| 165 |
-
node = self._trie
|
| 166 |
-
for end in range(start + 1, len(atoms) + 1):
|
| 167 |
-
child = node.get(atoms[end - 1])
|
| 168 |
-
if not isinstance(child, dict):
|
| 169 |
-
break
|
| 170 |
-
node = child
|
| 171 |
-
row = node.get(_TOKEN_ROW)
|
| 172 |
-
if isinstance(row, int):
|
| 173 |
-
overrides[sentence, start, end] = row
|
| 174 |
-
device = token_ids.device
|
| 175 |
-
return CanonicalBatch(
|
| 176 |
-
atom_ids.to(device),
|
| 177 |
-
atom_lengths.to(device),
|
| 178 |
-
preferred.to(device),
|
| 179 |
-
overrides.to(device),
|
| 180 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/progressive_model.py
DELETED
|
@@ -1,395 +0,0 @@
|
|
| 1 |
-
"""Sentence-Transformers module for growing StaRSE one ModernBERT block at a time."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import copy
|
| 6 |
-
import json
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Any
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
from torch import nn
|
| 12 |
-
|
| 13 |
-
from sentence_transformers.sentence_transformer.modules import Module
|
| 14 |
-
from tokenizers import Tokenizer
|
| 15 |
-
from transformers import ModernBertConfig, ModernBertModel
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
class GatedModernBertLayer(nn.Module):
|
| 19 |
-
"""Interpolate between an identity mapping and one pretrained encoder block."""
|
| 20 |
-
|
| 21 |
-
def __init__(
|
| 22 |
-
self,
|
| 23 |
-
block: nn.Module,
|
| 24 |
-
*,
|
| 25 |
-
attention_type: str | None = None,
|
| 26 |
-
initial_gate: float = 0.0,
|
| 27 |
-
) -> None:
|
| 28 |
-
super().__init__()
|
| 29 |
-
self.block = block
|
| 30 |
-
self.gate = nn.Parameter(torch.tensor(float(initial_gate), dtype=torch.float32))
|
| 31 |
-
self.attention_type = str(attention_type or getattr(block, "attention_type", "full_attention"))
|
| 32 |
-
|
| 33 |
-
def forward(
|
| 34 |
-
self,
|
| 35 |
-
hidden_states: torch.Tensor,
|
| 36 |
-
attention_mask: torch.Tensor | None = None,
|
| 37 |
-
position_embeddings: torch.Tensor | None = None,
|
| 38 |
-
**kwargs: Any,
|
| 39 |
-
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
| 40 |
-
block_kwargs = {**kwargs, "attention_mask": attention_mask}
|
| 41 |
-
if position_embeddings is not None:
|
| 42 |
-
block_kwargs["position_embeddings"] = position_embeddings
|
| 43 |
-
block_output = self.block(hidden_states, **block_kwargs)
|
| 44 |
-
transformed = block_output[0] if isinstance(block_output, tuple) else block_output
|
| 45 |
-
gate = self.gate.to(dtype=hidden_states.dtype)
|
| 46 |
-
output = hidden_states + gate * (transformed - hidden_states)
|
| 47 |
-
if isinstance(block_output, tuple):
|
| 48 |
-
return (output, *block_output[1:])
|
| 49 |
-
return output
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def _public_modernbert_config(config: ModernBertConfig) -> dict[str, Any]:
|
| 53 |
-
"""Return a self-contained architecture config without a local source path."""
|
| 54 |
-
|
| 55 |
-
payload = config.to_dict()
|
| 56 |
-
payload.pop("_name_or_path", None)
|
| 57 |
-
payload.pop("name_or_path", None)
|
| 58 |
-
rope_parameters = payload.get("rope_parameters")
|
| 59 |
-
if isinstance(rope_parameters, dict) and "full_attention" in rope_parameters:
|
| 60 |
-
payload["rope_parameters"] = {
|
| 61 |
-
key: rope_parameters[key] for key in ("sliding_attention", "full_attention") if key in rope_parameters
|
| 62 |
-
}
|
| 63 |
-
payload["local_rope_theta"] = float(payload["rope_parameters"]["sliding_attention"]["rope_theta"])
|
| 64 |
-
payload["global_rope_theta"] = float(payload["rope_parameters"]["full_attention"]["rope_theta"])
|
| 65 |
-
payload["vocab_size"] = 1
|
| 66 |
-
for token_id_key in ("pad_token_id", "bos_token_id", "eos_token_id", "cls_token_id", "sep_token_id"):
|
| 67 |
-
payload[token_id_key] = 0
|
| 68 |
-
return payload
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def _pca_project_to_donor_space(
|
| 72 |
-
base_weights: torch.Tensor,
|
| 73 |
-
donor_weights: torch.Tensor,
|
| 74 |
-
*,
|
| 75 |
-
target_dim: int,
|
| 76 |
-
device: str,
|
| 77 |
-
) -> torch.Tensor:
|
| 78 |
-
"""Reduce StaRSE to ``target_dim`` and rotate its PCA axes into donor space.
|
| 79 |
-
|
| 80 |
-
PCA supplies the fixed 512→384 dimensionality reduction. Its component
|
| 81 |
-
signs and rotations are arbitrary, so an orthogonal Procrustes rotation is
|
| 82 |
-
fitted from the token-aligned USER2 input table. The rotation has no bias,
|
| 83 |
-
no trainable parameters, and preserves all PCA-space Euclidean distances.
|
| 84 |
-
"""
|
| 85 |
-
|
| 86 |
-
if base_weights.ndim != 2 or donor_weights.ndim != 2:
|
| 87 |
-
raise ValueError("Embedding tables must be rank-2")
|
| 88 |
-
if base_weights.shape[0] != donor_weights.shape[0]:
|
| 89 |
-
raise ValueError("StaRSE and donor tables must contain the same aligned token rows")
|
| 90 |
-
if target_dim > base_weights.shape[1] or target_dim != donor_weights.shape[1]:
|
| 91 |
-
raise ValueError("PCA target dimension must equal donor hidden size and not exceed StaRSE dimension")
|
| 92 |
-
|
| 93 |
-
fit_device = torch.device(device)
|
| 94 |
-
source = base_weights.to(fit_device, dtype=torch.float32)
|
| 95 |
-
target = donor_weights.to(fit_device, dtype=torch.float32)
|
| 96 |
-
source_centered = source - source.mean(dim=0, keepdim=True)
|
| 97 |
-
covariance = source_centered.T @ source_centered / float(source_centered.shape[0])
|
| 98 |
-
_, eigenvectors = torch.linalg.eigh(covariance)
|
| 99 |
-
components = eigenvectors[:, -target_dim:]
|
| 100 |
-
reduced = source_centered @ components
|
| 101 |
-
|
| 102 |
-
target_centered = target - target.mean(dim=0, keepdim=True)
|
| 103 |
-
left, _, right_t = torch.linalg.svd(reduced.T @ target_centered, full_matrices=False)
|
| 104 |
-
rotation = left @ right_t
|
| 105 |
-
return (reduced @ rotation).contiguous().cpu()
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def _load_shape_compatible(target: nn.Module, source: nn.Module) -> None:
|
| 109 |
-
"""Copy matching tensors while intentionally skipping the donor token table."""
|
| 110 |
-
|
| 111 |
-
target_state = target.state_dict()
|
| 112 |
-
compatible = {
|
| 113 |
-
key: value
|
| 114 |
-
for key, value in source.state_dict().items()
|
| 115 |
-
if key in target_state and target_state[key].shape == value.shape
|
| 116 |
-
}
|
| 117 |
-
target.load_state_dict(compatible, strict=False)
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def _load_donor(model_name_or_path: str) -> ModernBertModel:
|
| 121 |
-
"""Load in FP32 across both Transformers 4.x and 5.x APIs."""
|
| 122 |
-
|
| 123 |
-
from transformers import __version__ as transformers_version
|
| 124 |
-
|
| 125 |
-
major_version = int(transformers_version.split(".", maxsplit=1)[0])
|
| 126 |
-
dtype_kwargs = {"dtype": torch.float32} if major_version >= 5 else {"torch_dtype": torch.float32}
|
| 127 |
-
return ModernBertModel.from_pretrained(model_name_or_path, **dtype_kwargs)
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
def _donor_layer_types(config: ModernBertConfig) -> list[str]:
|
| 131 |
-
layer_types = getattr(config, "layer_types", None)
|
| 132 |
-
if layer_types is not None:
|
| 133 |
-
return list(layer_types)
|
| 134 |
-
full_every = int(getattr(config, "global_attn_every_n_layers", 3))
|
| 135 |
-
return [
|
| 136 |
-
"full_attention" if layer_index % full_every == 0 else "sliding_attention"
|
| 137 |
-
for layer_index in range(int(config.num_hidden_layers))
|
| 138 |
-
]
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
class ProgressiveEncoder(Module):
|
| 142 |
-
"""PCA-reduced frozen StaRSE table with progressively grown USER2 blocks."""
|
| 143 |
-
|
| 144 |
-
config_file_name = "progressive_config.json"
|
| 145 |
-
|
| 146 |
-
def __init__(
|
| 147 |
-
self,
|
| 148 |
-
*,
|
| 149 |
-
tokenizer: Tokenizer,
|
| 150 |
-
base_weights: torch.Tensor,
|
| 151 |
-
donor_config: dict[str, Any],
|
| 152 |
-
source_layer_types: list[str],
|
| 153 |
-
active_depth: int,
|
| 154 |
-
max_seq_length: int = 128,
|
| 155 |
-
) -> None:
|
| 156 |
-
super().__init__()
|
| 157 |
-
if base_weights.ndim != 2:
|
| 158 |
-
raise ValueError("base_weights must have shape [vocab_size, embedding_dimension]")
|
| 159 |
-
if active_depth < 1 or active_depth > len(source_layer_types):
|
| 160 |
-
raise ValueError("active_depth must be between 1 and the donor layer count")
|
| 161 |
-
|
| 162 |
-
self.tokenizer = tokenizer
|
| 163 |
-
self.tokenizer.no_padding()
|
| 164 |
-
self.vocab_size, self.embedding_dimension = map(int, base_weights.shape)
|
| 165 |
-
self.max_seq_length = int(max_seq_length)
|
| 166 |
-
self.active_depth = int(active_depth)
|
| 167 |
-
self.source_layer_types = list(source_layer_types)
|
| 168 |
-
self.donor_config = dict(donor_config)
|
| 169 |
-
|
| 170 |
-
self.base_embedding = nn.Embedding.from_pretrained(base_weights, freeze=True)
|
| 171 |
-
hidden_size = int(self.donor_config["hidden_size"])
|
| 172 |
-
if self.embedding_dimension != hidden_size:
|
| 173 |
-
raise ValueError("PCA-reduced StaRSE dimension must equal donor hidden size")
|
| 174 |
-
self.output_gate = nn.Parameter(torch.tensor(0.0, dtype=torch.float32))
|
| 175 |
-
|
| 176 |
-
active_config_payload = dict(self.donor_config)
|
| 177 |
-
active_config_payload["vocab_size"] = 1
|
| 178 |
-
active_config_payload["num_hidden_layers"] = self.active_depth
|
| 179 |
-
active_config_payload["layer_types"] = self.source_layer_types[: self.active_depth]
|
| 180 |
-
for token_id_key in ("pad_token_id", "bos_token_id", "eos_token_id", "cls_token_id", "sep_token_id"):
|
| 181 |
-
active_config_payload[token_id_key] = 0
|
| 182 |
-
rope_parameters = active_config_payload.pop("rope_parameters", None)
|
| 183 |
-
backbone_config = ModernBertConfig.from_dict(active_config_payload)
|
| 184 |
-
if rope_parameters is not None:
|
| 185 |
-
backbone_config.rope_parameters = rope_parameters
|
| 186 |
-
self.backbone = ModernBertModel(backbone_config)
|
| 187 |
-
self.backbone.layers = nn.ModuleList(
|
| 188 |
-
GatedModernBertLayer(layer, attention_type=self.source_layer_types[index])
|
| 189 |
-
for index, layer in enumerate(self.backbone.layers)
|
| 190 |
-
)
|
| 191 |
-
self.freeze_for_stage()
|
| 192 |
-
|
| 193 |
-
@classmethod
|
| 194 |
-
def initialize_from_models(
|
| 195 |
-
cls,
|
| 196 |
-
*,
|
| 197 |
-
base_model_name: str,
|
| 198 |
-
donor_model_path: str,
|
| 199 |
-
max_seq_length: int = 128,
|
| 200 |
-
pca_components: int = 384,
|
| 201 |
-
pca_device: str = "cpu",
|
| 202 |
-
) -> "ProgressiveEncoder":
|
| 203 |
-
"""Build stage 1 from PCA-384 StaRSE and token-aligned USER2 blocks."""
|
| 204 |
-
|
| 205 |
-
from sentence_transformers import SentenceTransformer
|
| 206 |
-
from transformers import AutoTokenizer
|
| 207 |
-
|
| 208 |
-
base_model = SentenceTransformer(base_model_name, device="cpu", trust_remote_code=True)
|
| 209 |
-
base_module = base_model[0]
|
| 210 |
-
if not hasattr(base_module, "embedding") or not hasattr(base_module, "tokenizer"):
|
| 211 |
-
raise TypeError("The base model must expose a static embedding table and tokenizer")
|
| 212 |
-
base_weights = base_module.embedding.weight.detach().float().cpu()
|
| 213 |
-
tokenizer = base_module.tokenizer
|
| 214 |
-
if not isinstance(tokenizer, Tokenizer):
|
| 215 |
-
tokenizer = tokenizer._tokenizer
|
| 216 |
-
|
| 217 |
-
donor = _load_donor(donor_model_path)
|
| 218 |
-
donor_tokenizer = AutoTokenizer.from_pretrained(donor_model_path, use_fast=True)
|
| 219 |
-
donor_vocab = donor_tokenizer.get_vocab()
|
| 220 |
-
base_vocab = tokenizer.get_vocab()
|
| 221 |
-
mismatches = [token for token, token_id in base_vocab.items() if donor_vocab.get(token) != token_id]
|
| 222 |
-
if mismatches:
|
| 223 |
-
examples = ", ".join(repr(token) for token in mismatches[:5])
|
| 224 |
-
raise ValueError(f"Base and donor token IDs differ for {len(mismatches)} tokens: {examples}")
|
| 225 |
-
if max(base_vocab.values()) >= donor.get_input_embeddings().weight.shape[0]:
|
| 226 |
-
raise ValueError("The donor embedding table does not cover the complete StaRSE vocabulary")
|
| 227 |
-
|
| 228 |
-
donor_weights = donor.get_input_embeddings().weight.detach()[: base_weights.shape[0]].float().cpu()
|
| 229 |
-
reduced_weights = _pca_project_to_donor_space(
|
| 230 |
-
base_weights,
|
| 231 |
-
donor_weights,
|
| 232 |
-
target_dim=int(pca_components),
|
| 233 |
-
device=pca_device,
|
| 234 |
-
)
|
| 235 |
-
|
| 236 |
-
source_layer_types = _donor_layer_types(donor.config)
|
| 237 |
-
module = cls(
|
| 238 |
-
tokenizer=tokenizer,
|
| 239 |
-
base_weights=reduced_weights,
|
| 240 |
-
donor_config=_public_modernbert_config(donor.config),
|
| 241 |
-
source_layer_types=source_layer_types,
|
| 242 |
-
active_depth=1,
|
| 243 |
-
max_seq_length=max_seq_length,
|
| 244 |
-
)
|
| 245 |
-
_load_shape_compatible(module.backbone.embeddings, donor.embeddings)
|
| 246 |
-
module.backbone.final_norm.load_state_dict(donor.final_norm.state_dict())
|
| 247 |
-
module.backbone.layers[0] = GatedModernBertLayer(
|
| 248 |
-
copy.deepcopy(donor.layers[0]),
|
| 249 |
-
attention_type=source_layer_types[0],
|
| 250 |
-
)
|
| 251 |
-
with torch.no_grad():
|
| 252 |
-
module.output_gate.zero_()
|
| 253 |
-
module.freeze_for_stage()
|
| 254 |
-
return module
|
| 255 |
-
|
| 256 |
-
def add_donor_layer(self, donor_model_path: str) -> None:
|
| 257 |
-
"""Append the next pretrained donor block with an identity gate."""
|
| 258 |
-
|
| 259 |
-
if self.active_depth >= len(self.source_layer_types):
|
| 260 |
-
raise ValueError("All donor layers have already been added")
|
| 261 |
-
donor = _load_donor(donor_model_path)
|
| 262 |
-
if int(donor.config.hidden_size) != int(self.donor_config["hidden_size"]):
|
| 263 |
-
raise ValueError("The donor hidden size differs from the initialized progressive model")
|
| 264 |
-
if _donor_layer_types(donor.config) != self.source_layer_types:
|
| 265 |
-
raise ValueError("The donor attention layout differs from the initialized progressive model")
|
| 266 |
-
layer_index = self.active_depth
|
| 267 |
-
self.backbone.layers.append(
|
| 268 |
-
GatedModernBertLayer(
|
| 269 |
-
copy.deepcopy(donor.layers[layer_index]),
|
| 270 |
-
attention_type=self.source_layer_types[layer_index],
|
| 271 |
-
)
|
| 272 |
-
)
|
| 273 |
-
self.active_depth += 1
|
| 274 |
-
self.backbone.config.num_hidden_layers = self.active_depth
|
| 275 |
-
self.backbone.config.layer_types = self.source_layer_types[: self.active_depth]
|
| 276 |
-
if hasattr(self.backbone, "rotary_emb"):
|
| 277 |
-
from transformers.models.modernbert.modeling_modernbert import ModernBertRotaryEmbedding
|
| 278 |
-
|
| 279 |
-
self.backbone.rotary_emb = ModernBertRotaryEmbedding(config=self.backbone.config)
|
| 280 |
-
self.freeze_for_stage()
|
| 281 |
-
|
| 282 |
-
def freeze_for_stage(self) -> None:
|
| 283 |
-
"""Train only the newest block plus the shared residual gate."""
|
| 284 |
-
|
| 285 |
-
for parameter in self.parameters():
|
| 286 |
-
parameter.requires_grad = False
|
| 287 |
-
newest = self.backbone.layers[self.active_depth - 1]
|
| 288 |
-
for parameter in newest.parameters():
|
| 289 |
-
parameter.requires_grad = True
|
| 290 |
-
self.output_gate.requires_grad = True
|
| 291 |
-
|
| 292 |
-
def trainable_parameter_counts(self) -> dict[str, int]:
|
| 293 |
-
trainable = sum(parameter.numel() for parameter in self.parameters() if parameter.requires_grad)
|
| 294 |
-
total = sum(parameter.numel() for parameter in self.parameters())
|
| 295 |
-
return {"trainable": int(trainable), "total": int(total)}
|
| 296 |
-
|
| 297 |
-
def get_embedding_dimension(self) -> int:
|
| 298 |
-
return self.embedding_dimension
|
| 299 |
-
|
| 300 |
-
def preprocess(self, inputs: list[str], prompt: str | None = None, **kwargs: Any) -> dict[str, torch.Tensor]:
|
| 301 |
-
del kwargs
|
| 302 |
-
if prompt:
|
| 303 |
-
inputs = self._prepend_prompt(inputs, prompt)
|
| 304 |
-
encodings = self.tokenizer.encode_batch(inputs, add_special_tokens=False)
|
| 305 |
-
token_ids = [encoding.ids[: self.max_seq_length] for encoding in encodings]
|
| 306 |
-
sequence_length = max(len(ids) for ids in token_ids)
|
| 307 |
-
sequence_length = max(sequence_length, 1)
|
| 308 |
-
input_ids = torch.zeros((len(token_ids), sequence_length), dtype=torch.long)
|
| 309 |
-
attention_mask = torch.zeros((len(token_ids), sequence_length), dtype=torch.long)
|
| 310 |
-
for row, ids in enumerate(token_ids):
|
| 311 |
-
input_ids[row, : len(ids)] = torch.tensor(ids, dtype=torch.long)
|
| 312 |
-
attention_mask[row, : len(ids)] = 1
|
| 313 |
-
return {"input_ids": input_ids, "attention_mask": attention_mask}
|
| 314 |
-
|
| 315 |
-
def tokenize(self, texts: list[str], **kwargs: Any) -> dict[str, torch.Tensor]:
|
| 316 |
-
return self.preprocess(texts, **kwargs)
|
| 317 |
-
|
| 318 |
-
def forward(self, features: dict[str, torch.Tensor], **kwargs: Any) -> dict[str, torch.Tensor]:
|
| 319 |
-
del kwargs
|
| 320 |
-
input_ids = features["input_ids"]
|
| 321 |
-
attention_mask = features["attention_mask"]
|
| 322 |
-
base_tokens = self.base_embedding(input_ids)
|
| 323 |
-
branch_tokens = self.backbone(
|
| 324 |
-
inputs_embeds=base_tokens,
|
| 325 |
-
attention_mask=attention_mask,
|
| 326 |
-
return_dict=True,
|
| 327 |
-
).last_hidden_state
|
| 328 |
-
residual = torch.nan_to_num(branch_tokens - base_tokens)
|
| 329 |
-
output_gate = self.output_gate.to(dtype=base_tokens.dtype)
|
| 330 |
-
token_embeddings = base_tokens + output_gate * residual
|
| 331 |
-
mask = attention_mask.unsqueeze(-1).to(dtype=token_embeddings.dtype)
|
| 332 |
-
sentence_embedding = (token_embeddings * mask).sum(dim=1) / mask.sum(dim=1).clamp_min(1.0)
|
| 333 |
-
features["token_embeddings"] = token_embeddings
|
| 334 |
-
features["sentence_embedding"] = sentence_embedding
|
| 335 |
-
return features
|
| 336 |
-
|
| 337 |
-
def get_config_dict(self) -> dict[str, Any]:
|
| 338 |
-
return {
|
| 339 |
-
"active_depth": self.active_depth,
|
| 340 |
-
"base_vocab_size": self.vocab_size,
|
| 341 |
-
"embedding_dimension": self.embedding_dimension,
|
| 342 |
-
"max_seq_length": self.max_seq_length,
|
| 343 |
-
"source_layer_types": self.source_layer_types,
|
| 344 |
-
"donor_config": self.donor_config,
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
def save(self, output_path: str, *args: Any, safe_serialization: bool = True, **kwargs: Any) -> None:
|
| 348 |
-
del args, kwargs
|
| 349 |
-
Path(output_path).mkdir(parents=True, exist_ok=True)
|
| 350 |
-
self.save_config(output_path)
|
| 351 |
-
self.save_torch_weights(output_path, safe_serialization=safe_serialization)
|
| 352 |
-
self.tokenizer.save(str(Path(output_path) / "tokenizer.json"))
|
| 353 |
-
|
| 354 |
-
@classmethod
|
| 355 |
-
def load(
|
| 356 |
-
cls,
|
| 357 |
-
model_name_or_path: str,
|
| 358 |
-
subfolder: str = "",
|
| 359 |
-
token: bool | str | None = None,
|
| 360 |
-
cache_folder: str | None = None,
|
| 361 |
-
revision: str | None = None,
|
| 362 |
-
local_files_only: bool = False,
|
| 363 |
-
**kwargs: Any,
|
| 364 |
-
) -> "ProgressiveEncoder":
|
| 365 |
-
del kwargs
|
| 366 |
-
hub_kwargs = {
|
| 367 |
-
"subfolder": subfolder,
|
| 368 |
-
"token": token,
|
| 369 |
-
"cache_folder": cache_folder,
|
| 370 |
-
"revision": revision,
|
| 371 |
-
"local_files_only": local_files_only,
|
| 372 |
-
}
|
| 373 |
-
config_path = cls.load_file_path(
|
| 374 |
-
model_name_or_path,
|
| 375 |
-
filename=cls.config_file_name,
|
| 376 |
-
**hub_kwargs,
|
| 377 |
-
)
|
| 378 |
-
tokenizer_path = cls.load_file_path(model_name_or_path, filename="tokenizer.json", **hub_kwargs)
|
| 379 |
-
if config_path is None or tokenizer_path is None:
|
| 380 |
-
raise FileNotFoundError("Progressive model config or tokenizer is missing")
|
| 381 |
-
config = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
| 382 |
-
model = cls(
|
| 383 |
-
tokenizer=Tokenizer.from_file(tokenizer_path),
|
| 384 |
-
base_weights=torch.empty(
|
| 385 |
-
int(config["base_vocab_size"]),
|
| 386 |
-
int(config["embedding_dimension"]),
|
| 387 |
-
),
|
| 388 |
-
donor_config=dict(config["donor_config"]),
|
| 389 |
-
source_layer_types=list(config["source_layer_types"]),
|
| 390 |
-
active_depth=int(config["active_depth"]),
|
| 391 |
-
max_seq_length=int(config["max_seq_length"]),
|
| 392 |
-
)
|
| 393 |
-
cls.load_torch_weights(model_name_or_path=model_name_or_path, model=model, **hub_kwargs)
|
| 394 |
-
model.freeze_for_stage()
|
| 395 |
-
return model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/project.py
DELETED
|
@@ -1,120 +0,0 @@
|
|
| 1 |
-
"""Export the sign-projected token table used by StaRSE inference."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import argparse
|
| 6 |
-
import json
|
| 7 |
-
import math
|
| 8 |
-
import shutil
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
import numpy as np
|
| 12 |
-
import torch
|
| 13 |
-
from safetensors.torch import save_file
|
| 14 |
-
|
| 15 |
-
from starse.binary_static_embedding import BinaryStaticEmbedding
|
| 16 |
-
from starse.loss import static_embedding_weight
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def sign_project(weight: torch.Tensor) -> torch.Tensor:
|
| 20 |
-
"""Preserve each token norm and replace its direction with sign bits."""
|
| 21 |
-
|
| 22 |
-
source = weight.detach().float()
|
| 23 |
-
norms = torch.linalg.vector_norm(source, dim=1).clamp_min(1e-12)
|
| 24 |
-
signs = torch.where(source >= 0, torch.ones_like(source), -torch.ones_like(source))
|
| 25 |
-
return norms.unsqueeze(1) * signs / math.sqrt(source.shape[1])
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def export_model(model_path: str, output_dir: Path) -> None:
|
| 29 |
-
from sentence_transformers import SentenceTransformer
|
| 30 |
-
|
| 31 |
-
model = SentenceTransformer(model_path, device="cpu")
|
| 32 |
-
weight = static_embedding_weight(model)
|
| 33 |
-
projected = sign_project(weight)
|
| 34 |
-
packed_signs, norms = BinaryStaticEmbedding.pack(projected)
|
| 35 |
-
first_module = model._first_module()
|
| 36 |
-
tokenizer = first_module.tokenizer
|
| 37 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 38 |
-
module_source = Path(__file__).with_name("binary_static_embedding.py")
|
| 39 |
-
shutil.copyfile(module_source, output_dir / "binary_static_embedding.py")
|
| 40 |
-
tokenizer_backend = getattr(tokenizer, "backend_tokenizer", tokenizer)
|
| 41 |
-
tokenizer_backend.save(str(output_dir / "tokenizer.json"))
|
| 42 |
-
save_file(
|
| 43 |
-
{"packed_signs": packed_signs.contiguous(), "norms": norms.contiguous()},
|
| 44 |
-
str(output_dir / "model.safetensors"),
|
| 45 |
-
)
|
| 46 |
-
(output_dir / "binary_static_embedding_config.json").write_text(
|
| 47 |
-
json.dumps(
|
| 48 |
-
{
|
| 49 |
-
"embedding_dim": int(projected.shape[1]),
|
| 50 |
-
"vocab_size": int(projected.shape[0]),
|
| 51 |
-
"packed_bit_order": "big",
|
| 52 |
-
"scale": "norm / sqrt(embedding_dim)",
|
| 53 |
-
},
|
| 54 |
-
indent=2,
|
| 55 |
-
)
|
| 56 |
-
+ "\n",
|
| 57 |
-
encoding="utf-8",
|
| 58 |
-
)
|
| 59 |
-
(output_dir / "modules.json").write_text(
|
| 60 |
-
json.dumps(
|
| 61 |
-
[
|
| 62 |
-
{"idx": 0, "name": "0", "path": "", "type": "binary_static_embedding.BinaryStaticEmbedding"},
|
| 63 |
-
{
|
| 64 |
-
"idx": 1,
|
| 65 |
-
"name": "1",
|
| 66 |
-
"path": "1_Normalize",
|
| 67 |
-
"type": "sentence_transformers.models.Normalize",
|
| 68 |
-
},
|
| 69 |
-
],
|
| 70 |
-
indent=2,
|
| 71 |
-
)
|
| 72 |
-
+ "\n",
|
| 73 |
-
encoding="utf-8",
|
| 74 |
-
)
|
| 75 |
-
(output_dir / "config_sentence_transformers.json").write_text(
|
| 76 |
-
json.dumps(
|
| 77 |
-
{
|
| 78 |
-
"model_type": "SentenceTransformer",
|
| 79 |
-
"similarity_fn_name": "cosine",
|
| 80 |
-
"default_prompt_name": None,
|
| 81 |
-
"prompts": {"document": "", "query": ""},
|
| 82 |
-
},
|
| 83 |
-
indent=2,
|
| 84 |
-
)
|
| 85 |
-
+ "\n",
|
| 86 |
-
encoding="utf-8",
|
| 87 |
-
)
|
| 88 |
-
normalize_dir = output_dir / "1_Normalize"
|
| 89 |
-
normalize_dir.mkdir(exist_ok=True)
|
| 90 |
-
(normalize_dir / "config.json").write_text("{}\n", encoding="utf-8")
|
| 91 |
-
metadata = {
|
| 92 |
-
"projection": "rho * sign(unit) / sqrt(dim)",
|
| 93 |
-
"embedding_dim": int(projected.shape[1]),
|
| 94 |
-
"vocab_size": int(projected.shape[0]),
|
| 95 |
-
"storage": "packed sign bits plus one FP32 L2 norm per token",
|
| 96 |
-
"packed_signs_shape": list(packed_signs.shape),
|
| 97 |
-
"packed_weights_bytes": int(packed_signs.numel() + norms.numel() * np.dtype(np.float32).itemsize),
|
| 98 |
-
"requires_trust_remote_code": True,
|
| 99 |
-
}
|
| 100 |
-
(output_dir / "projection.json").write_text(
|
| 101 |
-
json.dumps(metadata, ensure_ascii=False, indent=2) + "\n",
|
| 102 |
-
encoding="utf-8",
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
| 107 |
-
parser = argparse.ArgumentParser(description=__doc__)
|
| 108 |
-
parser.add_argument("--model", required=True, help="Trained StaticEmbedding model")
|
| 109 |
-
parser.add_argument("--output-dir", type=Path, required=True)
|
| 110 |
-
return parser.parse_args(argv)
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def main(argv: list[str] | None = None) -> int:
|
| 114 |
-
args = parse_args(argv)
|
| 115 |
-
export_model(args.model, args.output_dir.resolve())
|
| 116 |
-
return 0
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
if __name__ == "__main__":
|
| 120 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/recursive_cascade.py
DELETED
|
@@ -1,1078 +0,0 @@
|
|
| 1 |
-
"""Recursive criterion-grown token vocabulary primitives.
|
| 2 |
-
|
| 3 |
-
This module is intentionally small and CPU-testable. It establishes the Q32
|
| 4 |
-
mechanism contract before the recursive tokenizer is connected to the streaming
|
| 5 |
-
trainer: learned tokens are emitted as discrete ids, may parent later tokens,
|
| 6 |
-
receive gradients, and resume with exact optimizer state.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import copy
|
| 12 |
-
from dataclasses import dataclass
|
| 13 |
-
import hashlib
|
| 14 |
-
import json
|
| 15 |
-
import math
|
| 16 |
-
from typing import Any, Iterable, Sequence
|
| 17 |
-
|
| 18 |
-
import torch
|
| 19 |
-
from torch import nn
|
| 20 |
-
|
| 21 |
-
from starse.collapse import _weighted_index_add_
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
@dataclass(frozen=True)
|
| 25 |
-
class CascadeToken:
|
| 26 |
-
token_id: int
|
| 27 |
-
span: tuple[int, ...]
|
| 28 |
-
generation: int
|
| 29 |
-
left_parent: int | None
|
| 30 |
-
right_parent: int | None
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
class RecursiveCascadeVocabulary(nn.Module):
|
| 34 |
-
"""A monotone DAG vocabulary over canonical base-token spans."""
|
| 35 |
-
|
| 36 |
-
def __init__(
|
| 37 |
-
self,
|
| 38 |
-
base_embeddings: torch.Tensor,
|
| 39 |
-
*,
|
| 40 |
-
max_span_length: int = 32,
|
| 41 |
-
) -> None:
|
| 42 |
-
super().__init__()
|
| 43 |
-
if base_embeddings.ndim != 2 or not base_embeddings.shape[0]:
|
| 44 |
-
raise ValueError("base_embeddings must be a nonempty matrix")
|
| 45 |
-
if not base_embeddings.is_floating_point() or not bool(
|
| 46 |
-
torch.isfinite(base_embeddings).all()
|
| 47 |
-
):
|
| 48 |
-
raise ValueError("base_embeddings must be finite floating point")
|
| 49 |
-
if max_span_length < 2:
|
| 50 |
-
raise ValueError("max_span_length must be at least two")
|
| 51 |
-
self.dim = int(base_embeddings.shape[1])
|
| 52 |
-
self.max_span_length = int(max_span_length)
|
| 53 |
-
self.base_size = int(base_embeddings.shape[0])
|
| 54 |
-
self.embeddings = nn.ParameterList(
|
| 55 |
-
nn.Parameter(row.detach().clone()) for row in base_embeddings
|
| 56 |
-
)
|
| 57 |
-
self._tokens = tuple(
|
| 58 |
-
CascadeToken(index, (index,), 0, None, None)
|
| 59 |
-
for index in range(self.base_size)
|
| 60 |
-
)
|
| 61 |
-
|
| 62 |
-
@property
|
| 63 |
-
def tokens(self) -> tuple[CascadeToken, ...]:
|
| 64 |
-
return self._tokens
|
| 65 |
-
|
| 66 |
-
def _span_index(self) -> dict[tuple[int, ...], int]:
|
| 67 |
-
return {token.span: token.token_id for token in self._tokens}
|
| 68 |
-
|
| 69 |
-
def tokenize(self, base_ids: Sequence[int]) -> tuple[int, ...]:
|
| 70 |
-
"""Greedily emit longest canonical spans with stable id tie-breaking."""
|
| 71 |
-
|
| 72 |
-
sequence = tuple(int(value) for value in base_ids)
|
| 73 |
-
if any(value < 0 or value >= self.base_size for value in sequence):
|
| 74 |
-
raise ValueError("tokenize input must contain only base token ids")
|
| 75 |
-
spans = self._span_index()
|
| 76 |
-
by_first: dict[int, list[tuple[tuple[int, ...], int]]] = {}
|
| 77 |
-
for span, token_id in spans.items():
|
| 78 |
-
by_first.setdefault(span[0], []).append((span, token_id))
|
| 79 |
-
for candidates in by_first.values():
|
| 80 |
-
candidates.sort(key=lambda item: (-len(item[0]), item[1]))
|
| 81 |
-
|
| 82 |
-
emitted: list[int] = []
|
| 83 |
-
position = 0
|
| 84 |
-
while position < len(sequence):
|
| 85 |
-
chosen_span = (sequence[position],)
|
| 86 |
-
chosen_id = sequence[position]
|
| 87 |
-
for span, token_id in by_first.get(sequence[position], ()):
|
| 88 |
-
if sequence[position : position + len(span)] == span:
|
| 89 |
-
chosen_span, chosen_id = span, token_id
|
| 90 |
-
break
|
| 91 |
-
emitted.append(chosen_id)
|
| 92 |
-
position += len(chosen_span)
|
| 93 |
-
return tuple(emitted)
|
| 94 |
-
|
| 95 |
-
@staticmethod
|
| 96 |
-
def adjacent_pairs(token_ids: Sequence[int]) -> tuple[tuple[int, int], ...]:
|
| 97 |
-
emitted = tuple(int(value) for value in token_ids)
|
| 98 |
-
return tuple(zip(emitted, emitted[1:]))
|
| 99 |
-
|
| 100 |
-
def lookup(self, token_ids: Sequence[int]) -> torch.Tensor:
|
| 101 |
-
ids = tuple(int(value) for value in token_ids)
|
| 102 |
-
if not ids:
|
| 103 |
-
raise ValueError("lookup requires at least one token")
|
| 104 |
-
if any(value < 0 or value >= len(self.embeddings) for value in ids):
|
| 105 |
-
raise ValueError("lookup token id is outside the vocabulary")
|
| 106 |
-
return torch.stack([self.embeddings[value] for value in ids])
|
| 107 |
-
|
| 108 |
-
def encode_mean(self, base_ids: Sequence[int]) -> tuple[torch.Tensor, tuple[int, ...]]:
|
| 109 |
-
emitted = self.tokenize(base_ids)
|
| 110 |
-
return self.lookup(emitted).mean(dim=0), emitted
|
| 111 |
-
|
| 112 |
-
@staticmethod
|
| 113 |
-
def _average_parent_state(
|
| 114 |
-
optimizer: torch.optim.Optimizer,
|
| 115 |
-
left: nn.Parameter,
|
| 116 |
-
right: nn.Parameter,
|
| 117 |
-
) -> dict[Any, Any]:
|
| 118 |
-
left_state = optimizer.state.get(left, {})
|
| 119 |
-
right_state = optimizer.state.get(right, {})
|
| 120 |
-
if not left_state or set(left_state) != set(right_state):
|
| 121 |
-
raise ValueError("both parents require aligned initialized optimizer state")
|
| 122 |
-
result: dict[Any, Any] = {}
|
| 123 |
-
for name in left_state:
|
| 124 |
-
left_value, right_value = left_state[name], right_state[name]
|
| 125 |
-
if torch.is_tensor(left_value) != torch.is_tensor(right_value):
|
| 126 |
-
raise ValueError(f"optimizer parent state type differs: {name}")
|
| 127 |
-
if not torch.is_tensor(left_value):
|
| 128 |
-
if left_value != right_value:
|
| 129 |
-
raise ValueError(f"optimizer parent scalar differs: {name}")
|
| 130 |
-
result[name] = left_value
|
| 131 |
-
elif left_value.ndim == 0:
|
| 132 |
-
if not torch.equal(left_value, right_value):
|
| 133 |
-
raise ValueError(f"optimizer parent step differs: {name}")
|
| 134 |
-
result[name] = left_value.detach().clone()
|
| 135 |
-
else:
|
| 136 |
-
if left_value.shape != left.shape or right_value.shape != right.shape:
|
| 137 |
-
raise ValueError(f"optimizer parent row shape differs: {name}")
|
| 138 |
-
result[name] = (0.5 * (left_value + right_value)).detach().clone()
|
| 139 |
-
return result
|
| 140 |
-
|
| 141 |
-
def promote_pairs(
|
| 142 |
-
self,
|
| 143 |
-
pairs: Iterable[tuple[int, int]],
|
| 144 |
-
*,
|
| 145 |
-
optimizer: torch.optim.Optimizer,
|
| 146 |
-
) -> tuple[CascadeToken, ...]:
|
| 147 |
-
"""Materialize every new canonical span and attach it to Adam in place."""
|
| 148 |
-
|
| 149 |
-
requested = tuple((int(left), int(right)) for left, right in pairs)
|
| 150 |
-
if not requested:
|
| 151 |
-
return ()
|
| 152 |
-
if len(set(requested)) != len(requested):
|
| 153 |
-
raise ValueError("promotion pairs must be unique")
|
| 154 |
-
existing = self._span_index()
|
| 155 |
-
additions: list[CascadeToken] = []
|
| 156 |
-
for left_id, right_id in requested:
|
| 157 |
-
if not (0 <= left_id < len(self._tokens) and 0 <= right_id < len(self._tokens)):
|
| 158 |
-
raise ValueError("promotion parent id is outside the vocabulary")
|
| 159 |
-
left_token, right_token = self._tokens[left_id], self._tokens[right_id]
|
| 160 |
-
span = left_token.span + right_token.span
|
| 161 |
-
if len(span) > self.max_span_length:
|
| 162 |
-
raise ValueError("promotion exceeds max_span_length")
|
| 163 |
-
if span in existing:
|
| 164 |
-
raise ValueError("promotion span already exists")
|
| 165 |
-
generation = max(left_token.generation, right_token.generation) + 1
|
| 166 |
-
token_id = len(self._tokens) + len(additions)
|
| 167 |
-
parameter = nn.Parameter(
|
| 168 |
-
0.5
|
| 169 |
-
* (
|
| 170 |
-
self.embeddings[left_id].detach()
|
| 171 |
-
+ self.embeddings[right_id].detach()
|
| 172 |
-
)
|
| 173 |
-
)
|
| 174 |
-
state = self._average_parent_state(
|
| 175 |
-
optimizer, self.embeddings[left_id], self.embeddings[right_id]
|
| 176 |
-
)
|
| 177 |
-
self.embeddings.append(parameter)
|
| 178 |
-
optimizer.param_groups[0]["params"].append(parameter)
|
| 179 |
-
optimizer.state[parameter] = state
|
| 180 |
-
token = CascadeToken(token_id, span, generation, left_id, right_id)
|
| 181 |
-
additions.append(token)
|
| 182 |
-
existing[span] = token_id
|
| 183 |
-
self._tokens = (*self._tokens, *additions)
|
| 184 |
-
return tuple(additions)
|
| 185 |
-
|
| 186 |
-
def snapshot(self, optimizer: torch.optim.Optimizer) -> dict[str, Any]:
|
| 187 |
-
return {
|
| 188 |
-
"version": 1,
|
| 189 |
-
"protocol": "recursive-cascade-micro-state-v1",
|
| 190 |
-
"base_size": self.base_size,
|
| 191 |
-
"dim": self.dim,
|
| 192 |
-
"max_span_length": self.max_span_length,
|
| 193 |
-
"tokens": [
|
| 194 |
-
{
|
| 195 |
-
"token_id": token.token_id,
|
| 196 |
-
"span": list(token.span),
|
| 197 |
-
"generation": token.generation,
|
| 198 |
-
"left_parent": token.left_parent,
|
| 199 |
-
"right_parent": token.right_parent,
|
| 200 |
-
}
|
| 201 |
-
for token in self._tokens
|
| 202 |
-
],
|
| 203 |
-
# PyTorch state_dict values alias live parameter/optimizer storage.
|
| 204 |
-
# A resume snapshot must be immutable while the source run keeps
|
| 205 |
-
# training, so clone the complete trees at the snapshot boundary.
|
| 206 |
-
"model": copy.deepcopy(self.state_dict()),
|
| 207 |
-
"optimizer": copy.deepcopy(optimizer.state_dict()),
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
@classmethod
|
| 211 |
-
def from_snapshot(
|
| 212 |
-
cls,
|
| 213 |
-
payload: dict[str, Any],
|
| 214 |
-
*,
|
| 215 |
-
optimizer_kwargs: dict[str, Any],
|
| 216 |
-
) -> tuple["RecursiveCascadeVocabulary", torch.optim.AdamW]:
|
| 217 |
-
if (
|
| 218 |
-
not isinstance(payload, dict)
|
| 219 |
-
or payload.get("version") != 1
|
| 220 |
-
or payload.get("protocol") != "recursive-cascade-micro-state-v1"
|
| 221 |
-
):
|
| 222 |
-
raise ValueError("recursive cascade snapshot identity differs")
|
| 223 |
-
tokens = payload.get("tokens")
|
| 224 |
-
if not isinstance(tokens, list) or len(tokens) < int(payload["base_size"]):
|
| 225 |
-
raise ValueError("recursive cascade snapshot tokens are invalid")
|
| 226 |
-
model_state = payload.get("model")
|
| 227 |
-
if not isinstance(model_state, dict):
|
| 228 |
-
raise ValueError("recursive cascade snapshot model is invalid")
|
| 229 |
-
base_rows = torch.stack(
|
| 230 |
-
[model_state[f"embeddings.{index}"] for index in range(int(payload["base_size"]))]
|
| 231 |
-
)
|
| 232 |
-
restored = cls(base_rows, max_span_length=int(payload["max_span_length"]))
|
| 233 |
-
restored_tokens: list[CascadeToken] = []
|
| 234 |
-
for index, row in enumerate(tokens):
|
| 235 |
-
token = CascadeToken(
|
| 236 |
-
int(row["token_id"]),
|
| 237 |
-
tuple(int(value) for value in row["span"]),
|
| 238 |
-
int(row["generation"]),
|
| 239 |
-
None if row["left_parent"] is None else int(row["left_parent"]),
|
| 240 |
-
None if row["right_parent"] is None else int(row["right_parent"]),
|
| 241 |
-
)
|
| 242 |
-
if token.token_id != index:
|
| 243 |
-
raise ValueError("recursive cascade token ids are not contiguous")
|
| 244 |
-
restored_tokens.append(token)
|
| 245 |
-
for index in range(restored.base_size, len(restored_tokens)):
|
| 246 |
-
restored.embeddings.append(
|
| 247 |
-
nn.Parameter(model_state[f"embeddings.{index}"].detach().clone())
|
| 248 |
-
)
|
| 249 |
-
restored._tokens = tuple(restored_tokens)
|
| 250 |
-
restored.load_state_dict(model_state, strict=True)
|
| 251 |
-
optimizer = torch.optim.AdamW(restored.parameters(), **optimizer_kwargs)
|
| 252 |
-
optimizer.load_state_dict(payload["optimizer"])
|
| 253 |
-
return restored, optimizer
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
def recursive_pair_key(left: int, right: int) -> int:
|
| 257 |
-
"""Return a collision-free Cantor identity for two non-negative token ids."""
|
| 258 |
-
|
| 259 |
-
left, right = int(left), int(right)
|
| 260 |
-
if left < 0 or right < 0:
|
| 261 |
-
raise ValueError("recursive pair parents must be non-negative")
|
| 262 |
-
total = left + right
|
| 263 |
-
key = total * (total + 1) // 2 + right
|
| 264 |
-
if key > torch.iinfo(torch.int64).max:
|
| 265 |
-
raise OverflowError("recursive pair identity exceeds int64")
|
| 266 |
-
return key
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
def decode_recursive_pair_key(key: int) -> tuple[int, int]:
|
| 270 |
-
"""Invert :func:`recursive_pair_key` exactly using integer arithmetic."""
|
| 271 |
-
|
| 272 |
-
key = int(key)
|
| 273 |
-
if key < 0:
|
| 274 |
-
raise ValueError("recursive pair key must be non-negative")
|
| 275 |
-
diagonal = (math.isqrt(8 * key + 1) - 1) // 2
|
| 276 |
-
diagonal_start = diagonal * (diagonal + 1) // 2
|
| 277 |
-
right = key - diagonal_start
|
| 278 |
-
left = diagonal - right
|
| 279 |
-
if recursive_pair_key(left, right) != key:
|
| 280 |
-
raise ValueError("recursive pair key is not canonical")
|
| 281 |
-
return left, right
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
def _recursive_pair_keys(left: torch.Tensor, right: torch.Tensor) -> torch.Tensor:
|
| 285 |
-
if left.dtype != torch.long or right.dtype != torch.long:
|
| 286 |
-
raise ValueError("recursive token ids must use torch.long")
|
| 287 |
-
if bool((left < 0).any()) or bool((right < 0).any()):
|
| 288 |
-
raise ValueError("recursive token ids must be non-negative")
|
| 289 |
-
total = left + right
|
| 290 |
-
# Cantor pairing is exact while the triangular term fits signed int64.
|
| 291 |
-
if total.numel() and int(total.max()) > 3_037_000_498:
|
| 292 |
-
raise OverflowError("recursive pair identity exceeds int64")
|
| 293 |
-
return total * (total + 1) // 2 + right
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
def _proposal_hash(
|
| 297 |
-
left: torch.Tensor, right: torch.Tensor, buckets: int
|
| 298 |
-
) -> torch.Tensor:
|
| 299 |
-
"""Preserve the Q15 proposal-row mapping at the exact Q32 launch."""
|
| 300 |
-
|
| 301 |
-
return ((left * 2_654_435_761 + right * 40_503).abs()) % int(buckets)
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
class ExactRecursiveDiscovery:
|
| 305 |
-
"""Unbounded exact pair/language statistics for one finite half-window."""
|
| 306 |
-
|
| 307 |
-
def __init__(self, *, n_languages: int) -> None:
|
| 308 |
-
if n_languages <= 0:
|
| 309 |
-
raise ValueError("recursive discovery needs a positive language count")
|
| 310 |
-
self.n_languages = int(n_languages)
|
| 311 |
-
self.records: dict[int, list[float | int]] = {}
|
| 312 |
-
self.observed_occurrences = 0
|
| 313 |
-
self.transferred_records = 0
|
| 314 |
-
|
| 315 |
-
@torch.no_grad()
|
| 316 |
-
def observe(
|
| 317 |
-
self,
|
| 318 |
-
pair_key: torch.Tensor,
|
| 319 |
-
language: torch.Tensor,
|
| 320 |
-
probability: torch.Tensor,
|
| 321 |
-
gradient: torch.Tensor,
|
| 322 |
-
) -> None:
|
| 323 |
-
if not pair_key.numel():
|
| 324 |
-
return
|
| 325 |
-
utility = -probability.detach().float() * gradient.detach().float()
|
| 326 |
-
finite = (
|
| 327 |
-
torch.isfinite(utility)
|
| 328 |
-
& torch.isfinite(probability.detach().float())
|
| 329 |
-
& (language >= 0)
|
| 330 |
-
& (language < self.n_languages)
|
| 331 |
-
)
|
| 332 |
-
if not bool(finite.any()):
|
| 333 |
-
return
|
| 334 |
-
pair_key = pair_key.detach()[finite].to(torch.long)
|
| 335 |
-
language = language.detach()[finite].to(torch.long)
|
| 336 |
-
probability = probability.detach()[finite].float()
|
| 337 |
-
utility = utility[finite]
|
| 338 |
-
self.observed_occurrences += int(pair_key.numel())
|
| 339 |
-
composite = pair_key * self.n_languages + language
|
| 340 |
-
unique, inverse = torch.unique(composite, return_inverse=True)
|
| 341 |
-
utility_sum = torch.zeros(unique.numel(), device=utility.device)
|
| 342 |
-
probability_sum = torch.zeros(unique.numel(), device=utility.device)
|
| 343 |
-
support = torch.zeros(unique.numel(), dtype=torch.long, device=utility.device)
|
| 344 |
-
positive_utility_support = torch.zeros(
|
| 345 |
-
unique.numel(), dtype=torch.long, device=utility.device
|
| 346 |
-
)
|
| 347 |
-
utility_sum.index_add_(0, inverse, utility)
|
| 348 |
-
probability_sum.index_add_(0, inverse, probability)
|
| 349 |
-
support.index_add_(0, inverse, torch.ones_like(inverse))
|
| 350 |
-
positive_utility_support.index_add_(
|
| 351 |
-
0, inverse, (utility > 0).to(torch.long)
|
| 352 |
-
)
|
| 353 |
-
rows = zip(
|
| 354 |
-
unique.cpu().tolist(),
|
| 355 |
-
utility_sum.cpu().tolist(),
|
| 356 |
-
probability_sum.cpu().tolist(),
|
| 357 |
-
support.cpu().tolist(),
|
| 358 |
-
positive_utility_support.cpu().tolist(),
|
| 359 |
-
strict=True,
|
| 360 |
-
)
|
| 361 |
-
for composite_key, value, probability_value, count, positive_count in rows:
|
| 362 |
-
key = int(composite_key)
|
| 363 |
-
row = self.records.setdefault(key, [0.0, 0.0, 0, 0])
|
| 364 |
-
row[0] = float(row[0]) + float(value)
|
| 365 |
-
row[1] = float(row[1]) + float(probability_value)
|
| 366 |
-
row[2] = int(row[2]) + int(count)
|
| 367 |
-
row[3] = int(row[3]) + int(positive_count)
|
| 368 |
-
self.transferred_records += 1
|
| 369 |
-
|
| 370 |
-
def snapshot_records(self) -> tuple[dict[str, float | int], ...]:
|
| 371 |
-
result = []
|
| 372 |
-
for composite, row in sorted(self.records.items()):
|
| 373 |
-
pair_key, language = divmod(composite, self.n_languages)
|
| 374 |
-
utility, probability_sum, support, positive_utility_support = (
|
| 375 |
-
float(row[0]), float(row[1]), int(row[2]), int(row[3])
|
| 376 |
-
)
|
| 377 |
-
result.append(
|
| 378 |
-
{
|
| 379 |
-
"composite_key": composite,
|
| 380 |
-
"pair_key": pair_key,
|
| 381 |
-
"language_index": language,
|
| 382 |
-
"utility": utility,
|
| 383 |
-
"probability_sum": probability_sum,
|
| 384 |
-
"captured_support": support,
|
| 385 |
-
"positive_utility_support": positive_utility_support,
|
| 386 |
-
"mean_probability": probability_sum / support,
|
| 387 |
-
}
|
| 388 |
-
)
|
| 389 |
-
return tuple(result)
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
class DenseRecursiveCascade(nn.Module):
|
| 393 |
-
"""Batched recursive tokenizer plus one dense trainable learned-token table.
|
| 394 |
-
|
| 395 |
-
Accepted rules are applied once per structural generation. Consequently a
|
| 396 |
-
token born at boundary ``g`` can be emitted and used by discovery during the
|
| 397 |
-
following interval, but a transition cannot recursively consume rows it is
|
| 398 |
-
creating itself. This makes every structural update an atomic DAG layer.
|
| 399 |
-
"""
|
| 400 |
-
|
| 401 |
-
def __init__(
|
| 402 |
-
self,
|
| 403 |
-
*,
|
| 404 |
-
base_size: int,
|
| 405 |
-
dim: int,
|
| 406 |
-
n_languages: int,
|
| 407 |
-
residual_buckets: int,
|
| 408 |
-
max_span_length: int = 32,
|
| 409 |
-
restored_state: dict[str, torch.Tensor] | None = None,
|
| 410 |
-
) -> None:
|
| 411 |
-
super().__init__()
|
| 412 |
-
if base_size <= 0 or dim <= 0 or n_languages <= 0:
|
| 413 |
-
raise ValueError("recursive cascade dimensions must be positive")
|
| 414 |
-
if residual_buckets <= 0 or max_span_length < 2:
|
| 415 |
-
raise ValueError("recursive residual capacity/span limit is invalid")
|
| 416 |
-
self.base_size = int(base_size)
|
| 417 |
-
self.dim = int(dim)
|
| 418 |
-
self.n_languages = int(n_languages)
|
| 419 |
-
self.residual_buckets = int(residual_buckets)
|
| 420 |
-
self.max_span_length = int(max_span_length)
|
| 421 |
-
|
| 422 |
-
restored = dict(restored_state or {})
|
| 423 |
-
metadata_names = {
|
| 424 |
-
"rule_left", "rule_right", "rule_generation", "span_offsets",
|
| 425 |
-
"span_values", "rule_active", "learned",
|
| 426 |
-
}
|
| 427 |
-
legacy_metadata_names = metadata_names - {"rule_active"}
|
| 428 |
-
if restored and set(restored) not in (metadata_names, legacy_metadata_names):
|
| 429 |
-
raise ValueError("recursive cascade restored state has an invalid schema")
|
| 430 |
-
learned = restored.get("learned", torch.empty((0, self.dim)))
|
| 431 |
-
if learned.ndim != 2 or learned.shape[1] != self.dim:
|
| 432 |
-
raise ValueError("recursive learned table has an invalid shape")
|
| 433 |
-
self.learned = nn.Parameter(learned.detach().clone())
|
| 434 |
-
count = int(learned.shape[0])
|
| 435 |
-
defaults = {
|
| 436 |
-
"rule_left": torch.empty(0, dtype=torch.long),
|
| 437 |
-
"rule_right": torch.empty(0, dtype=torch.long),
|
| 438 |
-
"rule_generation": torch.empty(0, dtype=torch.long),
|
| 439 |
-
"span_offsets": torch.zeros(1, dtype=torch.long),
|
| 440 |
-
"span_values": torch.empty(0, dtype=torch.long),
|
| 441 |
-
}
|
| 442 |
-
for name, default in defaults.items():
|
| 443 |
-
value = restored.get(name, default).detach().clone().to(torch.long)
|
| 444 |
-
self.register_buffer(name, value, persistent=True)
|
| 445 |
-
active = restored.get(
|
| 446 |
-
"rule_active", torch.ones(count, dtype=torch.bool)
|
| 447 |
-
).detach().clone().to(torch.bool)
|
| 448 |
-
self.register_buffer("rule_active", active, persistent=True)
|
| 449 |
-
if not (
|
| 450 |
-
self.rule_left.numel() == count
|
| 451 |
-
and self.rule_right.numel() == count
|
| 452 |
-
and self.rule_generation.numel() == count
|
| 453 |
-
and self.rule_active.numel() == count
|
| 454 |
-
and self.span_offsets.numel() == count + 1
|
| 455 |
-
and int(self.span_offsets[0]) == 0
|
| 456 |
-
and int(self.span_offsets[-1]) == self.span_values.numel()
|
| 457 |
-
):
|
| 458 |
-
raise ValueError("recursive rule metadata does not align with learned rows")
|
| 459 |
-
if count and (
|
| 460 |
-
bool((self.rule_generation <= 0).any())
|
| 461 |
-
or bool((self.rule_generation[1:] < self.rule_generation[:-1]).any())
|
| 462 |
-
or int(self.rule_left.max()) >= self.base_size + count
|
| 463 |
-
or int(self.rule_right.max()) >= self.base_size + count
|
| 464 |
-
):
|
| 465 |
-
raise ValueError("recursive rule DAG metadata is invalid")
|
| 466 |
-
self._validate_metadata()
|
| 467 |
-
|
| 468 |
-
self.proposal_merged = nn.Parameter(torch.zeros(self.residual_buckets, self.dim))
|
| 469 |
-
self.proposal_score = nn.Parameter(torch.zeros(self.residual_buckets))
|
| 470 |
-
self.language_bias = nn.Parameter(torch.full((self.n_languages,), -2.0))
|
| 471 |
-
self.discovery: ExactRecursiveDiscovery | None = None
|
| 472 |
-
self.usage_counts: torch.Tensor | None = None
|
| 473 |
-
|
| 474 |
-
@property
|
| 475 |
-
def learned_count(self) -> int:
|
| 476 |
-
return int(self.learned.shape[0])
|
| 477 |
-
|
| 478 |
-
@property
|
| 479 |
-
def generation(self) -> int:
|
| 480 |
-
return int(self.rule_generation[-1]) if self.rule_generation.numel() else 0
|
| 481 |
-
|
| 482 |
-
@property
|
| 483 |
-
def active_count(self) -> int:
|
| 484 |
-
return int(self.rule_active.sum())
|
| 485 |
-
|
| 486 |
-
def configure_discovery(self) -> None:
|
| 487 |
-
self.discovery = ExactRecursiveDiscovery(n_languages=self.n_languages)
|
| 488 |
-
self.usage_counts = torch.zeros(
|
| 489 |
-
self.learned_count, dtype=torch.long, device=self.learned.device
|
| 490 |
-
)
|
| 491 |
-
|
| 492 |
-
def snapshot_usage_token_ids(self) -> tuple[int, ...]:
|
| 493 |
-
if self.usage_counts is None:
|
| 494 |
-
raise RuntimeError("recursive usage collection is not configured")
|
| 495 |
-
rows = torch.nonzero(self.usage_counts > 0, as_tuple=False).squeeze(1)
|
| 496 |
-
return tuple(
|
| 497 |
-
self.base_size + int(row) for row in rows.detach().cpu().tolist()
|
| 498 |
-
)
|
| 499 |
-
|
| 500 |
-
def active_pair_keys(self) -> tuple[int, ...]:
|
| 501 |
-
left = self.rule_left.detach().cpu().tolist()
|
| 502 |
-
right = self.rule_right.detach().cpu().tolist()
|
| 503 |
-
active = self.rule_active.detach().cpu().tolist()
|
| 504 |
-
return tuple(
|
| 505 |
-
recursive_pair_key(int(left[row]), int(right[row]))
|
| 506 |
-
for row, enabled in enumerate(active)
|
| 507 |
-
if enabled
|
| 508 |
-
)
|
| 509 |
-
|
| 510 |
-
def token_span(self, token_id: int) -> tuple[int, ...]:
|
| 511 |
-
token_id = int(token_id)
|
| 512 |
-
if 0 <= token_id < self.base_size:
|
| 513 |
-
return (token_id,)
|
| 514 |
-
row = token_id - self.base_size
|
| 515 |
-
if row < 0 or row >= self.learned_count:
|
| 516 |
-
raise ValueError("recursive token id is outside the vocabulary")
|
| 517 |
-
start, stop = int(self.span_offsets[row]), int(self.span_offsets[row + 1])
|
| 518 |
-
return tuple(int(value) for value in self.span_values[start:stop].tolist())
|
| 519 |
-
|
| 520 |
-
def _validate_metadata(self) -> None:
|
| 521 |
-
seen_pairs: set[tuple[int, int]] = set()
|
| 522 |
-
seen_spans: set[tuple[int, ...]] = set()
|
| 523 |
-
left_values = self.rule_left.detach().cpu().tolist()
|
| 524 |
-
right_values = self.rule_right.detach().cpu().tolist()
|
| 525 |
-
generation_values = self.rule_generation.detach().cpu().tolist()
|
| 526 |
-
active_values = self.rule_active.detach().cpu().tolist()
|
| 527 |
-
offsets = self.span_offsets.detach().cpu().tolist()
|
| 528 |
-
span_values = self.span_values.detach().cpu().tolist()
|
| 529 |
-
row_spans = [
|
| 530 |
-
tuple(int(value) for value in span_values[offsets[row] : offsets[row + 1]])
|
| 531 |
-
for row in range(self.learned_count)
|
| 532 |
-
]
|
| 533 |
-
generations = set(int(value) for value in generation_values)
|
| 534 |
-
if generations and generations != set(range(1, max(generations) + 1)):
|
| 535 |
-
raise ValueError("recursive structural generations are not contiguous")
|
| 536 |
-
for row in range(self.learned_count):
|
| 537 |
-
token_id = self.base_size + row
|
| 538 |
-
left = int(left_values[row])
|
| 539 |
-
right = int(right_values[row])
|
| 540 |
-
generation = int(generation_values[row])
|
| 541 |
-
if left >= token_id or right >= token_id:
|
| 542 |
-
raise ValueError("recursive rule references a non-earlier token")
|
| 543 |
-
left_generation = (
|
| 544 |
-
0 if left < self.base_size else int(generation_values[left - self.base_size])
|
| 545 |
-
)
|
| 546 |
-
right_generation = (
|
| 547 |
-
0 if right < self.base_size else int(generation_values[right - self.base_size])
|
| 548 |
-
)
|
| 549 |
-
if left_generation >= generation or right_generation >= generation:
|
| 550 |
-
raise ValueError("recursive rule parent is not from an earlier generation")
|
| 551 |
-
pair = (left, right)
|
| 552 |
-
span = row_spans[row]
|
| 553 |
-
expected = (
|
| 554 |
-
((left,) if left < self.base_size else row_spans[left - self.base_size])
|
| 555 |
-
+ ((right,) if right < self.base_size else row_spans[right - self.base_size])
|
| 556 |
-
)
|
| 557 |
-
if pair in seen_pairs or span in seen_spans:
|
| 558 |
-
raise ValueError("recursive rule or canonical span is duplicated")
|
| 559 |
-
if span != expected or not span or len(span) > self.max_span_length:
|
| 560 |
-
raise ValueError("recursive canonical span metadata is invalid")
|
| 561 |
-
if min(span) < 0 or max(span) >= self.base_size:
|
| 562 |
-
raise ValueError("recursive canonical span contains a non-base id")
|
| 563 |
-
seen_pairs.add(pair)
|
| 564 |
-
seen_spans.add(span)
|
| 565 |
-
for row in range(self.learned_count):
|
| 566 |
-
if not bool(active_values[row]):
|
| 567 |
-
continue
|
| 568 |
-
for parent in (int(left_values[row]), int(right_values[row])):
|
| 569 |
-
if parent >= self.base_size and not bool(
|
| 570 |
-
active_values[parent - self.base_size]
|
| 571 |
-
):
|
| 572 |
-
raise ValueError("active recursive rule has an inactive parent")
|
| 573 |
-
|
| 574 |
-
def metadata_sha256(self) -> str:
|
| 575 |
-
payload = {
|
| 576 |
-
name: getattr(self, name).detach().cpu().tolist()
|
| 577 |
-
for name in (
|
| 578 |
-
"rule_left", "rule_right", "rule_generation", "span_offsets",
|
| 579 |
-
"span_values", "rule_active",
|
| 580 |
-
)
|
| 581 |
-
}
|
| 582 |
-
return hashlib.sha256(
|
| 583 |
-
json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
| 584 |
-
).hexdigest()
|
| 585 |
-
|
| 586 |
-
def _lookup(
|
| 587 |
-
self, token_ids: torch.Tensor, base_embedding: torch.Tensor
|
| 588 |
-
) -> torch.Tensor:
|
| 589 |
-
if base_embedding.ndim != 2 or tuple(base_embedding.shape) != (
|
| 590 |
-
self.base_size, self.dim
|
| 591 |
-
):
|
| 592 |
-
raise ValueError("base embedding does not match recursive cascade")
|
| 593 |
-
if token_ids.numel() and (
|
| 594 |
-
int(token_ids.min()) < 0
|
| 595 |
-
or int(token_ids.max()) >= self.base_size + self.learned_count
|
| 596 |
-
):
|
| 597 |
-
raise ValueError("recursive token id is outside the vocabulary")
|
| 598 |
-
result = torch.empty(
|
| 599 |
-
(token_ids.numel(), self.dim),
|
| 600 |
-
dtype=base_embedding.dtype,
|
| 601 |
-
device=base_embedding.device,
|
| 602 |
-
)
|
| 603 |
-
base = token_ids < self.base_size
|
| 604 |
-
if bool(base.any()):
|
| 605 |
-
result[base] = base_embedding[token_ids[base]]
|
| 606 |
-
if bool((~base).any()):
|
| 607 |
-
result[~base] = self.learned[token_ids[~base] - self.base_size].to(
|
| 608 |
-
dtype=base_embedding.dtype
|
| 609 |
-
)
|
| 610 |
-
return result
|
| 611 |
-
|
| 612 |
-
@staticmethod
|
| 613 |
-
def _nonoverlapping_leftmost(candidate: torch.Tensor) -> torch.Tensor:
|
| 614 |
-
if candidate.dtype != torch.bool or candidate.ndim != 1:
|
| 615 |
-
raise ValueError("recursive merge candidates must be a boolean vector")
|
| 616 |
-
if not candidate.numel():
|
| 617 |
-
return candidate
|
| 618 |
-
positions = torch.arange(candidate.numel(), device=candidate.device)
|
| 619 |
-
previous_false = torch.cat(
|
| 620 |
-
(torch.ones(1, dtype=torch.bool, device=candidate.device), ~candidate[:-1])
|
| 621 |
-
)
|
| 622 |
-
starts = candidate & previous_false
|
| 623 |
-
start_positions = torch.where(starts, positions, torch.full_like(positions, -1))
|
| 624 |
-
last_start = torch.cummax(start_positions, dim=0).values
|
| 625 |
-
return candidate & ((positions - last_start).remainder(2) == 0)
|
| 626 |
-
|
| 627 |
-
def retokenize(
|
| 628 |
-
self, content: torch.Tensor, segment: torch.Tensor
|
| 629 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 630 |
-
"""Apply the complete rule DAG to one packed batch on its current device."""
|
| 631 |
-
|
| 632 |
-
if content.dtype != torch.long or segment.dtype != torch.long:
|
| 633 |
-
raise ValueError("recursive packed tokens and segments must use torch.long")
|
| 634 |
-
if content.ndim != 1 or segment.shape != content.shape:
|
| 635 |
-
raise ValueError("recursive packed tokens and segments must align")
|
| 636 |
-
if content.numel() and (
|
| 637 |
-
int(content.min()) < 0 or int(content.max()) >= self.base_size
|
| 638 |
-
):
|
| 639 |
-
raise ValueError("recursive tokenizer input must contain base ids only")
|
| 640 |
-
emitted, emitted_segment = content, segment
|
| 641 |
-
for generation in range(1, self.generation + 1):
|
| 642 |
-
if emitted.numel() < 2:
|
| 643 |
-
break
|
| 644 |
-
rows = torch.nonzero(
|
| 645 |
-
(self.rule_generation == generation) & self.rule_active,
|
| 646 |
-
as_tuple=False,
|
| 647 |
-
).squeeze(1)
|
| 648 |
-
if not rows.numel():
|
| 649 |
-
continue
|
| 650 |
-
rule_keys = _recursive_pair_keys(
|
| 651 |
-
self.rule_left[rows], self.rule_right[rows]
|
| 652 |
-
)
|
| 653 |
-
order = torch.argsort(rule_keys, stable=True)
|
| 654 |
-
rule_keys = rule_keys[order]
|
| 655 |
-
rule_ids = rows[order] + self.base_size
|
| 656 |
-
inside = emitted_segment[:-1] == emitted_segment[1:]
|
| 657 |
-
adjacent = _recursive_pair_keys(emitted[:-1], emitted[1:])
|
| 658 |
-
positions = torch.searchsorted(rule_keys, adjacent)
|
| 659 |
-
safe = positions.clamp(max=rule_keys.numel() - 1)
|
| 660 |
-
found = inside & (positions < rule_keys.numel()) & (
|
| 661 |
-
rule_keys[safe] == adjacent
|
| 662 |
-
)
|
| 663 |
-
chosen = self._nonoverlapping_leftmost(found)
|
| 664 |
-
if not bool(chosen.any()):
|
| 665 |
-
continue
|
| 666 |
-
chosen_positions = torch.nonzero(chosen, as_tuple=False).squeeze(1)
|
| 667 |
-
replacement = emitted.clone()
|
| 668 |
-
replacement[chosen_positions] = rule_ids[safe[chosen_positions]]
|
| 669 |
-
keep = torch.ones_like(emitted, dtype=torch.bool)
|
| 670 |
-
keep[chosen_positions + 1] = False
|
| 671 |
-
emitted = replacement[keep]
|
| 672 |
-
emitted_segment = emitted_segment[keep]
|
| 673 |
-
return emitted, emitted_segment
|
| 674 |
-
|
| 675 |
-
def pool(
|
| 676 |
-
self,
|
| 677 |
-
base_embedding: torch.Tensor,
|
| 678 |
-
content: torch.Tensor,
|
| 679 |
-
segment: torch.Tensor,
|
| 680 |
-
language: torch.Tensor,
|
| 681 |
-
n_sentences: int,
|
| 682 |
-
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 683 |
-
"""Retokenize, train emitted rows, and observe next-generation pairs."""
|
| 684 |
-
|
| 685 |
-
emitted, emitted_segment = self.retokenize(content, segment)
|
| 686 |
-
if self.training and self.usage_counts is not None and emitted.numel():
|
| 687 |
-
learned_rows = emitted[emitted >= self.base_size] - self.base_size
|
| 688 |
-
if learned_rows.numel():
|
| 689 |
-
self.usage_counts.index_add_(
|
| 690 |
-
0,
|
| 691 |
-
learned_rows,
|
| 692 |
-
torch.ones_like(learned_rows, dtype=self.usage_counts.dtype),
|
| 693 |
-
)
|
| 694 |
-
vectors = self._lookup(emitted, base_embedding)
|
| 695 |
-
weight = torch.ones(emitted.numel(), dtype=vectors.dtype, device=vectors.device)
|
| 696 |
-
numerator = torch.zeros(
|
| 697 |
-
n_sentences, self.dim, dtype=vectors.dtype, device=vectors.device
|
| 698 |
-
)
|
| 699 |
-
denominator = torch.zeros(n_sentences, dtype=vectors.dtype, device=vectors.device)
|
| 700 |
-
denominator.index_add_(0, emitted_segment, weight)
|
| 701 |
-
if emitted.numel() > 1:
|
| 702 |
-
inside = emitted_segment[:-1] == emitted_segment[1:]
|
| 703 |
-
if bool(inside.any()):
|
| 704 |
-
left, right = emitted[:-1][inside], emitted[1:][inside]
|
| 705 |
-
pair_segment = emitted_segment[:-1][inside]
|
| 706 |
-
pair_language = language[pair_segment]
|
| 707 |
-
pair_key = _recursive_pair_keys(left, right)
|
| 708 |
-
bucket = _proposal_hash(left, right, self.residual_buckets)
|
| 709 |
-
probability = torch.sigmoid(
|
| 710 |
-
self.proposal_score[bucket] + self.language_bias[pair_language]
|
| 711 |
-
)
|
| 712 |
-
if (
|
| 713 |
-
self.discovery is not None
|
| 714 |
-
and self.training
|
| 715 |
-
and probability.requires_grad
|
| 716 |
-
):
|
| 717 |
-
saved_key = pair_key.detach()
|
| 718 |
-
saved_language = pair_language.detach()
|
| 719 |
-
saved_probability = probability.detach()
|
| 720 |
-
|
| 721 |
-
def observe(gradient: torch.Tensor) -> None:
|
| 722 |
-
if self.discovery is not None:
|
| 723 |
-
self.discovery.observe(
|
| 724 |
-
saved_key, saved_language, saved_probability, gradient
|
| 725 |
-
)
|
| 726 |
-
|
| 727 |
-
probability.register_hook(observe)
|
| 728 |
-
positions = torch.nonzero(inside, as_tuple=False).squeeze(1)
|
| 729 |
-
half = 0.5 * probability
|
| 730 |
-
weight = weight.index_add(0, positions, -half)
|
| 731 |
-
weight = weight.index_add(0, positions + 1, -half)
|
| 732 |
-
_weighted_index_add_(
|
| 733 |
-
numerator,
|
| 734 |
-
pair_segment,
|
| 735 |
-
self.proposal_merged[bucket],
|
| 736 |
-
probability,
|
| 737 |
-
)
|
| 738 |
-
denominator.index_add_(0, pair_segment, -probability)
|
| 739 |
-
_weighted_index_add_(numerator, emitted_segment, vectors, weight)
|
| 740 |
-
return (
|
| 741 |
-
numerator / denominator.clamp_min(1e-3).unsqueeze(1),
|
| 742 |
-
emitted,
|
| 743 |
-
emitted_segment,
|
| 744 |
-
)
|
| 745 |
-
|
| 746 |
-
@staticmethod
|
| 747 |
-
def _optimizer_row(
|
| 748 |
-
optimizer: torch.optim.Optimizer,
|
| 749 |
-
parameter: nn.Parameter,
|
| 750 |
-
row: int,
|
| 751 |
-
) -> dict[Any, Any]:
|
| 752 |
-
state = optimizer.state.get(parameter, {})
|
| 753 |
-
if not state:
|
| 754 |
-
raise ValueError("recursive promotion requires initialized parent Adam state")
|
| 755 |
-
result: dict[Any, Any] = {}
|
| 756 |
-
for name, value in state.items():
|
| 757 |
-
if not torch.is_tensor(value):
|
| 758 |
-
result[name] = value
|
| 759 |
-
elif value.ndim == 0:
|
| 760 |
-
result[name] = value.detach().clone()
|
| 761 |
-
elif tuple(value.shape) == tuple(parameter.shape):
|
| 762 |
-
result[name] = value[row].detach().clone()
|
| 763 |
-
else:
|
| 764 |
-
raise ValueError(f"unknown recursive optimizer state shape: {name}")
|
| 765 |
-
return result
|
| 766 |
-
|
| 767 |
-
@staticmethod
|
| 768 |
-
def _average_states(left: dict[Any, Any], right: dict[Any, Any]) -> dict[Any, Any]:
|
| 769 |
-
if set(left) != set(right):
|
| 770 |
-
raise ValueError("recursive parent Adam states differ")
|
| 771 |
-
result: dict[Any, Any] = {}
|
| 772 |
-
for name in left:
|
| 773 |
-
a, b = left[name], right[name]
|
| 774 |
-
if torch.is_tensor(a) != torch.is_tensor(b):
|
| 775 |
-
raise ValueError(f"recursive parent Adam state type differs: {name}")
|
| 776 |
-
if not torch.is_tensor(a):
|
| 777 |
-
if a != b:
|
| 778 |
-
raise ValueError(f"recursive parent Adam scalar differs: {name}")
|
| 779 |
-
result[name] = a
|
| 780 |
-
elif a.ndim == 0:
|
| 781 |
-
if not torch.equal(a, b):
|
| 782 |
-
raise ValueError(f"recursive parent Adam step differs: {name}")
|
| 783 |
-
result[name] = a.detach().clone()
|
| 784 |
-
else:
|
| 785 |
-
if a.shape != b.shape:
|
| 786 |
-
raise ValueError(f"recursive parent Adam row differs: {name}")
|
| 787 |
-
result[name] = (0.5 * (a + b)).detach().clone()
|
| 788 |
-
return result
|
| 789 |
-
|
| 790 |
-
def _parent_state(
|
| 791 |
-
self,
|
| 792 |
-
optimizer: torch.optim.Optimizer,
|
| 793 |
-
base_embedding: nn.Parameter,
|
| 794 |
-
token_id: int,
|
| 795 |
-
) -> dict[Any, Any]:
|
| 796 |
-
if token_id < self.base_size:
|
| 797 |
-
return self._optimizer_row(optimizer, base_embedding, token_id)
|
| 798 |
-
return self._optimizer_row(
|
| 799 |
-
optimizer, self.learned, token_id - self.base_size
|
| 800 |
-
)
|
| 801 |
-
|
| 802 |
-
def promotable_pair_keys(
|
| 803 |
-
self, pair_keys: Iterable[int]
|
| 804 |
-
) -> tuple[tuple[int, ...], dict[str, int]]:
|
| 805 |
-
existing_pairs = {
|
| 806 |
-
recursive_pair_key(int(left), int(right))
|
| 807 |
-
for left, right in zip(
|
| 808 |
-
self.rule_left.detach().cpu().tolist(),
|
| 809 |
-
self.rule_right.detach().cpu().tolist(),
|
| 810 |
-
strict=True,
|
| 811 |
-
)
|
| 812 |
-
}
|
| 813 |
-
existing_spans = {
|
| 814 |
-
self.token_span(token_id)
|
| 815 |
-
for token_id in range(self.base_size, self.base_size + self.learned_count)
|
| 816 |
-
}
|
| 817 |
-
accepted: list[int] = []
|
| 818 |
-
rejections = {
|
| 819 |
-
"already_rule": 0,
|
| 820 |
-
"unknown_parent": 0,
|
| 821 |
-
"duplicate_span": 0,
|
| 822 |
-
"span_too_long": 0,
|
| 823 |
-
}
|
| 824 |
-
total = self.base_size + self.learned_count
|
| 825 |
-
for key in sorted({int(value) for value in pair_keys}):
|
| 826 |
-
left, right = decode_recursive_pair_key(key)
|
| 827 |
-
if key in existing_pairs:
|
| 828 |
-
rejections["already_rule"] += 1
|
| 829 |
-
continue
|
| 830 |
-
if left >= total or right >= total:
|
| 831 |
-
rejections["unknown_parent"] += 1
|
| 832 |
-
continue
|
| 833 |
-
span = self.token_span(left) + self.token_span(right)
|
| 834 |
-
if len(span) > self.max_span_length:
|
| 835 |
-
rejections["span_too_long"] += 1
|
| 836 |
-
continue
|
| 837 |
-
if span in existing_spans:
|
| 838 |
-
rejections["duplicate_span"] += 1
|
| 839 |
-
continue
|
| 840 |
-
accepted.append(key)
|
| 841 |
-
existing_spans.add(span)
|
| 842 |
-
return tuple(accepted), rejections
|
| 843 |
-
|
| 844 |
-
def deactivate_unobserved(
|
| 845 |
-
self, observed_token_ids: Iterable[int]
|
| 846 |
-
) -> dict[str, Any]:
|
| 847 |
-
"""Deactivate the exact active sub-DAG absent from both audit windows.
|
| 848 |
-
|
| 849 |
-
Every active ancestor of an observed learned token is retained. All
|
| 850 |
-
remaining active rules can be disabled without changing either observed
|
| 851 |
-
token stream: they were neither emitted nor needed to emit a descendant.
|
| 852 |
-
Physical rows and Adam state remain stable for cheap future reactivation.
|
| 853 |
-
"""
|
| 854 |
-
|
| 855 |
-
observed = sorted({int(value) for value in observed_token_ids})
|
| 856 |
-
if any(
|
| 857 |
-
token_id < self.base_size
|
| 858 |
-
or token_id >= self.base_size + self.learned_count
|
| 859 |
-
for token_id in observed
|
| 860 |
-
):
|
| 861 |
-
raise ValueError("recursive usage contains an unknown learned token")
|
| 862 |
-
active = self.rule_active.detach().cpu().tolist()
|
| 863 |
-
left = self.rule_left.detach().cpu().tolist()
|
| 864 |
-
right = self.rule_right.detach().cpu().tolist()
|
| 865 |
-
keep = [False] * self.learned_count
|
| 866 |
-
if observed:
|
| 867 |
-
rows = [token_id - self.base_size for token_id in observed]
|
| 868 |
-
if not all(active[row] for row in rows):
|
| 869 |
-
raise ValueError("recursive usage contains an inactive token")
|
| 870 |
-
for row in rows:
|
| 871 |
-
keep[row] = True
|
| 872 |
-
# Child ids are always larger than learned parent ids, so one reverse
|
| 873 |
-
# pass closes the retained set over every active learned ancestor.
|
| 874 |
-
for row in range(self.learned_count - 1, -1, -1):
|
| 875 |
-
if not keep[row]:
|
| 876 |
-
continue
|
| 877 |
-
for parent in (int(left[row]), int(right[row])):
|
| 878 |
-
if parent >= self.base_size:
|
| 879 |
-
keep[parent - self.base_size] = True
|
| 880 |
-
rows = [
|
| 881 |
-
row for row, enabled in enumerate(active) if enabled and not keep[row]
|
| 882 |
-
]
|
| 883 |
-
if rows:
|
| 884 |
-
device_rows = torch.tensor(
|
| 885 |
-
rows, dtype=torch.long, device=self.rule_active.device
|
| 886 |
-
)
|
| 887 |
-
self.rule_active[device_rows] = False
|
| 888 |
-
token_ids = [self.base_size + row for row in rows]
|
| 889 |
-
return {
|
| 890 |
-
"deactivated_rows": len(token_ids),
|
| 891 |
-
"active_rows": self.active_count,
|
| 892 |
-
"token_ids_sha256": hashlib.sha256(
|
| 893 |
-
json.dumps(token_ids, separators=(",", ":")).encode("utf-8")
|
| 894 |
-
).hexdigest(),
|
| 895 |
-
}
|
| 896 |
-
|
| 897 |
-
def reactivate_pair_keys(self, pair_keys: Iterable[int]) -> dict[str, Any]:
|
| 898 |
-
"""Reactivate inactive exact rules whose complete parent path is active."""
|
| 899 |
-
|
| 900 |
-
left_values = self.rule_left.detach().cpu().tolist()
|
| 901 |
-
right_values = self.rule_right.detach().cpu().tolist()
|
| 902 |
-
by_key = {
|
| 903 |
-
recursive_pair_key(int(left), int(right)): row
|
| 904 |
-
for row, (left, right) in enumerate(
|
| 905 |
-
zip(left_values, right_values, strict=True)
|
| 906 |
-
)
|
| 907 |
-
}
|
| 908 |
-
reactivated_keys: list[int] = []
|
| 909 |
-
reactivated_ids: list[int] = []
|
| 910 |
-
rejected_inactive_parent = 0
|
| 911 |
-
active = self.rule_active.detach().cpu().tolist()
|
| 912 |
-
for key in sorted({int(value) for value in pair_keys}):
|
| 913 |
-
row = by_key.get(key)
|
| 914 |
-
if row is None or bool(active[row]):
|
| 915 |
-
continue
|
| 916 |
-
parents = (int(left_values[row]), int(right_values[row]))
|
| 917 |
-
if any(
|
| 918 |
-
parent >= self.base_size
|
| 919 |
-
and not bool(active[parent - self.base_size])
|
| 920 |
-
for parent in parents
|
| 921 |
-
):
|
| 922 |
-
rejected_inactive_parent += 1
|
| 923 |
-
continue
|
| 924 |
-
active[row] = True
|
| 925 |
-
reactivated_keys.append(key)
|
| 926 |
-
reactivated_ids.append(self.base_size + row)
|
| 927 |
-
if reactivated_ids:
|
| 928 |
-
rows = torch.tensor(
|
| 929 |
-
[token_id - self.base_size for token_id in reactivated_ids],
|
| 930 |
-
dtype=torch.long,
|
| 931 |
-
device=self.rule_active.device,
|
| 932 |
-
)
|
| 933 |
-
self.rule_active[rows] = True
|
| 934 |
-
return {
|
| 935 |
-
"reactivated_rows": len(reactivated_ids),
|
| 936 |
-
"active_rows": self.active_count,
|
| 937 |
-
"pair_keys": reactivated_keys,
|
| 938 |
-
"token_ids": reactivated_ids,
|
| 939 |
-
"rejected_inactive_parent": rejected_inactive_parent,
|
| 940 |
-
}
|
| 941 |
-
|
| 942 |
-
def grow(
|
| 943 |
-
self,
|
| 944 |
-
pair_keys: Iterable[int],
|
| 945 |
-
*,
|
| 946 |
-
base_embedding: nn.Parameter,
|
| 947 |
-
optimizer: torch.optim.Optimizer,
|
| 948 |
-
initialization: str = "parent_mean",
|
| 949 |
-
) -> dict[str, Any]:
|
| 950 |
-
"""Atomically append one dense generation and migrate live Adam state."""
|
| 951 |
-
|
| 952 |
-
if initialization not in {"parent_mean", "parent_sum"}:
|
| 953 |
-
raise ValueError("unknown recursive row initialization")
|
| 954 |
-
|
| 955 |
-
accepted, rejections = self.promotable_pair_keys(pair_keys)
|
| 956 |
-
if not accepted:
|
| 957 |
-
return {
|
| 958 |
-
"generation": self.generation,
|
| 959 |
-
"added_rows": 0,
|
| 960 |
-
"learned_rows": self.learned_count,
|
| 961 |
-
"active_rows": self.active_count,
|
| 962 |
-
"rejections": rejections,
|
| 963 |
-
}
|
| 964 |
-
pairs = tuple(decode_recursive_pair_key(key) for key in accepted)
|
| 965 |
-
old_parameter = self.learned
|
| 966 |
-
old_count = self.learned_count
|
| 967 |
-
new_values = []
|
| 968 |
-
new_states = []
|
| 969 |
-
spans = []
|
| 970 |
-
for left, right in pairs:
|
| 971 |
-
parent_ids = torch.tensor(
|
| 972 |
-
[left, right], dtype=torch.long, device=base_embedding.device
|
| 973 |
-
)
|
| 974 |
-
parent_values = self._lookup(parent_ids, base_embedding)
|
| 975 |
-
new_values.append(
|
| 976 |
-
parent_values.sum(dim=0)
|
| 977 |
-
if initialization == "parent_sum"
|
| 978 |
-
else parent_values.mean(dim=0)
|
| 979 |
-
)
|
| 980 |
-
new_states.append(
|
| 981 |
-
self._average_states(
|
| 982 |
-
self._parent_state(optimizer, base_embedding, left),
|
| 983 |
-
self._parent_state(optimizer, base_embedding, right),
|
| 984 |
-
)
|
| 985 |
-
)
|
| 986 |
-
spans.append(self.token_span(left) + self.token_span(right))
|
| 987 |
-
new_parameter = nn.Parameter(
|
| 988 |
-
torch.cat((old_parameter.detach(), torch.stack(new_values)), dim=0),
|
| 989 |
-
requires_grad=True if not old_count else old_parameter.requires_grad,
|
| 990 |
-
)
|
| 991 |
-
|
| 992 |
-
occurrences = sum(
|
| 993 |
-
candidate is old_parameter
|
| 994 |
-
for group in optimizer.param_groups
|
| 995 |
-
for candidate in group["params"]
|
| 996 |
-
)
|
| 997 |
-
if occurrences not in ({0, 1} if not old_count else {1}):
|
| 998 |
-
raise ValueError("recursive learned table must occur once in the optimizer")
|
| 999 |
-
old_state = optimizer.state.get(old_parameter, {})
|
| 1000 |
-
migrated: dict[Any, Any] = {}
|
| 1001 |
-
for name in new_states[0]:
|
| 1002 |
-
additions = [state[name] for state in new_states]
|
| 1003 |
-
first = additions[0]
|
| 1004 |
-
if not torch.is_tensor(first):
|
| 1005 |
-
if any(value != first for value in additions[1:]):
|
| 1006 |
-
raise ValueError(f"recursive added Adam scalar differs: {name}")
|
| 1007 |
-
migrated[name] = first
|
| 1008 |
-
elif first.ndim == 0:
|
| 1009 |
-
if any(not torch.equal(value, first) for value in additions[1:]):
|
| 1010 |
-
raise ValueError(f"recursive added Adam step differs: {name}")
|
| 1011 |
-
if old_state and not torch.equal(old_state[name], first):
|
| 1012 |
-
raise ValueError(f"recursive existing/added Adam step differs: {name}")
|
| 1013 |
-
migrated[name] = first.detach().clone()
|
| 1014 |
-
else:
|
| 1015 |
-
added = torch.stack(additions)
|
| 1016 |
-
if old_count:
|
| 1017 |
-
old_value = old_state.get(name)
|
| 1018 |
-
if (
|
| 1019 |
-
not torch.is_tensor(old_value)
|
| 1020 |
-
or tuple(old_value.shape) != tuple(old_parameter.shape)
|
| 1021 |
-
):
|
| 1022 |
-
raise ValueError(f"recursive existing Adam rows differ: {name}")
|
| 1023 |
-
added = added.to(device=old_value.device, dtype=old_value.dtype)
|
| 1024 |
-
migrated[name] = torch.cat((old_value.detach(), added), dim=0)
|
| 1025 |
-
else:
|
| 1026 |
-
migrated[name] = added
|
| 1027 |
-
for group in optimizer.param_groups:
|
| 1028 |
-
replaced: list[nn.Parameter] = []
|
| 1029 |
-
for parameter in group["params"]:
|
| 1030 |
-
replaced.append(
|
| 1031 |
-
new_parameter if parameter is old_parameter else parameter
|
| 1032 |
-
)
|
| 1033 |
-
if occurrences == 0 and parameter is base_embedding:
|
| 1034 |
-
replaced.append(new_parameter)
|
| 1035 |
-
group["params"] = replaced
|
| 1036 |
-
optimizer.state.pop(old_parameter, None)
|
| 1037 |
-
optimizer.state[new_parameter] = migrated
|
| 1038 |
-
self.learned = new_parameter
|
| 1039 |
-
|
| 1040 |
-
device = self.rule_left.device
|
| 1041 |
-
self.rule_left = torch.cat(
|
| 1042 |
-
(self.rule_left, torch.tensor([left for left, _ in pairs], device=device))
|
| 1043 |
-
)
|
| 1044 |
-
self.rule_right = torch.cat(
|
| 1045 |
-
(self.rule_right, torch.tensor([right for _, right in pairs], device=device))
|
| 1046 |
-
)
|
| 1047 |
-
new_generation = self.generation + 1
|
| 1048 |
-
self.rule_generation = torch.cat(
|
| 1049 |
-
(
|
| 1050 |
-
self.rule_generation,
|
| 1051 |
-
torch.full((len(pairs),), new_generation, dtype=torch.long, device=device),
|
| 1052 |
-
)
|
| 1053 |
-
)
|
| 1054 |
-
self.rule_active = torch.cat(
|
| 1055 |
-
(
|
| 1056 |
-
self.rule_active,
|
| 1057 |
-
torch.ones(len(pairs), dtype=torch.bool, device=device),
|
| 1058 |
-
)
|
| 1059 |
-
)
|
| 1060 |
-
span_values = [value for span in spans for value in span]
|
| 1061 |
-
lengths = torch.tensor([len(span) for span in spans], dtype=torch.long, device=device)
|
| 1062 |
-
appended_offsets = self.span_offsets[-1] + lengths.cumsum(0)
|
| 1063 |
-
self.span_offsets = torch.cat((self.span_offsets, appended_offsets))
|
| 1064 |
-
self.span_values = torch.cat(
|
| 1065 |
-
(self.span_values, torch.tensor(span_values, dtype=torch.long, device=device))
|
| 1066 |
-
)
|
| 1067 |
-
self.discovery = None
|
| 1068 |
-
return {
|
| 1069 |
-
"generation": new_generation,
|
| 1070 |
-
"added_rows": len(pairs),
|
| 1071 |
-
"learned_rows": self.learned_count,
|
| 1072 |
-
"active_rows": self.active_count,
|
| 1073 |
-
"token_ids": list(range(self.base_size + old_count, self.base_size + self.learned_count)),
|
| 1074 |
-
"pairs": [[left, right] for left, right in pairs],
|
| 1075 |
-
"rejections": rejections,
|
| 1076 |
-
"initialization": initialization,
|
| 1077 |
-
"metadata_sha256": self.metadata_sha256(),
|
| 1078 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/split_vocabulary.py
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
"""A second vocabulary for contested (language, token) pairs.
|
| 2 |
-
|
| 3 |
-
Every other conditioning mechanism in this repo failed for one measured reason:
|
| 4 |
-
the shared table rewrites about 85% of a row's magnitude in 3,000 steps, so it
|
| 5 |
-
absorbs any per-language transform a head could learn and leaves the head
|
| 6 |
-
redundant.
|
| 7 |
-
|
| 8 |
-
This does something different. A selected (language, token) pair gets its **own
|
| 9 |
-
row**, and a learned gate mixes it with the shared row:
|
| 10 |
-
|
| 11 |
-
E'[w, l] = (1 - g) * E_shared[w] + g * E_split[idx(l, w)]
|
| 12 |
-
|
| 13 |
-
The split row is free capacity, not a correction, so there is nothing for the
|
| 14 |
-
shared table to absorb. Split rows start as exact copies of their shared row, so
|
| 15 |
-
the model is numerically identical to the unconditioned one at step zero
|
| 16 |
-
whatever the gate says, and both paths receive gradient immediately — the
|
| 17 |
-
failure mode where a zero-initialised head never leaves zero cannot occur.
|
| 18 |
-
|
| 19 |
-
An L1 penalty on the gate makes a split row earn its keep. After training the
|
| 20 |
-
gate is the answer to the scientific question: which pairs actually needed
|
| 21 |
-
separating, and does that agree with the interference measured beforehand.
|
| 22 |
-
"""
|
| 23 |
-
|
| 24 |
-
from __future__ import annotations
|
| 25 |
-
|
| 26 |
-
import numpy as np
|
| 27 |
-
import torch
|
| 28 |
-
from torch import nn
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
class SplitVocabulary(nn.Module):
|
| 32 |
-
def __init__(self, index_path: str, vocab_size: int, dim: int, n_languages: int,
|
| 33 |
-
gate_init: float = 0.0) -> None:
|
| 34 |
-
super().__init__()
|
| 35 |
-
data = np.load(index_path)
|
| 36 |
-
language_index = torch.tensor(data["language_index"].astype(np.int64))
|
| 37 |
-
token_index = torch.tensor(data["token_index"].astype(np.int64))
|
| 38 |
-
self.n_rows = int(language_index.numel())
|
| 39 |
-
|
| 40 |
-
# (language, token) -> split row, as one flat lookup. -1 means "shared
|
| 41 |
-
# only", which is the vast majority of pairs.
|
| 42 |
-
lookup = torch.full((n_languages, vocab_size), -1, dtype=torch.long)
|
| 43 |
-
lookup[language_index, token_index] = torch.arange(self.n_rows)
|
| 44 |
-
self.register_buffer("lookup", lookup, persistent=False)
|
| 45 |
-
self.register_buffer("row_language", language_index, persistent=False)
|
| 46 |
-
self.register_buffer("row_token", token_index, persistent=False)
|
| 47 |
-
|
| 48 |
-
self.rows = nn.Parameter(torch.zeros(self.n_rows, dim))
|
| 49 |
-
self.gate_logit = nn.Parameter(torch.full((self.n_rows,), float(gate_init)))
|
| 50 |
-
self._initialised = False
|
| 51 |
-
|
| 52 |
-
@torch.no_grad()
|
| 53 |
-
def seed_from(self, table: torch.Tensor) -> None:
|
| 54 |
-
"""Copy each split row from the shared row it specialises."""
|
| 55 |
-
self.rows.copy_(table[self.row_token])
|
| 56 |
-
self._initialised = True
|
| 57 |
-
|
| 58 |
-
def gate(self) -> torch.Tensor:
|
| 59 |
-
return torch.sigmoid(self.gate_logit)
|
| 60 |
-
|
| 61 |
-
def penalty(self) -> torch.Tensor:
|
| 62 |
-
"""Mean gate opening — an L1 that pushes unused rows back to shared."""
|
| 63 |
-
return self.gate().mean()
|
| 64 |
-
|
| 65 |
-
def fusion_penalty(self) -> torch.Tensor:
|
| 66 |
-
"""Pull every language's row for a token toward the consensus for it.
|
| 67 |
-
|
| 68 |
-
This is the merging mechanism. Training starts with one private row per
|
| 69 |
-
(language, token) — full capacity, no interference — and this term makes
|
| 70 |
-
a row stay distinct only if the contrastive loss pays for it. Sweeping
|
| 71 |
-
its weight walks continuously from one vocabulary per language to a
|
| 72 |
-
single shared one, so the *effective* number of vocabularies is read off
|
| 73 |
-
rather than chosen.
|
| 74 |
-
|
| 75 |
-
Fusing toward the per-token mean is the O(L) relaxation of the O(L^2)
|
| 76 |
-
all-pairs fused lasso; rows that need not differ collapse onto the
|
| 77 |
-
centroid, and which languages stay off it together is the clustering.
|
| 78 |
-
"""
|
| 79 |
-
token = self.row_token
|
| 80 |
-
uniq, inverse = torch.unique(token, return_inverse=True)
|
| 81 |
-
total = torch.zeros(uniq.numel(), self.rows.shape[1],
|
| 82 |
-
dtype=self.rows.dtype, device=self.rows.device)
|
| 83 |
-
total.index_add_(0, inverse, self.rows)
|
| 84 |
-
count = torch.zeros(uniq.numel(), dtype=self.rows.dtype, device=self.rows.device)
|
| 85 |
-
count.index_add_(0, inverse, torch.ones_like(inverse, dtype=self.rows.dtype))
|
| 86 |
-
centroid = total / count.clamp(min=1.0).unsqueeze(1)
|
| 87 |
-
# Group lasso over languages: the L2 norm of each deviation, averaged.
|
| 88 |
-
return (self.rows - centroid[inverse]).norm(dim=1).mean()
|
| 89 |
-
|
| 90 |
-
def forward(self, token_ids: torch.Tensor, lang_per_token: torch.Tensor,
|
| 91 |
-
shared: torch.Tensor, use_gate: bool = True) -> torch.Tensor:
|
| 92 |
-
row = self.lookup[lang_per_token, token_ids]
|
| 93 |
-
has = row >= 0
|
| 94 |
-
if not bool(has.any()):
|
| 95 |
-
return shared
|
| 96 |
-
safe = row.clamp(min=0)
|
| 97 |
-
if not use_gate:
|
| 98 |
-
# Fusion mode: the private row replaces the shared one outright, and
|
| 99 |
-
# merging is driven by the penalty rather than by a mixing gate.
|
| 100 |
-
return torch.where(has.unsqueeze(1), self.rows[safe], shared)
|
| 101 |
-
g = self.gate()[safe].unsqueeze(1) * has.unsqueeze(1)
|
| 102 |
-
return shared * (1.0 - g) + self.rows[safe] * g
|
| 103 |
-
|
| 104 |
-
def extra_repr(self) -> str:
|
| 105 |
-
return f"rows={self.n_rows}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/streaming.py
DELETED
|
@@ -1,1023 +0,0 @@
|
|
| 1 |
-
"""Random, background-prefetched Parquet streams for multilingual training."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import bisect
|
| 6 |
-
import hashlib
|
| 7 |
-
import os
|
| 8 |
-
import queue
|
| 9 |
-
import random
|
| 10 |
-
import re
|
| 11 |
-
import threading
|
| 12 |
-
import unicodedata
|
| 13 |
-
from collections.abc import Iterator, Sequence
|
| 14 |
-
from dataclasses import dataclass
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
from typing import Any
|
| 17 |
-
|
| 18 |
-
import numpy as np
|
| 19 |
-
import pyarrow.parquet as pq
|
| 20 |
-
|
| 21 |
-
from starse.weighted_schedule import SmoothWeightedRoundRobin
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
@dataclass(frozen=True)
|
| 25 |
-
class PairShard:
|
| 26 |
-
source: str
|
| 27 |
-
path: str
|
| 28 |
-
anchor_column: str
|
| 29 |
-
positive_column: str
|
| 30 |
-
sampling_weight: float = 1.0
|
| 31 |
-
language_id: str = ""
|
| 32 |
-
# For a parallel stream the two sides are in different languages, so each
|
| 33 |
-
# needs its own marker. Defaults to language_id, i.e. a monolingual pair.
|
| 34 |
-
anchor_language_id: str = ""
|
| 35 |
-
usable_rows: int = 0
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
_WORD_RE = re.compile(r"[^\W\d_]+", re.UNICODE)
|
| 39 |
-
_PUNCTUATION_RE = re.compile(r"^[^\w]+$", re.UNICODE)
|
| 40 |
-
_UNSEGMENTED_SCRIPTS = {"Hani", "Hans", "Hant", "Jpan", "Khmr", "Laoo", "Mymr", "Thai"}
|
| 41 |
-
|
| 42 |
-
# simplemma uses BCP-47/ISO-639-1 for most languages while FineWeb2 directory
|
| 43 |
-
# names use Glottocode-style ISO-639-3 language identifiers.
|
| 44 |
-
_SIMPLEMMA_LANGUAGE = {
|
| 45 |
-
"ast": "ast",
|
| 46 |
-
"bul": "bg",
|
| 47 |
-
"cat": "ca",
|
| 48 |
-
"ces": "cs",
|
| 49 |
-
"cym": "cy",
|
| 50 |
-
"dan": "da",
|
| 51 |
-
"deu": "de",
|
| 52 |
-
"ekk": "et",
|
| 53 |
-
"ell": "el",
|
| 54 |
-
"eng": "en",
|
| 55 |
-
"enm": "enm",
|
| 56 |
-
"epo": "eo",
|
| 57 |
-
"est": "et",
|
| 58 |
-
"fas": "fa",
|
| 59 |
-
"fil": "tl",
|
| 60 |
-
"fin": "fi",
|
| 61 |
-
"fra": "fr",
|
| 62 |
-
"gla": "gd",
|
| 63 |
-
"gle": "ga",
|
| 64 |
-
"glg": "gl",
|
| 65 |
-
"glv": "gv",
|
| 66 |
-
"hbs": "hbs",
|
| 67 |
-
"hin": "hi",
|
| 68 |
-
"hrv": "hbs",
|
| 69 |
-
"hun": "hu",
|
| 70 |
-
"hye": "hy",
|
| 71 |
-
"ind": "id",
|
| 72 |
-
"isl": "is",
|
| 73 |
-
"ita": "it",
|
| 74 |
-
"kat": "ka",
|
| 75 |
-
"lat": "la",
|
| 76 |
-
"lit": "lt",
|
| 77 |
-
"ltz": "lb",
|
| 78 |
-
"lvs": "lv",
|
| 79 |
-
"mkd": "mk",
|
| 80 |
-
"nld": "nl",
|
| 81 |
-
"nno": "nn",
|
| 82 |
-
"nob": "nb",
|
| 83 |
-
"pol": "pl",
|
| 84 |
-
"por": "pt",
|
| 85 |
-
"ron": "ro",
|
| 86 |
-
"rus": "ru",
|
| 87 |
-
"slk": "sk",
|
| 88 |
-
"slv": "sl",
|
| 89 |
-
"sme": "se",
|
| 90 |
-
"spa": "es",
|
| 91 |
-
"sqi": "sq",
|
| 92 |
-
"srp": "hbs",
|
| 93 |
-
"swe": "sv",
|
| 94 |
-
"swh": "sw",
|
| 95 |
-
"tur": "tr",
|
| 96 |
-
"ukr": "uk",
|
| 97 |
-
"zsm": "ms",
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
# Extra mappings are useful for stop-word removal even when simplemma has no
|
| 101 |
-
# dictionary for a language.
|
| 102 |
-
_STOPWORD_LANGUAGE = {
|
| 103 |
-
**_SIMPLEMMA_LANGUAGE,
|
| 104 |
-
"afr": "af",
|
| 105 |
-
"arb": "ar",
|
| 106 |
-
"ben": "bn",
|
| 107 |
-
"eus": "eu",
|
| 108 |
-
"heb": "he",
|
| 109 |
-
"jpn": "ja",
|
| 110 |
-
"kor": "ko",
|
| 111 |
-
"tha": "th",
|
| 112 |
-
"vie": "vi",
|
| 113 |
-
"yue": "zh",
|
| 114 |
-
"cmn": "zh",
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def _language_code(language_id: str) -> str:
|
| 119 |
-
return language_id.split("_", 1)[0].casefold()
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def _canonical_text(text: str) -> str:
|
| 123 |
-
return " ".join(unicodedata.normalize("NFKC", text).casefold().split())
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
class _LemmaOverlapFilter:
|
| 127 |
-
"""StaRSE-style Dice overlap over lemma sets, computed online per pair."""
|
| 128 |
-
|
| 129 |
-
def __init__(self, settings: dict[str, Any] | None) -> None:
|
| 130 |
-
settings = dict(settings or {})
|
| 131 |
-
self.enabled = bool(settings.get("enabled", False))
|
| 132 |
-
self.minimum = float(settings.get("min_overlap", 0.0))
|
| 133 |
-
self.strict = bool(settings.get("strict", True))
|
| 134 |
-
self.remove_stopwords = bool(settings.get("remove_stopwords", True))
|
| 135 |
-
self.fallback = str(settings.get("unsupported_language_fallback", "surface"))
|
| 136 |
-
if not 0.0 <= self.minimum <= 1.0:
|
| 137 |
-
raise ValueError("lemma_filter.min_overlap must be between 0 and 1")
|
| 138 |
-
if self.fallback not in {"surface", "reject"}:
|
| 139 |
-
raise ValueError("lemma_filter.unsupported_language_fallback must be surface or reject")
|
| 140 |
-
self._lemmatizer: Any | None = None
|
| 141 |
-
self._russian_morph: Any | None = None
|
| 142 |
-
self._russian_lemma_cache: dict[str, str] = {}
|
| 143 |
-
self._japanese_tokenizer: Any | None = None
|
| 144 |
-
self._japanese_split_mode: Any | None = None
|
| 145 |
-
self._stopwords: dict[str, frozenset[str]] = {}
|
| 146 |
-
|
| 147 |
-
def _get_lemmatizer(self) -> Any:
|
| 148 |
-
if self._lemmatizer is None:
|
| 149 |
-
from simplemma import Lemmatizer
|
| 150 |
-
from simplemma.strategies import DefaultDictionaryFactory, DefaultStrategy
|
| 151 |
-
|
| 152 |
-
# One dictionary per worker bounds RAM. Rows arrive in shard-sized
|
| 153 |
-
# fragments, so dictionary loading is amortized over many pairs.
|
| 154 |
-
dictionary_factory = DefaultDictionaryFactory(cache_max_size=1)
|
| 155 |
-
self._lemmatizer = Lemmatizer(
|
| 156 |
-
cache_max_size=131_072,
|
| 157 |
-
lemmatization_strategy=DefaultStrategy(dictionary_factory=dictionary_factory),
|
| 158 |
-
)
|
| 159 |
-
return self._lemmatizer
|
| 160 |
-
|
| 161 |
-
def _get_stopwords(self, language_code: str) -> frozenset[str]:
|
| 162 |
-
stopword_language = _STOPWORD_LANGUAGE.get(language_code, "")
|
| 163 |
-
if not self.remove_stopwords or not stopword_language:
|
| 164 |
-
return frozenset()
|
| 165 |
-
if stopword_language not in self._stopwords:
|
| 166 |
-
from stopwordsiso import stopwords
|
| 167 |
-
|
| 168 |
-
words = (unicodedata.normalize("NFKC", word).casefold() for word in stopwords(stopword_language))
|
| 169 |
-
self._stopwords[stopword_language] = frozenset(words)
|
| 170 |
-
return self._stopwords[stopword_language]
|
| 171 |
-
|
| 172 |
-
def _russian_lemma(self, token: str) -> str:
|
| 173 |
-
cached = self._russian_lemma_cache.get(token)
|
| 174 |
-
if cached is not None:
|
| 175 |
-
return cached
|
| 176 |
-
if self._russian_morph is None:
|
| 177 |
-
import pymorphy3
|
| 178 |
-
|
| 179 |
-
self._russian_morph = pymorphy3.MorphAnalyzer()
|
| 180 |
-
parsed = self._russian_morph.parse(token)
|
| 181 |
-
lemma = parsed[0].normal_form.casefold().replace("ё", "е") if parsed else token
|
| 182 |
-
if len(self._russian_lemma_cache) < 2_000_000:
|
| 183 |
-
self._russian_lemma_cache[token] = lemma
|
| 184 |
-
return lemma
|
| 185 |
-
|
| 186 |
-
def _japanese_terms(self, text: str) -> set[str]:
|
| 187 |
-
if self._japanese_tokenizer is None:
|
| 188 |
-
from sudachipy import dictionary, tokenizer
|
| 189 |
-
|
| 190 |
-
self._japanese_tokenizer = dictionary.Dictionary().create()
|
| 191 |
-
self._japanese_split_mode = tokenizer.Tokenizer.SplitMode.C
|
| 192 |
-
|
| 193 |
-
stopwords = self._get_stopwords("jpn")
|
| 194 |
-
ignored_pos = {"助詞", "助動詞", "補助記号", "空白"}
|
| 195 |
-
terms: set[str] = set()
|
| 196 |
-
normalized = unicodedata.normalize("NFKC", text)
|
| 197 |
-
for morpheme in self._japanese_tokenizer.tokenize(normalized, self._japanese_split_mode):
|
| 198 |
-
if morpheme.part_of_speech()[0] in ignored_pos:
|
| 199 |
-
continue
|
| 200 |
-
surface = morpheme.surface().casefold().strip()
|
| 201 |
-
lemma = morpheme.dictionary_form().casefold().strip()
|
| 202 |
-
if not lemma or lemma == "*":
|
| 203 |
-
lemma = surface
|
| 204 |
-
if lemma and lemma not in stopwords and not _PUNCTUATION_RE.fullmatch(lemma):
|
| 205 |
-
terms.add(lemma)
|
| 206 |
-
return terms
|
| 207 |
-
|
| 208 |
-
@staticmethod
|
| 209 |
-
def _character_bigrams(text: str) -> set[str]:
|
| 210 |
-
letters = [
|
| 211 |
-
character
|
| 212 |
-
for character in unicodedata.normalize("NFKC", text).casefold()
|
| 213 |
-
if unicodedata.category(character).startswith("L")
|
| 214 |
-
]
|
| 215 |
-
if len(letters) < 2:
|
| 216 |
-
return set(letters)
|
| 217 |
-
return {"".join(letters[index : index + 2]) for index in range(len(letters) - 1)}
|
| 218 |
-
|
| 219 |
-
def _terms(self, text: str, language_id: str) -> set[str]:
|
| 220 |
-
language_code, _, script = language_id.partition("_")
|
| 221 |
-
language_code = language_code.casefold()
|
| 222 |
-
if language_code == "jpn":
|
| 223 |
-
return self._japanese_terms(text)
|
| 224 |
-
if script in _UNSEGMENTED_SCRIPTS:
|
| 225 |
-
return self._character_bigrams(text)
|
| 226 |
-
|
| 227 |
-
lemmatizer_language = _SIMPLEMMA_LANGUAGE.get(language_code)
|
| 228 |
-
if lemmatizer_language is None and self.fallback == "reject":
|
| 229 |
-
return set()
|
| 230 |
-
lemmatizer = self._get_lemmatizer() if lemmatizer_language and language_code != "rus" else None
|
| 231 |
-
stopwords = self._get_stopwords(language_code)
|
| 232 |
-
terms: set[str] = set()
|
| 233 |
-
for match in _WORD_RE.finditer(unicodedata.normalize("NFKC", text)):
|
| 234 |
-
token = match.group(0).casefold()
|
| 235 |
-
if len(token) < 2 or token in stopwords:
|
| 236 |
-
continue
|
| 237 |
-
if language_code == "rus":
|
| 238 |
-
lemma = self._russian_lemma(token)
|
| 239 |
-
else:
|
| 240 |
-
lemma = lemmatizer.lemmatize(token, lemmatizer_language).casefold() if lemmatizer else token
|
| 241 |
-
if len(lemma) >= 2 and lemma not in stopwords:
|
| 242 |
-
terms.add(lemma)
|
| 243 |
-
return terms
|
| 244 |
-
|
| 245 |
-
def passes(self, anchor: str, positive: str, language_id: str) -> bool:
|
| 246 |
-
if not self.enabled:
|
| 247 |
-
return True
|
| 248 |
-
anchor_terms = self._terms(anchor, language_id)
|
| 249 |
-
positive_terms = self._terms(positive, language_id)
|
| 250 |
-
denominator = len(anchor_terms) + len(positive_terms)
|
| 251 |
-
overlap = 0.0 if denominator == 0 else 2.0 * len(anchor_terms & positive_terms) / denominator
|
| 252 |
-
return overlap > self.minimum if self.strict else overlap >= self.minimum
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
def _language_sampling_weights(
|
| 256 |
-
files: Sequence[Path],
|
| 257 |
-
root: Path,
|
| 258 |
-
settings: dict[str, Any],
|
| 259 |
-
) -> tuple[list[Path], dict[Path, float]]:
|
| 260 |
-
"""Select languages and assign shard weights so P(lang) is proportional to size**temperature."""
|
| 261 |
-
|
| 262 |
-
component = int(settings.get("path_component", 0))
|
| 263 |
-
temperature = float(settings.get("temperature", 1.0))
|
| 264 |
-
max_languages = int(settings.get("max_languages", 0))
|
| 265 |
-
included_codes = {_language_code(str(value)) for value in settings.get("include_language_codes", [])}
|
| 266 |
-
# Codes ignore the script suffix, so "tel" admits both tel_Telu and tel_Latn.
|
| 267 |
-
# include_languages matches the directory name exactly, which is what you want
|
| 268 |
-
# whenever the model has a fixed language list.
|
| 269 |
-
included_names = {str(value) for value in settings.get("include_languages", [])}
|
| 270 |
-
excluded_codes = {_language_code(str(value)) for value in settings.get("exclude_language_codes", [])}
|
| 271 |
-
if component < 0:
|
| 272 |
-
raise ValueError("language_sampling.path_component must be non-negative")
|
| 273 |
-
if not 0.0 <= temperature <= 1.0:
|
| 274 |
-
raise ValueError("language_sampling.temperature must be between 0 and 1")
|
| 275 |
-
if included_codes & excluded_codes:
|
| 276 |
-
raise ValueError("language_sampling include/exclude language codes overlap")
|
| 277 |
-
|
| 278 |
-
language_files: dict[str, list[Path]] = {}
|
| 279 |
-
for path in files:
|
| 280 |
-
relative_parts = path.relative_to(root).parts
|
| 281 |
-
if component >= len(relative_parts):
|
| 282 |
-
raise ValueError(f"Cannot infer a language from {path} using path component {component}")
|
| 283 |
-
language = relative_parts[component]
|
| 284 |
-
code = _language_code(language)
|
| 285 |
-
if included_names and language not in included_names:
|
| 286 |
-
continue
|
| 287 |
-
if included_codes and code not in included_codes:
|
| 288 |
-
continue
|
| 289 |
-
if code in excluded_codes:
|
| 290 |
-
continue
|
| 291 |
-
language_files.setdefault(language, []).append(path)
|
| 292 |
-
|
| 293 |
-
language_sizes = {
|
| 294 |
-
language: sum(path.stat().st_size for path in paths) for language, paths in language_files.items()
|
| 295 |
-
}
|
| 296 |
-
ranked_languages = sorted(language_sizes, key=lambda language: (-language_sizes[language], language))
|
| 297 |
-
if max_languages > 0:
|
| 298 |
-
ranked_languages = ranked_languages[:max_languages]
|
| 299 |
-
selected_languages = set(ranked_languages)
|
| 300 |
-
selected_files = [path for path in files if path.relative_to(root).parts[component] in selected_languages]
|
| 301 |
-
|
| 302 |
-
raw_weights: dict[Path, float] = {}
|
| 303 |
-
for path in selected_files:
|
| 304 |
-
language = path.relative_to(root).parts[component]
|
| 305 |
-
language_size = float(language_sizes[language])
|
| 306 |
-
# File-size weighting within a language makes the total language mass
|
| 307 |
-
# exactly size(language)**temperature.
|
| 308 |
-
raw_weights[path] = float(path.stat().st_size) * language_size ** (temperature - 1.0)
|
| 309 |
-
if not raw_weights:
|
| 310 |
-
return [], {}
|
| 311 |
-
maximum = max(raw_weights.values())
|
| 312 |
-
return selected_files, {path: weight / maximum for path, weight in raw_weights.items()}
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
def _resolve_language(
|
| 316 |
-
spec: dict[str, Any],
|
| 317 |
-
path: Path,
|
| 318 |
-
root: Path,
|
| 319 |
-
component: int | None,
|
| 320 |
-
) -> str:
|
| 321 |
-
"""Name the language of a shard's positive side.
|
| 322 |
-
|
| 323 |
-
A parallel corpus names its directories after a language *pair* (``en-de``),
|
| 324 |
-
so `language_map` translates that into the same identifier the rest of the
|
| 325 |
-
pipeline and the language markers use.
|
| 326 |
-
"""
|
| 327 |
-
if component is None:
|
| 328 |
-
return str(spec.get("language_id", ""))
|
| 329 |
-
raw = path.relative_to(root).parts[component]
|
| 330 |
-
mapping = dict(spec.get("language_map") or {})
|
| 331 |
-
return str(mapping.get(raw, raw))
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
def discover_shards(
|
| 335 |
-
stream_specs: Sequence[dict[str, Any]],
|
| 336 |
-
roots: dict[str, Path],
|
| 337 |
-
*,
|
| 338 |
-
seed: int,
|
| 339 |
-
) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
| 340 |
-
"""Resolve configured Parquet globs and validate their pair columns."""
|
| 341 |
-
|
| 342 |
-
shards: list[dict[str, Any]] = []
|
| 343 |
-
counts: dict[str, int] = {}
|
| 344 |
-
for spec in stream_specs:
|
| 345 |
-
name = str(spec["name"])
|
| 346 |
-
root_name = str(spec["root"])
|
| 347 |
-
if root_name not in roots:
|
| 348 |
-
raise ValueError(f"No dataset root was provided for {root_name!r}")
|
| 349 |
-
root = roots[root_name].expanduser().resolve()
|
| 350 |
-
if not root.is_dir():
|
| 351 |
-
raise FileNotFoundError(f"Dataset root does not exist: {root}")
|
| 352 |
-
|
| 353 |
-
pattern = str(spec.get("parquet", "**/*.parquet"))
|
| 354 |
-
files = sorted(path.resolve() for path in root.glob(pattern) if path.is_file())
|
| 355 |
-
if not files:
|
| 356 |
-
raise FileNotFoundError(f"No Parquet files matched {pattern!r} under {root}")
|
| 357 |
-
|
| 358 |
-
sampling_weights: dict[Path, float] = {}
|
| 359 |
-
language_sampling = spec.get("language_sampling")
|
| 360 |
-
if language_sampling:
|
| 361 |
-
files, sampling_weights = _language_sampling_weights(files, root, dict(language_sampling))
|
| 362 |
-
if not files:
|
| 363 |
-
raise ValueError(f"Language sampling removed every file from stream {name!r}")
|
| 364 |
-
|
| 365 |
-
anchor_column = str(spec["anchor_column"])
|
| 366 |
-
positive_column = str(spec["positive_column"])
|
| 367 |
-
columns = set(pq.ParquetFile(files[0]).schema_arrow.names)
|
| 368 |
-
missing = {anchor_column, positive_column} - columns
|
| 369 |
-
if missing:
|
| 370 |
-
raise ValueError(f"Stream {name!r} is missing columns {sorted(missing)} in {files[0].name}")
|
| 371 |
-
|
| 372 |
-
counts[name] = len(files)
|
| 373 |
-
language_component = int(dict(language_sampling).get("path_component", 0)) if language_sampling else None
|
| 374 |
-
shards.extend(
|
| 375 |
-
{
|
| 376 |
-
"source": name,
|
| 377 |
-
"path": str(path),
|
| 378 |
-
"anchor_column": anchor_column,
|
| 379 |
-
"positive_column": positive_column,
|
| 380 |
-
"sampling_weight": sampling_weights.get(path, 1.0),
|
| 381 |
-
"language_id": _resolve_language(spec, path, root, language_component),
|
| 382 |
-
"anchor_language_id": str(spec.get("anchor_language", "")) or
|
| 383 |
-
_resolve_language(spec, path, root, language_component),
|
| 384 |
-
"usable_rows": int(pq.ParquetFile(path).metadata.num_rows),
|
| 385 |
-
}
|
| 386 |
-
for path in files
|
| 387 |
-
)
|
| 388 |
-
|
| 389 |
-
random.Random(seed).shuffle(shards)
|
| 390 |
-
return shards, counts
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
class _SourceRows:
|
| 394 |
-
def __init__(
|
| 395 |
-
self,
|
| 396 |
-
shards: Sequence[PairShard],
|
| 397 |
-
*,
|
| 398 |
-
rng: random.Random,
|
| 399 |
-
fragment_rows: int,
|
| 400 |
-
max_chars: int,
|
| 401 |
-
lemma_filter: dict[str, Any] | None = None,
|
| 402 |
-
holdout_row_groups: int = 0,
|
| 403 |
-
) -> None:
|
| 404 |
-
if not shards:
|
| 405 |
-
raise ValueError("A streaming source has no Parquet shards")
|
| 406 |
-
self.shards = list(shards)
|
| 407 |
-
self.rng = rng
|
| 408 |
-
self.fragment_rows = int(fragment_rows)
|
| 409 |
-
self.max_chars = int(max_chars)
|
| 410 |
-
# Trailing row groups reserved for evaluation. Training never reads them,
|
| 411 |
-
# so a held-out score cannot be inflated by memorisation — which matters
|
| 412 |
-
# here because arms differ in how often they revisit a given language.
|
| 413 |
-
self.holdout_row_groups = max(0, int(holdout_row_groups))
|
| 414 |
-
self.lemma_filter = _LemmaOverlapFilter(lemma_filter)
|
| 415 |
-
self.shard_weights = [float(shard.sampling_weight) for shard in self.shards]
|
| 416 |
-
self.weighted_shards = any(weight != self.shard_weights[0] for weight in self.shard_weights[1:])
|
| 417 |
-
self.deck: list[PairShard] = []
|
| 418 |
-
self.fragment: Iterator[tuple[str, str]] = iter(())
|
| 419 |
-
|
| 420 |
-
def _next_shard(self) -> PairShard:
|
| 421 |
-
if self.weighted_shards:
|
| 422 |
-
return self.rng.choices(self.shards, weights=self.shard_weights, k=1)[0]
|
| 423 |
-
if not self.deck:
|
| 424 |
-
self.deck = self.shards.copy()
|
| 425 |
-
self.rng.shuffle(self.deck)
|
| 426 |
-
return self.deck.pop()
|
| 427 |
-
|
| 428 |
-
def _read_fragment(self) -> Iterator[tuple[str, str]]:
|
| 429 |
-
shard = self._next_shard()
|
| 430 |
-
parquet = pq.ParquetFile(shard.path)
|
| 431 |
-
if parquet.metadata.num_row_groups == 0:
|
| 432 |
-
return iter(())
|
| 433 |
-
|
| 434 |
-
trainable_groups = parquet.metadata.num_row_groups - self.holdout_row_groups
|
| 435 |
-
if trainable_groups < 1:
|
| 436 |
-
# Never starve a shard that is too small to hold anything back; such
|
| 437 |
-
# a language simply has no held-out split.
|
| 438 |
-
trainable_groups = parquet.metadata.num_row_groups
|
| 439 |
-
row_group = self.rng.randrange(trainable_groups)
|
| 440 |
-
table = parquet.read_row_group(
|
| 441 |
-
row_group,
|
| 442 |
-
columns=[shard.anchor_column, shard.positive_column],
|
| 443 |
-
use_threads=False,
|
| 444 |
-
)
|
| 445 |
-
anchors, positives = _sample_pair_columns(
|
| 446 |
-
table, self.rng, fragment_rows=self.fragment_rows
|
| 447 |
-
)
|
| 448 |
-
|
| 449 |
-
def rows() -> Iterator[tuple[str, str]]:
|
| 450 |
-
for anchor, positive in zip(anchors, positives, strict=True):
|
| 451 |
-
if not isinstance(anchor, str) or not isinstance(positive, str):
|
| 452 |
-
continue
|
| 453 |
-
anchor = anchor.strip()
|
| 454 |
-
positive = positive.strip()
|
| 455 |
-
if not anchor or not positive:
|
| 456 |
-
continue
|
| 457 |
-
if self.max_chars > 0 and (len(anchor) > self.max_chars or len(positive) > self.max_chars):
|
| 458 |
-
continue
|
| 459 |
-
if _canonical_text(anchor) == _canonical_text(positive):
|
| 460 |
-
continue
|
| 461 |
-
if not self.lemma_filter.passes(anchor, positive, shard.language_id):
|
| 462 |
-
continue
|
| 463 |
-
yield anchor, positive
|
| 464 |
-
|
| 465 |
-
return rows()
|
| 466 |
-
|
| 467 |
-
def next(self) -> tuple[str, str]:
|
| 468 |
-
while True:
|
| 469 |
-
try:
|
| 470 |
-
return next(self.fragment)
|
| 471 |
-
except StopIteration:
|
| 472 |
-
self.fragment = self._read_fragment()
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
def _stable_group_batch_seed(
|
| 476 |
-
seed: int, group: tuple[str, str], occurrence: int
|
| 477 |
-
) -> int:
|
| 478 |
-
payload = f"{seed}\0{group[0]}\0{group[1]}\0{occurrence}".encode("utf-8")
|
| 479 |
-
return int.from_bytes(hashlib.blake2b(payload, digest_size=8).digest(), "little")
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
def _sample_pair_columns(
|
| 483 |
-
table: Any, rng: random.Random, *, fragment_rows: int
|
| 484 |
-
) -> tuple[list[Any], list[Any]]:
|
| 485 |
-
"""Convert only the sampled rows instead of every string in a row group."""
|
| 486 |
-
|
| 487 |
-
rows = int(table.num_rows)
|
| 488 |
-
if rows <= 0:
|
| 489 |
-
return [], []
|
| 490 |
-
limit = rows if fragment_rows <= 0 else min(rows, int(fragment_rows))
|
| 491 |
-
generator = np.random.default_rng(rng.getrandbits(64))
|
| 492 |
-
if limit < rows:
|
| 493 |
-
indices = generator.choice(rows, size=limit, replace=False, shuffle=True)
|
| 494 |
-
else:
|
| 495 |
-
indices = np.arange(rows, dtype=np.int64)
|
| 496 |
-
generator.shuffle(indices)
|
| 497 |
-
sampled = table.take(indices)
|
| 498 |
-
return sampled.column(0).to_pylist(), sampled.column(1).to_pylist()
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
class _DeterministicGroupBatches:
|
| 502 |
-
"""Produce a batch from its group-local index, independent of cursor state."""
|
| 503 |
-
|
| 504 |
-
def __init__(
|
| 505 |
-
self,
|
| 506 |
-
shards: Sequence[PairShard],
|
| 507 |
-
*,
|
| 508 |
-
group: tuple[str, str],
|
| 509 |
-
seed: int,
|
| 510 |
-
fragment_rows: int,
|
| 511 |
-
max_chars: int,
|
| 512 |
-
lemma_filter: dict[str, Any] | None,
|
| 513 |
-
holdout_row_groups: int,
|
| 514 |
-
) -> None:
|
| 515 |
-
self.group = group
|
| 516 |
-
self.seed = int(seed)
|
| 517 |
-
self.fragment_rows = int(fragment_rows)
|
| 518 |
-
self.max_chars = int(max_chars)
|
| 519 |
-
self.lemma_filter = _LemmaOverlapFilter(lemma_filter)
|
| 520 |
-
self.fragments: list[tuple[PairShard, int]] = []
|
| 521 |
-
self.fragment_weights: list[float] = []
|
| 522 |
-
for shard in shards:
|
| 523 |
-
parquet = pq.ParquetFile(shard.path)
|
| 524 |
-
trainable_groups = parquet.metadata.num_row_groups - holdout_row_groups
|
| 525 |
-
if trainable_groups < 1:
|
| 526 |
-
trainable_groups = parquet.metadata.num_row_groups
|
| 527 |
-
row_counts = [
|
| 528 |
-
int(parquet.metadata.row_group(index).num_rows)
|
| 529 |
-
for index in range(trainable_groups)
|
| 530 |
-
]
|
| 531 |
-
total = sum(row_counts)
|
| 532 |
-
if total <= 0:
|
| 533 |
-
continue
|
| 534 |
-
for index, count in enumerate(row_counts):
|
| 535 |
-
self.fragments.append((shard, index))
|
| 536 |
-
self.fragment_weights.append(
|
| 537 |
-
float(shard.sampling_weight) * count / total
|
| 538 |
-
)
|
| 539 |
-
if not self.fragments or not any(weight > 0 for weight in self.fragment_weights):
|
| 540 |
-
raise ValueError(f"deterministic group {group!r} has no trainable rows")
|
| 541 |
-
|
| 542 |
-
def _read_fragment(self, rng: random.Random) -> Iterator[tuple[str, str]]:
|
| 543 |
-
shard, row_group = rng.choices(
|
| 544 |
-
self.fragments, weights=self.fragment_weights, k=1
|
| 545 |
-
)[0]
|
| 546 |
-
table = pq.ParquetFile(shard.path).read_row_group(
|
| 547 |
-
row_group,
|
| 548 |
-
columns=[shard.anchor_column, shard.positive_column],
|
| 549 |
-
use_threads=False,
|
| 550 |
-
)
|
| 551 |
-
anchors, positives = _sample_pair_columns(
|
| 552 |
-
table, rng, fragment_rows=self.fragment_rows
|
| 553 |
-
)
|
| 554 |
-
for anchor, positive in zip(anchors, positives, strict=True):
|
| 555 |
-
if not isinstance(anchor, str) or not isinstance(positive, str):
|
| 556 |
-
continue
|
| 557 |
-
anchor = anchor.strip()
|
| 558 |
-
positive = positive.strip()
|
| 559 |
-
if not anchor or not positive:
|
| 560 |
-
continue
|
| 561 |
-
if self.max_chars > 0 and (
|
| 562 |
-
len(anchor) > self.max_chars or len(positive) > self.max_chars
|
| 563 |
-
):
|
| 564 |
-
continue
|
| 565 |
-
if _canonical_text(anchor) == _canonical_text(positive):
|
| 566 |
-
continue
|
| 567 |
-
if not self.lemma_filter.passes(anchor, positive, shard.language_id):
|
| 568 |
-
continue
|
| 569 |
-
yield anchor, positive
|
| 570 |
-
|
| 571 |
-
def batch(
|
| 572 |
-
self, occurrence: int, *, batch_size: int, strict_unique: bool
|
| 573 |
-
) -> list[tuple[str, str]]:
|
| 574 |
-
rng = random.Random(
|
| 575 |
-
_stable_group_batch_seed(self.seed, self.group, occurrence)
|
| 576 |
-
)
|
| 577 |
-
seen: set[str] = set()
|
| 578 |
-
pairs: list[tuple[str, str]] = []
|
| 579 |
-
attempts = 0
|
| 580 |
-
while len(pairs) < batch_size and attempts < batch_size * 20:
|
| 581 |
-
for anchor, positive in self._read_fragment(rng):
|
| 582 |
-
attempts += 1
|
| 583 |
-
keys = (_canonical_text(anchor), _canonical_text(positive))
|
| 584 |
-
if any(key in seen for key in keys):
|
| 585 |
-
if attempts >= batch_size * 20:
|
| 586 |
-
break
|
| 587 |
-
continue
|
| 588 |
-
seen.update(keys)
|
| 589 |
-
pairs.append((anchor, positive))
|
| 590 |
-
if len(pairs) >= batch_size or attempts >= batch_size * 20:
|
| 591 |
-
break
|
| 592 |
-
if len(pairs) < batch_size and strict_unique:
|
| 593 |
-
raise RuntimeError(
|
| 594 |
-
f"could not construct a unique batch of {batch_size} pairs for "
|
| 595 |
-
f"source={self.group[0]!r}, language={self.group[1]!r}"
|
| 596 |
-
)
|
| 597 |
-
while len(pairs) < batch_size:
|
| 598 |
-
fragment = list(self._read_fragment(rng))
|
| 599 |
-
if fragment:
|
| 600 |
-
pairs.extend(fragment[: batch_size - len(pairs)])
|
| 601 |
-
return pairs
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
def _weighted_source_picker(
|
| 605 |
-
sources: Sequence[str],
|
| 606 |
-
weights: dict[str, float],
|
| 607 |
-
rng: random.Random,
|
| 608 |
-
) -> Iterator[str]:
|
| 609 |
-
values = [float(weights.get(source, 1.0)) for source in sources]
|
| 610 |
-
if any(value < 0 for value in values) or not any(value > 0 for value in values):
|
| 611 |
-
raise ValueError("Stream weights must be non-negative and at least one must be positive")
|
| 612 |
-
cumulative = np.cumsum(values).tolist()
|
| 613 |
-
total = cumulative[-1]
|
| 614 |
-
while True:
|
| 615 |
-
yield sources[bisect.bisect_left(cumulative, rng.random() * total)]
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
def _batch_no_duplicates(
|
| 619 |
-
pairs: Iterator[dict[str, str]],
|
| 620 |
-
*,
|
| 621 |
-
batch_size: int,
|
| 622 |
-
) -> Iterator[dict[str, str]]:
|
| 623 |
-
"""Reject repeated anchor/positive texts inside each emitted batch."""
|
| 624 |
-
|
| 625 |
-
seen: set[str] = set()
|
| 626 |
-
emitted_in_batch = 0
|
| 627 |
-
for pair in pairs:
|
| 628 |
-
keys = (_canonical_text(pair["anchor"]), _canonical_text(pair["positive"]))
|
| 629 |
-
if any(key in seen for key in keys):
|
| 630 |
-
continue
|
| 631 |
-
seen.update(keys)
|
| 632 |
-
yield pair
|
| 633 |
-
emitted_in_batch += 1
|
| 634 |
-
if emitted_in_batch >= batch_size:
|
| 635 |
-
seen.clear()
|
| 636 |
-
emitted_in_batch = 0
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
def _monolingual_batches(
|
| 640 |
-
by_source: dict[str, list[PairShard]],
|
| 641 |
-
*,
|
| 642 |
-
settings: dict[str, Any],
|
| 643 |
-
rng: random.Random,
|
| 644 |
-
batch_size: int,
|
| 645 |
-
holdout_row_groups: int,
|
| 646 |
-
) -> Iterator[dict[str, str]]:
|
| 647 |
-
"""Emit runs of ``batch_size`` pairs drawn from a single language.
|
| 648 |
-
|
| 649 |
-
In-batch negatives are the whole contrastive signal here. A language-mixed
|
| 650 |
-
batch of 16,384 over 60 languages leaves only a few hundred same-language
|
| 651 |
-
negatives; the rest are separable on script alone and teach nothing. Keeping
|
| 652 |
-
a batch monolingual makes every negative a real one at no extra cost.
|
| 653 |
-
|
| 654 |
-
Languages are picked with probability proportional to the shard sampling
|
| 655 |
-
weights, which already encode ``size**temperature``, so this changes only
|
| 656 |
-
how pairs are grouped, never how often a language is seen.
|
| 657 |
-
"""
|
| 658 |
-
|
| 659 |
-
# A group must be homogeneous in both source and target language. FineWeb2
|
| 660 |
-
# and CCMatrix can share the same positive-side language but not the same
|
| 661 |
-
# anchor language: a Russian FineWeb pair is ru->ru while a CCMatrix pair is
|
| 662 |
-
# en->ru. Grouping by language alone used the first shuffled shard's marker
|
| 663 |
-
# for both sources, silently mistagging one side of every other source.
|
| 664 |
-
by_group: dict[tuple[str, str], list[PairShard]] = {}
|
| 665 |
-
for shards in by_source.values():
|
| 666 |
-
for shard in shards:
|
| 667 |
-
by_group.setdefault((shard.source, shard.language_id), []).append(shard)
|
| 668 |
-
if not by_group:
|
| 669 |
-
raise ValueError("Monolingual batching found no language-tagged shards")
|
| 670 |
-
|
| 671 |
-
groups = sorted(by_group)
|
| 672 |
-
source_weights = dict(settings.get("source_weights") or {})
|
| 673 |
-
if any(float(value) < 0 for value in source_weights.values()):
|
| 674 |
-
raise ValueError("Stream weights must be non-negative")
|
| 675 |
-
group_mass = {
|
| 676 |
-
group: sum(float(shard.sampling_weight) for shard in shards)
|
| 677 |
-
for group, shards in by_group.items()
|
| 678 |
-
}
|
| 679 |
-
source_mass: dict[str, float] = {}
|
| 680 |
-
for (source_name, _), mass in group_mass.items():
|
| 681 |
-
source_mass[source_name] = source_mass.get(source_name, 0.0) + mass
|
| 682 |
-
# First normalise languages inside a source, then assign the configured
|
| 683 |
-
# source mass. This makes a 0.5/0.5 stream mixture a realised 0.5/0.5 batch
|
| 684 |
-
# mixture regardless of how many shards either source owns.
|
| 685 |
-
weights = [
|
| 686 |
-
group_mass[group] / source_mass[group[0]]
|
| 687 |
-
* float(source_weights.get(group[0], 1.0))
|
| 688 |
-
for group in groups
|
| 689 |
-
]
|
| 690 |
-
if not any(weight > 0 for weight in weights):
|
| 691 |
-
raise ValueError("Monolingual batching needs at least one positive language weight")
|
| 692 |
-
|
| 693 |
-
smooth = bool(settings.get("smooth_weighted_round_robin", False))
|
| 694 |
-
source_scheduler: SmoothWeightedRoundRobin | None = None
|
| 695 |
-
language_schedulers: dict[str, SmoothWeightedRoundRobin] = {}
|
| 696 |
-
group_occurrences = {group: 0 for group in groups}
|
| 697 |
-
if smooth:
|
| 698 |
-
source_temperature = float(settings.get("source_temperature", 1.0))
|
| 699 |
-
language_temperature = float(settings.get("language_temperature", 1.0))
|
| 700 |
-
if not 0.0 <= source_temperature <= 1.0:
|
| 701 |
-
raise ValueError("source_temperature must be between zero and one")
|
| 702 |
-
if not 0.0 <= language_temperature <= 1.0:
|
| 703 |
-
raise ValueError("language_temperature must be between zero and one")
|
| 704 |
-
|
| 705 |
-
def usable_rows(shard: PairShard) -> int:
|
| 706 |
-
if holdout_row_groups <= 0 and shard.usable_rows > 0:
|
| 707 |
-
return int(shard.usable_rows)
|
| 708 |
-
parquet = pq.ParquetFile(shard.path)
|
| 709 |
-
trainable_groups = parquet.metadata.num_row_groups - holdout_row_groups
|
| 710 |
-
if trainable_groups < 1:
|
| 711 |
-
trainable_groups = parquet.metadata.num_row_groups
|
| 712 |
-
return sum(
|
| 713 |
-
int(parquet.metadata.row_group(index).num_rows)
|
| 714 |
-
for index in range(trainable_groups)
|
| 715 |
-
)
|
| 716 |
-
|
| 717 |
-
group_rows = {
|
| 718 |
-
group: sum(usable_rows(shard) for shard in shards)
|
| 719 |
-
for group, shards in by_group.items()
|
| 720 |
-
}
|
| 721 |
-
if any(count <= 0 for count in group_rows.values()):
|
| 722 |
-
raise ValueError("smooth scheduling requires positive usable rows per group")
|
| 723 |
-
source_rows: dict[str, int] = {}
|
| 724 |
-
for (source_name, _), count in group_rows.items():
|
| 725 |
-
source_rows[source_name] = source_rows.get(source_name, 0) + count
|
| 726 |
-
sources = sorted(source_rows)
|
| 727 |
-
source_schedule_weights = [
|
| 728 |
-
float(source_weights.get(source_name, 1.0))
|
| 729 |
-
* float(source_rows[source_name]) ** source_temperature
|
| 730 |
-
for source_name in sources
|
| 731 |
-
]
|
| 732 |
-
source_scheduler = SmoothWeightedRoundRobin(
|
| 733 |
-
sources,
|
| 734 |
-
source_schedule_weights,
|
| 735 |
-
seed=rng.getrandbits(64),
|
| 736 |
-
)
|
| 737 |
-
for source_name in sources:
|
| 738 |
-
languages = sorted(
|
| 739 |
-
language for source, language in groups if source == source_name
|
| 740 |
-
)
|
| 741 |
-
language_schedulers[source_name] = SmoothWeightedRoundRobin(
|
| 742 |
-
languages,
|
| 743 |
-
[
|
| 744 |
-
float(group_rows[source_name, language]) ** language_temperature
|
| 745 |
-
for language in languages
|
| 746 |
-
],
|
| 747 |
-
seed=rng.getrandbits(64),
|
| 748 |
-
)
|
| 749 |
-
|
| 750 |
-
for _ in range(int(settings.get("scheduler_start_batch", 0))):
|
| 751 |
-
source_name = source_scheduler.next_item()
|
| 752 |
-
group = (source_name, language_schedulers[source_name].next_item())
|
| 753 |
-
group_occurrences[group] += 1
|
| 754 |
-
|
| 755 |
-
rows = {
|
| 756 |
-
group: (
|
| 757 |
-
_DeterministicGroupBatches(
|
| 758 |
-
by_group[group],
|
| 759 |
-
group=group,
|
| 760 |
-
seed=int(settings["seed"]),
|
| 761 |
-
fragment_rows=int(settings["fragment_rows"]),
|
| 762 |
-
max_chars=int(settings["max_chars"]),
|
| 763 |
-
lemma_filter=dict(settings.get("lemma_filter") or {}),
|
| 764 |
-
holdout_row_groups=holdout_row_groups,
|
| 765 |
-
)
|
| 766 |
-
if smooth
|
| 767 |
-
else _SourceRows(
|
| 768 |
-
by_group[group],
|
| 769 |
-
rng=random.Random(rng.getrandbits(64)),
|
| 770 |
-
fragment_rows=int(settings["fragment_rows"]),
|
| 771 |
-
max_chars=int(settings["max_chars"]),
|
| 772 |
-
lemma_filter=dict(settings.get("lemma_filter") or {}),
|
| 773 |
-
holdout_row_groups=holdout_row_groups,
|
| 774 |
-
)
|
| 775 |
-
)
|
| 776 |
-
for group in groups
|
| 777 |
-
}
|
| 778 |
-
|
| 779 |
-
# Tests need to see which language produced a pair; training must not, because
|
| 780 |
-
# the dataset features are fixed to anchor/positive.
|
| 781 |
-
debug = bool(settings.get("debug_language", False))
|
| 782 |
-
|
| 783 |
-
def emit(
|
| 784 |
-
anchor: str, positive: str, source_name: str, language: str
|
| 785 |
-
) -> dict[str, str]:
|
| 786 |
-
# The language is carried inside the text as a marker token, the way NLLB
|
| 787 |
-
# is told its source language. That keeps the dataset schema at two string
|
| 788 |
-
# columns, so nothing in the collator or trainer has to know about it, and
|
| 789 |
-
# each side of a pair can carry a different language.
|
| 790 |
-
pair = {"anchor": anchor_tag + anchor, "positive": positive_tag + positive}
|
| 791 |
-
if debug:
|
| 792 |
-
pair["source"] = source_name
|
| 793 |
-
pair["language"] = language
|
| 794 |
-
return pair
|
| 795 |
-
|
| 796 |
-
marker = bool(settings.get("language_markers", False))
|
| 797 |
-
strict_unique = bool(settings.get("strict_unique_batches", False))
|
| 798 |
-
|
| 799 |
-
while True:
|
| 800 |
-
if source_scheduler is None:
|
| 801 |
-
group = rng.choices(groups, weights=weights, k=1)[0]
|
| 802 |
-
else:
|
| 803 |
-
source_name = source_scheduler.next_item()
|
| 804 |
-
group = (source_name, language_schedulers[source_name].next_item())
|
| 805 |
-
source = rows[group]
|
| 806 |
-
first = by_group[group][0]
|
| 807 |
-
language = first.language_id
|
| 808 |
-
anchor_tag = f"__{first.anchor_language_id or language}__ " if marker else ""
|
| 809 |
-
positive_tag = f"__{language}__ " if marker else ""
|
| 810 |
-
if isinstance(source, _DeterministicGroupBatches):
|
| 811 |
-
occurrence = group_occurrences[group]
|
| 812 |
-
group_occurrences[group] += 1
|
| 813 |
-
for anchor, positive in source.batch(
|
| 814 |
-
occurrence,
|
| 815 |
-
batch_size=batch_size,
|
| 816 |
-
strict_unique=strict_unique,
|
| 817 |
-
):
|
| 818 |
-
yield emit(anchor, positive, group[0], language)
|
| 819 |
-
continue
|
| 820 |
-
seen: set[str] = set()
|
| 821 |
-
emitted = 0
|
| 822 |
-
attempts = 0
|
| 823 |
-
while emitted < batch_size and attempts < batch_size * 20:
|
| 824 |
-
attempts += 1
|
| 825 |
-
anchor, positive = source.next()
|
| 826 |
-
keys = (_canonical_text(anchor), _canonical_text(positive))
|
| 827 |
-
if any(key in seen for key in keys):
|
| 828 |
-
continue
|
| 829 |
-
seen.update(keys)
|
| 830 |
-
emitted += 1
|
| 831 |
-
yield emit(anchor, positive, group[0], language)
|
| 832 |
-
if emitted < batch_size and strict_unique:
|
| 833 |
-
raise RuntimeError(
|
| 834 |
-
f"could not construct a unique batch of {batch_size} pairs for "
|
| 835 |
-
f"source={group[0]!r}, language={language!r}"
|
| 836 |
-
)
|
| 837 |
-
while emitted < batch_size:
|
| 838 |
-
# Never emit a short batch: a ragged tail would silently change the
|
| 839 |
-
# number of in-batch negatives and confound the comparison.
|
| 840 |
-
anchor, positive = source.next()
|
| 841 |
-
emitted += 1
|
| 842 |
-
yield emit(anchor, positive, group[0], language)
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
def _random_pairs(shards: Sequence[dict[str, Any]], settings: dict[str, Any]) -> Iterator[dict[str, str]]:
|
| 846 |
-
worker_id = 0
|
| 847 |
-
try:
|
| 848 |
-
from torch.utils.data import get_worker_info
|
| 849 |
-
|
| 850 |
-
worker = get_worker_info()
|
| 851 |
-
worker_id = worker.id if worker is not None else 0
|
| 852 |
-
except ImportError:
|
| 853 |
-
pass
|
| 854 |
-
rank = int(os.environ.get("RANK", os.environ.get("LOCAL_RANK", "0")))
|
| 855 |
-
seed = int(settings["seed"]) + rank * 1_000_003 + worker_id * 10_007
|
| 856 |
-
rng = random.Random(seed)
|
| 857 |
-
|
| 858 |
-
by_source: dict[str, list[PairShard]] = {}
|
| 859 |
-
for raw in shards:
|
| 860 |
-
shard = PairShard(**raw)
|
| 861 |
-
by_source.setdefault(shard.source, []).append(shard)
|
| 862 |
-
if not by_source:
|
| 863 |
-
raise ValueError("This worker received no Parquet shards")
|
| 864 |
-
|
| 865 |
-
holdout_row_groups = int(settings.get("holdout_row_groups", 0))
|
| 866 |
-
source_rows = {
|
| 867 |
-
source: _SourceRows(
|
| 868 |
-
source_shards,
|
| 869 |
-
rng=random.Random(rng.getrandbits(64)),
|
| 870 |
-
fragment_rows=int(settings["fragment_rows"]),
|
| 871 |
-
max_chars=int(settings["max_chars"]),
|
| 872 |
-
lemma_filter=dict(settings.get("lemma_filter") or {}),
|
| 873 |
-
holdout_row_groups=holdout_row_groups,
|
| 874 |
-
)
|
| 875 |
-
for source, source_shards in by_source.items()
|
| 876 |
-
}
|
| 877 |
-
sources = sorted(source_rows)
|
| 878 |
-
source_picker = _weighted_source_picker(sources, dict(settings["source_weights"]), rng)
|
| 879 |
-
|
| 880 |
-
def mixed_pairs() -> Iterator[dict[str, str]]:
|
| 881 |
-
while True:
|
| 882 |
-
source = next(source_picker)
|
| 883 |
-
anchor, positive = source_rows[source].next()
|
| 884 |
-
yield {"anchor": anchor, "positive": positive}
|
| 885 |
-
|
| 886 |
-
batch_size = int(settings.get("batch_size", 0))
|
| 887 |
-
if bool(settings.get("monolingual_batches", False)):
|
| 888 |
-
if batch_size <= 1:
|
| 889 |
-
raise ValueError("monolingual_batches requires streaming.batch_size")
|
| 890 |
-
yield from _monolingual_batches(
|
| 891 |
-
by_source,
|
| 892 |
-
settings=settings,
|
| 893 |
-
rng=rng,
|
| 894 |
-
batch_size=batch_size,
|
| 895 |
-
holdout_row_groups=holdout_row_groups,
|
| 896 |
-
)
|
| 897 |
-
return
|
| 898 |
-
|
| 899 |
-
stream: Iterator[dict[str, str]] = mixed_pairs()
|
| 900 |
-
shuffle_buffer_size = int(settings["shuffle_buffer_size"])
|
| 901 |
-
if shuffle_buffer_size > 1:
|
| 902 |
-
unshuffled = stream
|
| 903 |
-
|
| 904 |
-
def shuffled_pairs() -> Iterator[dict[str, str]]:
|
| 905 |
-
buffer = [next(unshuffled) for _ in range(shuffle_buffer_size)]
|
| 906 |
-
while True:
|
| 907 |
-
index = rng.randrange(shuffle_buffer_size)
|
| 908 |
-
result = buffer[index]
|
| 909 |
-
buffer[index] = next(unshuffled)
|
| 910 |
-
yield result
|
| 911 |
-
|
| 912 |
-
stream = shuffled_pairs()
|
| 913 |
-
|
| 914 |
-
if not bool(settings.get("batch_no_duplicates", False)) or batch_size <= 1:
|
| 915 |
-
yield from stream
|
| 916 |
-
return
|
| 917 |
-
|
| 918 |
-
yield from _batch_no_duplicates(stream, batch_size=batch_size)
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
@dataclass(frozen=True)
|
| 922 |
-
class _ProducerFailure:
|
| 923 |
-
error: BaseException
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
def _queued_pairs(shards: Sequence[dict[str, Any]], settings: dict[str, Any]) -> Iterator[dict[str, str]]:
|
| 927 |
-
pair_queue: queue.Queue[dict[str, str] | _ProducerFailure] = queue.Queue(maxsize=int(settings["queue_size"]))
|
| 928 |
-
stopped = threading.Event()
|
| 929 |
-
|
| 930 |
-
def put(item: dict[str, str] | _ProducerFailure) -> None:
|
| 931 |
-
while not stopped.is_set():
|
| 932 |
-
try:
|
| 933 |
-
pair_queue.put(item, timeout=0.5)
|
| 934 |
-
return
|
| 935 |
-
except queue.Full:
|
| 936 |
-
continue
|
| 937 |
-
|
| 938 |
-
def produce() -> None:
|
| 939 |
-
try:
|
| 940 |
-
for pair in _random_pairs(shards, settings):
|
| 941 |
-
if stopped.is_set():
|
| 942 |
-
return
|
| 943 |
-
put(pair)
|
| 944 |
-
except BaseException as error:
|
| 945 |
-
put(_ProducerFailure(error))
|
| 946 |
-
|
| 947 |
-
producer = threading.Thread(target=produce, name="starse-parquet-prefetch", daemon=True)
|
| 948 |
-
producer.start()
|
| 949 |
-
try:
|
| 950 |
-
while True:
|
| 951 |
-
item = pair_queue.get()
|
| 952 |
-
if isinstance(item, _ProducerFailure):
|
| 953 |
-
raise RuntimeError("The Parquet prefetch worker failed") from item.error
|
| 954 |
-
yield item
|
| 955 |
-
finally:
|
| 956 |
-
stopped.set()
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
def _queued_pairs_from_replica(
|
| 960 |
-
shard_replicas: Sequence[Sequence[dict[str, Any]]],
|
| 961 |
-
settings: dict[str, Any],
|
| 962 |
-
) -> Iterator[dict[str, str]]:
|
| 963 |
-
"""Give every DataLoader worker the full weighted shard population."""
|
| 964 |
-
|
| 965 |
-
if len(shard_replicas) != 1:
|
| 966 |
-
raise ValueError("Each dataset shard must contain exactly one shard replica")
|
| 967 |
-
yield from _queued_pairs(shard_replicas[0], settings)
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
def build_streaming_dataset(
|
| 971 |
-
stream_specs: Sequence[dict[str, Any]],
|
| 972 |
-
roots: dict[str, Path],
|
| 973 |
-
streaming_settings: dict[str, Any],
|
| 974 |
-
) -> tuple[Any, dict[str, int]]:
|
| 975 |
-
"""Create an infinite Hugging Face IterableDataset with known pair features."""
|
| 976 |
-
|
| 977 |
-
from datasets import Features, IterableDataset, Value
|
| 978 |
-
|
| 979 |
-
seed = int(streaming_settings["seed"])
|
| 980 |
-
shards, counts = discover_shards(stream_specs, roots, seed=seed)
|
| 981 |
-
source_weights = {str(spec["name"]): float(spec.get("weight", 1.0)) for spec in stream_specs}
|
| 982 |
-
settings = {
|
| 983 |
-
"seed": seed,
|
| 984 |
-
"source_weights": source_weights,
|
| 985 |
-
"fragment_rows": int(streaming_settings.get("fragment_rows", 2048)),
|
| 986 |
-
"max_chars": int(streaming_settings.get("max_chars", 512)),
|
| 987 |
-
"shuffle_buffer_size": int(streaming_settings.get("shuffle_buffer_size", 65536)),
|
| 988 |
-
"queue_size": int(streaming_settings.get("queue_size", 8192)),
|
| 989 |
-
"lemma_filter": dict(streaming_settings.get("lemma_filter") or {}),
|
| 990 |
-
"batch_size": int(streaming_settings.get("batch_size", 0)),
|
| 991 |
-
"batch_no_duplicates": bool(streaming_settings.get("batch_no_duplicates", False)),
|
| 992 |
-
"monolingual_batches": bool(streaming_settings.get("monolingual_batches", False)),
|
| 993 |
-
"language_markers": bool(streaming_settings.get("language_markers", False)),
|
| 994 |
-
"holdout_row_groups": int(streaming_settings.get("holdout_row_groups", 0)),
|
| 995 |
-
"smooth_weighted_round_robin": bool(
|
| 996 |
-
streaming_settings.get("smooth_weighted_round_robin", False)
|
| 997 |
-
),
|
| 998 |
-
"source_temperature": float(streaming_settings.get("source_temperature", 1.0)),
|
| 999 |
-
"language_temperature": float(
|
| 1000 |
-
streaming_settings.get("language_temperature", 1.0)
|
| 1001 |
-
),
|
| 1002 |
-
"scheduler_start_batch": int(
|
| 1003 |
-
streaming_settings.get("scheduler_start_batch", 0)
|
| 1004 |
-
),
|
| 1005 |
-
"strict_unique_batches": bool(
|
| 1006 |
-
streaming_settings.get("strict_unique_batches", False)
|
| 1007 |
-
),
|
| 1008 |
-
}
|
| 1009 |
-
features = Features({"anchor": Value("string"), "positive": Value("string")})
|
| 1010 |
-
worker_replicas = int(streaming_settings.get("worker_replicas", 0))
|
| 1011 |
-
if worker_replicas > 0:
|
| 1012 |
-
dataset = IterableDataset.from_generator(
|
| 1013 |
-
_queued_pairs_from_replica,
|
| 1014 |
-
gen_kwargs={"shard_replicas": [tuple(shards)] * worker_replicas, "settings": settings},
|
| 1015 |
-
features=features,
|
| 1016 |
-
)
|
| 1017 |
-
else:
|
| 1018 |
-
dataset = IterableDataset.from_generator(
|
| 1019 |
-
_queued_pairs,
|
| 1020 |
-
gen_kwargs={"shards": shards, "settings": settings},
|
| 1021 |
-
features=features,
|
| 1022 |
-
)
|
| 1023 |
-
return dataset, counts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/structured_tokenizer.py
DELETED
|
@@ -1,151 +0,0 @@
|
|
| 1 |
-
"""Exact batched matching primitives for a differentiable hard tokenizer."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
|
| 7 |
-
import torch
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
@dataclass(frozen=True)
|
| 11 |
-
class MatchingResult:
|
| 12 |
-
"""Partition statistics and deterministic MAP edges for a padded batch."""
|
| 13 |
-
|
| 14 |
-
log_partition: torch.Tensor
|
| 15 |
-
marginals: torch.Tensor
|
| 16 |
-
map_edges: torch.Tensor
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def _validate_edges(
|
| 20 |
-
edge_scores: torch.Tensor, edge_mask: torch.Tensor
|
| 21 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 22 |
-
if edge_scores.ndim != 2 or edge_mask.ndim != 2:
|
| 23 |
-
raise ValueError("edge_scores and edge_mask must both have shape [batch, edges]")
|
| 24 |
-
if edge_scores.shape != edge_mask.shape:
|
| 25 |
-
raise ValueError("edge_scores and edge_mask must have identical shapes")
|
| 26 |
-
if not edge_scores.is_floating_point():
|
| 27 |
-
raise TypeError("edge_scores must be floating point")
|
| 28 |
-
if edge_mask.dtype is not torch.bool:
|
| 29 |
-
raise TypeError("edge_mask must be boolean")
|
| 30 |
-
if edge_scores.device != edge_mask.device:
|
| 31 |
-
raise ValueError("edge_scores and edge_mask must use the same device")
|
| 32 |
-
if bool((~torch.isfinite(edge_scores) & edge_mask).any()):
|
| 33 |
-
raise ValueError("valid edge scores must be finite")
|
| 34 |
-
scores = torch.where(edge_mask, edge_scores.float(), torch.zeros_like(edge_scores.float()))
|
| 35 |
-
return scores, edge_mask
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def _prefix_log_partitions(
|
| 39 |
-
scores: torch.Tensor, mask: torch.Tensor
|
| 40 |
-
) -> list[torch.Tensor]:
|
| 41 |
-
batch = scores.shape[0]
|
| 42 |
-
zero = torch.zeros(batch, dtype=torch.float32, device=scores.device)
|
| 43 |
-
prefix = [zero, zero]
|
| 44 |
-
for edge in range(scores.shape[1]):
|
| 45 |
-
separate = prefix[-1]
|
| 46 |
-
merged = prefix[-2] + scores[:, edge]
|
| 47 |
-
prefix.append(
|
| 48 |
-
torch.where(mask[:, edge], torch.logaddexp(separate, merged), separate)
|
| 49 |
-
)
|
| 50 |
-
return prefix
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def _suffix_log_partitions(
|
| 54 |
-
scores: torch.Tensor, mask: torch.Tensor
|
| 55 |
-
) -> list[torch.Tensor]:
|
| 56 |
-
batch, edges = scores.shape
|
| 57 |
-
zero = torch.zeros(batch, dtype=torch.float32, device=scores.device)
|
| 58 |
-
suffix = [zero for _ in range(edges + 2)]
|
| 59 |
-
for edge in range(edges - 1, -1, -1):
|
| 60 |
-
separate = suffix[edge + 1]
|
| 61 |
-
merged = scores[:, edge] + suffix[edge + 2]
|
| 62 |
-
suffix[edge] = torch.where(
|
| 63 |
-
mask[:, edge], torch.logaddexp(separate, merged), separate
|
| 64 |
-
)
|
| 65 |
-
return suffix
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def _map_matching(scores: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 69 |
-
batch, edges = scores.shape
|
| 70 |
-
if edges == 0:
|
| 71 |
-
return torch.zeros_like(mask)
|
| 72 |
-
|
| 73 |
-
zero = torch.zeros(batch, dtype=torch.float32, device=scores.device)
|
| 74 |
-
best = [zero, zero]
|
| 75 |
-
take_by_edge = torch.zeros_like(mask)
|
| 76 |
-
for edge in range(edges):
|
| 77 |
-
separate = best[-1]
|
| 78 |
-
merged = best[-2] + scores[:, edge]
|
| 79 |
-
take = mask[:, edge] & (merged > separate)
|
| 80 |
-
best.append(torch.where(take, merged, separate))
|
| 81 |
-
take_by_edge[:, edge] = take
|
| 82 |
-
|
| 83 |
-
selected = torch.zeros_like(mask)
|
| 84 |
-
vertices = torch.full(
|
| 85 |
-
(batch,), edges + 1, dtype=torch.long, device=scores.device
|
| 86 |
-
)
|
| 87 |
-
rows = torch.arange(batch, device=scores.device)
|
| 88 |
-
for _ in range(edges + 1):
|
| 89 |
-
active = vertices >= 2
|
| 90 |
-
edge = (vertices - 2).clamp(min=0, max=edges - 1)
|
| 91 |
-
take = active & take_by_edge[rows, edge]
|
| 92 |
-
selected[rows, edge] |= take
|
| 93 |
-
vertices = vertices - torch.where(take, 2, 1) * active.to(torch.long)
|
| 94 |
-
return selected
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def batched_matching(
|
| 98 |
-
edge_scores: torch.Tensor,
|
| 99 |
-
edge_mask: torch.Tensor,
|
| 100 |
-
) -> MatchingResult:
|
| 101 |
-
"""Solve independent monomer-dimer CRFs over a padded sentence batch.
|
| 102 |
-
|
| 103 |
-
Invalid edges act as fixed token boundaries. Every probabilistic recurrence
|
| 104 |
-
runs in float32 even when the caller is inside BF16 autocast.
|
| 105 |
-
"""
|
| 106 |
-
|
| 107 |
-
scores, mask = _validate_edges(edge_scores, edge_mask)
|
| 108 |
-
prefix = _prefix_log_partitions(scores, mask)
|
| 109 |
-
log_partition = prefix[-1]
|
| 110 |
-
|
| 111 |
-
if scores.shape[1] == 0:
|
| 112 |
-
marginals = torch.empty_like(scores)
|
| 113 |
-
else:
|
| 114 |
-
suffix = _suffix_log_partitions(scores, mask)
|
| 115 |
-
log_marginals = torch.stack(
|
| 116 |
-
[
|
| 117 |
-
prefix[edge]
|
| 118 |
-
+ scores[:, edge]
|
| 119 |
-
+ suffix[edge + 2]
|
| 120 |
-
- log_partition
|
| 121 |
-
for edge in range(scores.shape[1])
|
| 122 |
-
],
|
| 123 |
-
dim=1,
|
| 124 |
-
)
|
| 125 |
-
marginals = torch.where(mask, torch.exp(log_marginals), torch.zeros_like(scores))
|
| 126 |
-
|
| 127 |
-
map_edges = _map_matching(scores, mask)
|
| 128 |
-
if not bool(torch.isfinite(log_partition).all()):
|
| 129 |
-
raise FloatingPointError("nonfinite matching log partition")
|
| 130 |
-
if not bool(torch.isfinite(marginals).all()):
|
| 131 |
-
raise FloatingPointError("nonfinite matching marginals")
|
| 132 |
-
return MatchingResult(log_partition, marginals, map_edges)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def structured_straight_through(
|
| 136 |
-
marginals: torch.Tensor,
|
| 137 |
-
map_edges: torch.Tensor,
|
| 138 |
-
*,
|
| 139 |
-
dtype: torch.dtype,
|
| 140 |
-
) -> torch.Tensor:
|
| 141 |
-
"""Return hard MAP values whose gradient follows exact edge marginals."""
|
| 142 |
-
|
| 143 |
-
if marginals.shape != map_edges.shape:
|
| 144 |
-
raise ValueError("marginals and map_edges must have identical shapes")
|
| 145 |
-
if map_edges.dtype is not torch.bool:
|
| 146 |
-
raise TypeError("map_edges must be boolean")
|
| 147 |
-
if not dtype.is_floating_point:
|
| 148 |
-
raise TypeError("straight-through dtype must be floating point")
|
| 149 |
-
soft = marginals.to(dtype=dtype)
|
| 150 |
-
hard = map_edges.to(dtype=dtype)
|
| 151 |
-
return soft + (hard - soft).detach()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/train.py
DELETED
|
@@ -1,508 +0,0 @@
|
|
| 1 |
-
"""Train StaRSE on a source-balanced collection of Parquet sentence pairs."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import argparse
|
| 6 |
-
from bisect import bisect_right
|
| 7 |
-
import glob
|
| 8 |
-
import json
|
| 9 |
-
import os
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
from typing import Any, Iterator, Sequence
|
| 12 |
-
|
| 13 |
-
import yaml
|
| 14 |
-
|
| 15 |
-
from starse.loss import SignProjectedContrastiveLoss
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def configure_comet(config: dict[str, Any], enabled: bool) -> str | list[str]:
|
| 19 |
-
if not enabled:
|
| 20 |
-
return "none"
|
| 21 |
-
if not os.environ.get("COMET_API_KEY"):
|
| 22 |
-
raise RuntimeError("COMET_API_KEY is required unless --no-comet is used")
|
| 23 |
-
comet = dict(config.get("comet") or {})
|
| 24 |
-
os.environ.setdefault("COMET_PROJECT_NAME", str(comet.get("project_name", "starse-training")))
|
| 25 |
-
os.environ["COMET_EXPERIMENT_NAME"] = str(config["run_name"])
|
| 26 |
-
if comet.get("workspace"):
|
| 27 |
-
os.environ.setdefault("COMET_WORKSPACE", str(comet["workspace"]))
|
| 28 |
-
os.environ.setdefault("COMET_START_ONLINE", "1")
|
| 29 |
-
return ["comet_ml"]
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
class CyclingRoundRobinBatchSampler:
|
| 33 |
-
"""Repeat smaller sources so every source contributes one batch per round."""
|
| 34 |
-
|
| 35 |
-
def __init__(
|
| 36 |
-
self,
|
| 37 |
-
*,
|
| 38 |
-
dataset: Any,
|
| 39 |
-
batch_samplers: list[Any],
|
| 40 |
-
dataset_names: Sequence[str],
|
| 41 |
-
primary_dataset: str,
|
| 42 |
-
) -> None:
|
| 43 |
-
if len(dataset.datasets) != len(batch_samplers):
|
| 44 |
-
raise ValueError("dataset and batch sampler counts differ")
|
| 45 |
-
if primary_dataset not in dataset_names:
|
| 46 |
-
raise ValueError(f"Unknown primary dataset {primary_dataset!r}")
|
| 47 |
-
if any(len(sampler) == 0 for sampler in batch_samplers):
|
| 48 |
-
raise ValueError("Every training source must contain at least one complete batch")
|
| 49 |
-
self.dataset = dataset
|
| 50 |
-
self.batch_samplers = batch_samplers
|
| 51 |
-
self.dataset_names = list(dataset_names)
|
| 52 |
-
self.primary_index = self.dataset_names.index(primary_dataset)
|
| 53 |
-
self.batch_size = int(getattr(batch_samplers[0], "batch_size", 1))
|
| 54 |
-
self.drop_last = bool(getattr(batch_samplers[0], "drop_last", True))
|
| 55 |
-
self.epoch = 0
|
| 56 |
-
|
| 57 |
-
def set_epoch(self, epoch: int) -> None:
|
| 58 |
-
self.epoch = int(epoch)
|
| 59 |
-
|
| 60 |
-
def __len__(self) -> int:
|
| 61 |
-
return len(self.batch_samplers[self.primary_index]) * len(self.batch_samplers)
|
| 62 |
-
|
| 63 |
-
def _iterator(self, source_index: int, cycle: int) -> Iterator[list[int]]:
|
| 64 |
-
sampler = self.batch_samplers[source_index]
|
| 65 |
-
if hasattr(sampler, "set_epoch"):
|
| 66 |
-
sampler.set_epoch(self.epoch * 100_000 + cycle)
|
| 67 |
-
return iter(sampler)
|
| 68 |
-
|
| 69 |
-
def __iter__(self) -> Iterator[list[int]]:
|
| 70 |
-
lengths = [len(source) for source in self.dataset.datasets]
|
| 71 |
-
offsets = [0]
|
| 72 |
-
for length in lengths:
|
| 73 |
-
offsets.append(offsets[-1] + length)
|
| 74 |
-
|
| 75 |
-
iterators = [self._iterator(index, 0) for index in range(len(self.batch_samplers))]
|
| 76 |
-
cycles = [0] * len(self.batch_samplers)
|
| 77 |
-
rounds = len(self.batch_samplers[self.primary_index])
|
| 78 |
-
for _ in range(rounds):
|
| 79 |
-
for source_index in range(len(self.batch_samplers)):
|
| 80 |
-
try:
|
| 81 |
-
batch = next(iterators[source_index])
|
| 82 |
-
except StopIteration:
|
| 83 |
-
cycles[source_index] += 1
|
| 84 |
-
iterators[source_index] = self._iterator(source_index, cycles[source_index])
|
| 85 |
-
batch = next(iterators[source_index])
|
| 86 |
-
yield [index + offsets[source_index] for index in batch]
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
class LocalityAwareNoDuplicatesBatchSampler:
|
| 90 |
-
"""Shuffle Arrow-sized row blocks while keeping every batch duplicate-free.
|
| 91 |
-
|
| 92 |
-
A fully global random permutation makes a memory-mapped text dataset perform
|
| 93 |
-
thousands of unrelated page reads for every batch. This sampler uses a
|
| 94 |
-
hierarchical shuffle instead: block order and row order within every block
|
| 95 |
-
are independently randomized for each epoch. Rows with a text value that
|
| 96 |
-
already occurs in the current batch are skipped, matching the invariant of
|
| 97 |
-
Sentence Transformers' ``NO_DUPLICATES`` sampler without the random-I/O
|
| 98 |
-
pathology on hundred-million-row corpora.
|
| 99 |
-
"""
|
| 100 |
-
|
| 101 |
-
_EXCLUDED_COLUMNS = {"dataset_name", "return_loss"}
|
| 102 |
-
|
| 103 |
-
def __init__(
|
| 104 |
-
self,
|
| 105 |
-
dataset: Any,
|
| 106 |
-
*,
|
| 107 |
-
batch_size: int,
|
| 108 |
-
drop_last: bool,
|
| 109 |
-
valid_label_columns: Sequence[str] | None = None,
|
| 110 |
-
seed: int = 0,
|
| 111 |
-
block_size: int = 65_536,
|
| 112 |
-
) -> None:
|
| 113 |
-
if batch_size <= 0:
|
| 114 |
-
raise ValueError("batch_size must be positive")
|
| 115 |
-
if block_size < batch_size:
|
| 116 |
-
raise ValueError("block_size must be at least batch_size")
|
| 117 |
-
self.dataset = dataset
|
| 118 |
-
self.batch_size = int(batch_size)
|
| 119 |
-
self.drop_last = bool(drop_last)
|
| 120 |
-
self.valid_label_columns = set(valid_label_columns or ())
|
| 121 |
-
self.seed = int(seed)
|
| 122 |
-
self.block_size = int(block_size)
|
| 123 |
-
self.epoch = 0
|
| 124 |
-
|
| 125 |
-
def set_epoch(self, epoch: int) -> None:
|
| 126 |
-
self.epoch = int(epoch)
|
| 127 |
-
|
| 128 |
-
def __len__(self) -> int:
|
| 129 |
-
full_batches, remainder = divmod(len(self.dataset), self.batch_size)
|
| 130 |
-
if not self.drop_last and remainder:
|
| 131 |
-
return full_batches + 1
|
| 132 |
-
return full_batches
|
| 133 |
-
|
| 134 |
-
def _sample_values(self, row: dict[str, Any]) -> set[str]:
|
| 135 |
-
excluded = self._EXCLUDED_COLUMNS | self.valid_label_columns
|
| 136 |
-
return {str(value) for key, value in row.items() if key not in excluded}
|
| 137 |
-
|
| 138 |
-
def __iter__(self) -> Iterator[list[int]]:
|
| 139 |
-
import torch
|
| 140 |
-
|
| 141 |
-
row_count = len(self.dataset)
|
| 142 |
-
if row_count == 0:
|
| 143 |
-
return
|
| 144 |
-
|
| 145 |
-
generator = torch.Generator()
|
| 146 |
-
generator.manual_seed(self.seed + self.epoch)
|
| 147 |
-
block_count = (row_count + self.block_size - 1) // self.block_size
|
| 148 |
-
block_order = torch.randperm(block_count, generator=generator).tolist()
|
| 149 |
-
batch: list[int] = []
|
| 150 |
-
batch_values: set[str] = set()
|
| 151 |
-
|
| 152 |
-
for block_index in block_order:
|
| 153 |
-
start = block_index * self.block_size
|
| 154 |
-
stop = min(start + self.block_size, row_count)
|
| 155 |
-
block = self.dataset[start:stop]
|
| 156 |
-
if isinstance(block, dict):
|
| 157 |
-
columns = list(block)
|
| 158 |
-
rows = (
|
| 159 |
-
{column: block[column][local_index] for column in columns} for local_index in range(stop - start)
|
| 160 |
-
)
|
| 161 |
-
else:
|
| 162 |
-
rows = iter(block)
|
| 163 |
-
block_rows = list(rows)
|
| 164 |
-
local_order = torch.randperm(stop - start, generator=generator).tolist()
|
| 165 |
-
for local_index in local_order:
|
| 166 |
-
index = start + local_index
|
| 167 |
-
sample_values = self._sample_values(block_rows[local_index])
|
| 168 |
-
if not sample_values.isdisjoint(batch_values):
|
| 169 |
-
continue
|
| 170 |
-
batch.append(index)
|
| 171 |
-
batch_values.update(sample_values)
|
| 172 |
-
if len(batch) == self.batch_size:
|
| 173 |
-
# Ordering does not affect an in-batch contrastive loss. A
|
| 174 |
-
# sorted gather lets Arrow read the selected random subset
|
| 175 |
-
# sequentially instead of faulting pages in random order.
|
| 176 |
-
yield sorted(batch)
|
| 177 |
-
batch = []
|
| 178 |
-
batch_values = set()
|
| 179 |
-
|
| 180 |
-
if batch and not self.drop_last:
|
| 181 |
-
yield sorted(batch)
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
def enable_batched_concat_fetch(dataset: Any) -> None:
|
| 185 |
-
"""Give ``torch.utils.data.ConcatDataset`` a vectorized fetch path."""
|
| 186 |
-
|
| 187 |
-
if getattr(dataset, "__getitems__", None) is not None:
|
| 188 |
-
return
|
| 189 |
-
|
| 190 |
-
def getitems(indices: Sequence[int]) -> list[Any]:
|
| 191 |
-
grouped: dict[int, list[tuple[int, int]]] = {}
|
| 192 |
-
total = len(dataset)
|
| 193 |
-
for output_position, raw_index in enumerate(indices):
|
| 194 |
-
index = int(raw_index)
|
| 195 |
-
if index < 0:
|
| 196 |
-
index += total
|
| 197 |
-
if index < 0 or index >= total:
|
| 198 |
-
raise IndexError("concat dataset index out of range")
|
| 199 |
-
dataset_index = bisect_right(dataset.cumulative_sizes, index)
|
| 200 |
-
previous_size = dataset.cumulative_sizes[dataset_index - 1] if dataset_index else 0
|
| 201 |
-
grouped.setdefault(dataset_index, []).append((output_position, index - previous_size))
|
| 202 |
-
|
| 203 |
-
output: list[Any] = [None] * len(indices)
|
| 204 |
-
for dataset_index, positions in grouped.items():
|
| 205 |
-
child = dataset.datasets[dataset_index]
|
| 206 |
-
child_indices = [index for _, index in positions]
|
| 207 |
-
child_getitems = getattr(child, "__getitems__", None)
|
| 208 |
-
if child_getitems is not None:
|
| 209 |
-
samples = child_getitems(child_indices)
|
| 210 |
-
else:
|
| 211 |
-
samples = [child[index] for index in child_indices]
|
| 212 |
-
if isinstance(samples, dict):
|
| 213 |
-
columns = list(samples)
|
| 214 |
-
samples = [
|
| 215 |
-
{column: samples[column][row_index] for column in columns}
|
| 216 |
-
for row_index in range(len(child_indices))
|
| 217 |
-
]
|
| 218 |
-
for (output_position, _), sample in zip(positions, samples):
|
| 219 |
-
output[output_position] = sample
|
| 220 |
-
return output
|
| 221 |
-
|
| 222 |
-
dataset.__getitems__ = getitems
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
def load_config(path: Path) -> dict[str, Any]:
|
| 226 |
-
payload = yaml.safe_load(path.read_text(encoding="utf-8"))
|
| 227 |
-
if not isinstance(payload, dict):
|
| 228 |
-
raise TypeError("Training config must be a YAML object")
|
| 229 |
-
for key in ("run_name", "primary_dataset", "sources", "training"):
|
| 230 |
-
if key not in payload:
|
| 231 |
-
raise ValueError(f"Missing required config key: {key}")
|
| 232 |
-
if not isinstance(payload["sources"], list) or not payload["sources"]:
|
| 233 |
-
raise ValueError("sources must be a non-empty list")
|
| 234 |
-
return payload
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
def resolve_files(pattern: str, data_root: Path) -> list[Path]:
|
| 238 |
-
candidate = Path(pattern).expanduser()
|
| 239 |
-
if not candidate.is_absolute():
|
| 240 |
-
candidate = data_root / candidate
|
| 241 |
-
if candidate.is_dir():
|
| 242 |
-
files = sorted(candidate.glob("*.parquet"))
|
| 243 |
-
elif any(character in str(candidate) for character in "*?[]"):
|
| 244 |
-
files = [Path(item) for item in sorted(glob.glob(str(candidate)))]
|
| 245 |
-
else:
|
| 246 |
-
files = [candidate]
|
| 247 |
-
files = [path.resolve() for path in files if path.is_file()]
|
| 248 |
-
if not files:
|
| 249 |
-
raise FileNotFoundError(f"No Parquet files matched {pattern!r} below {data_root}")
|
| 250 |
-
return files
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
def load_pair_dataset(spec: dict[str, Any], data_root: Path) -> Any:
|
| 254 |
-
from datasets import Dataset
|
| 255 |
-
|
| 256 |
-
name = str(spec["name"])
|
| 257 |
-
anchor_column = str(spec.get("anchor_column", "anchor"))
|
| 258 |
-
positive_column = str(spec.get("positive_column", "positive"))
|
| 259 |
-
files = resolve_files(str(spec["parquet"]), data_root)
|
| 260 |
-
dataset = Dataset.from_parquet([str(path) for path in files], columns=[anchor_column, positive_column])
|
| 261 |
-
max_rows = int(spec.get("max_rows", 0) or 0)
|
| 262 |
-
if max_rows > 0 and len(dataset) > max_rows:
|
| 263 |
-
dataset = dataset.select(range(max_rows))
|
| 264 |
-
if anchor_column != "anchor":
|
| 265 |
-
dataset = dataset.rename_column(anchor_column, "anchor")
|
| 266 |
-
if positive_column != "positive":
|
| 267 |
-
dataset = dataset.rename_column(positive_column, "positive")
|
| 268 |
-
dataset = dataset.select_columns(["anchor", "positive"])
|
| 269 |
-
if len(dataset) == 0:
|
| 270 |
-
raise ValueError(f"Dataset {name!r} is empty")
|
| 271 |
-
return dataset
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
def load_eval_dataset(pattern: str, data_root: Path) -> Any:
|
| 275 |
-
return load_pair_dataset(
|
| 276 |
-
{"name": "validation", "parquet": pattern, "anchor_column": "anchor", "positive_column": "positive"},
|
| 277 |
-
data_root,
|
| 278 |
-
)
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
def train(
|
| 282 |
-
*,
|
| 283 |
-
config: dict[str, Any],
|
| 284 |
-
data_root: Path,
|
| 285 |
-
model_path: str,
|
| 286 |
-
output_dir: Path,
|
| 287 |
-
eval_parquet: str | None,
|
| 288 |
-
max_steps_override: int | None,
|
| 289 |
-
batch_size_override: int | None,
|
| 290 |
-
comet_enabled: bool,
|
| 291 |
-
disable_bf16: bool,
|
| 292 |
-
auto_resume: bool,
|
| 293 |
-
) -> dict[str, Any]:
|
| 294 |
-
from sentence_transformers import SentenceTransformer
|
| 295 |
-
from sentence_transformers.sentence_transformer.losses import MultipleNegativesSymmetricRankingLoss
|
| 296 |
-
from sentence_transformers.sentence_transformer.trainer import SentenceTransformerTrainer
|
| 297 |
-
from sentence_transformers.sentence_transformer.training_args import (
|
| 298 |
-
BatchSamplers,
|
| 299 |
-
MultiDatasetBatchSamplers,
|
| 300 |
-
SentenceTransformerTrainingArguments,
|
| 301 |
-
)
|
| 302 |
-
|
| 303 |
-
train_datasets = {}
|
| 304 |
-
for spec in config["sources"]:
|
| 305 |
-
source_name = str(spec["name"])
|
| 306 |
-
print(json.dumps({"event": "dataset_loading", "source": source_name}), flush=True)
|
| 307 |
-
dataset = load_pair_dataset(spec, data_root)
|
| 308 |
-
train_datasets[source_name] = dataset
|
| 309 |
-
print(
|
| 310 |
-
json.dumps({"event": "dataset_loaded", "source": source_name, "rows": len(dataset)}),
|
| 311 |
-
flush=True,
|
| 312 |
-
)
|
| 313 |
-
primary_dataset = str(config["primary_dataset"])
|
| 314 |
-
if primary_dataset not in train_datasets:
|
| 315 |
-
raise ValueError(f"primary_dataset={primary_dataset!r} is not present in sources")
|
| 316 |
-
|
| 317 |
-
eval_pattern = eval_parquet if eval_parquet is not None else config.get("eval_parquet")
|
| 318 |
-
eval_dataset = load_eval_dataset(str(eval_pattern), data_root) if eval_pattern else None
|
| 319 |
-
model = SentenceTransformer(model_path)
|
| 320 |
-
objective = str(config.get("objective", "contrastive"))
|
| 321 |
-
loss_settings = dict(config.get("loss") or {})
|
| 322 |
-
if objective == "contrastive":
|
| 323 |
-
loss = MultipleNegativesSymmetricRankingLoss(
|
| 324 |
-
model=model,
|
| 325 |
-
scale=float(loss_settings.get("scale", 20.0)),
|
| 326 |
-
)
|
| 327 |
-
elif objective == "sign_projected":
|
| 328 |
-
teacher = SentenceTransformer(model_path)
|
| 329 |
-
loss = SignProjectedContrastiveLoss(
|
| 330 |
-
model=model,
|
| 331 |
-
teacher=teacher,
|
| 332 |
-
scale=float(loss_settings["scale"]),
|
| 333 |
-
pair_weight=float(loss_settings["pair_weight"]),
|
| 334 |
-
teacher_kl_weight=float(loss_settings["teacher_kl_weight"]),
|
| 335 |
-
cube_weight=float(loss_settings.get("cube_weight", 0.0)),
|
| 336 |
-
balance_weight=float(loss_settings.get("balance_weight", 0.0)),
|
| 337 |
-
independence_weight=float(loss_settings.get("independence_weight", 0.0)),
|
| 338 |
-
unit_prior_weight=float(loss_settings.get("unit_prior_weight", 0.0)),
|
| 339 |
-
regularizer_sample_size=int(loss_settings.get("regularizer_sample_size", 8192)),
|
| 340 |
-
)
|
| 341 |
-
else:
|
| 342 |
-
raise ValueError(f"Unknown training objective: {objective!r}")
|
| 343 |
-
settings = dict(config["training"])
|
| 344 |
-
max_steps = int(max_steps_override or settings["max_steps"])
|
| 345 |
-
batch_size = int(batch_size_override or settings["per_device_train_batch_size"])
|
| 346 |
-
bf16 = bool(settings.get("bf16", True)) and not disable_bf16
|
| 347 |
-
scheduler_type = str(settings["lr_scheduler_type"])
|
| 348 |
-
scheduler_kwargs = dict(settings.get("lr_scheduler_kwargs") or {})
|
| 349 |
-
report_to = configure_comet(config, comet_enabled)
|
| 350 |
-
|
| 351 |
-
class StaRSETrainer(SentenceTransformerTrainer):
|
| 352 |
-
def get_batch_sampler(
|
| 353 |
-
self,
|
| 354 |
-
dataset: Any,
|
| 355 |
-
batch_size: int,
|
| 356 |
-
drop_last: bool,
|
| 357 |
-
valid_label_columns: list[str] | None = None,
|
| 358 |
-
generator: Any | None = None,
|
| 359 |
-
seed: int = 0,
|
| 360 |
-
) -> Any:
|
| 361 |
-
del generator
|
| 362 |
-
return LocalityAwareNoDuplicatesBatchSampler(
|
| 363 |
-
dataset,
|
| 364 |
-
batch_size=batch_size,
|
| 365 |
-
drop_last=drop_last,
|
| 366 |
-
valid_label_columns=valid_label_columns,
|
| 367 |
-
seed=seed,
|
| 368 |
-
block_size=int(settings.get("sampler_block_size", 65_536)),
|
| 369 |
-
)
|
| 370 |
-
|
| 371 |
-
def get_multi_dataset_batch_sampler(
|
| 372 |
-
self,
|
| 373 |
-
dataset: Any,
|
| 374 |
-
batch_samplers: list[Any],
|
| 375 |
-
generator: Any | None = None,
|
| 376 |
-
seed: int | None = 0,
|
| 377 |
-
) -> Any:
|
| 378 |
-
del generator, seed
|
| 379 |
-
enable_batched_concat_fetch(dataset)
|
| 380 |
-
return CyclingRoundRobinBatchSampler(
|
| 381 |
-
dataset=dataset,
|
| 382 |
-
batch_samplers=batch_samplers,
|
| 383 |
-
dataset_names=list(self.train_dataset.keys()),
|
| 384 |
-
primary_dataset=primary_dataset,
|
| 385 |
-
)
|
| 386 |
-
|
| 387 |
-
args = SentenceTransformerTrainingArguments(
|
| 388 |
-
output_dir=str(output_dir),
|
| 389 |
-
run_name=str(config["run_name"]),
|
| 390 |
-
max_steps=max_steps,
|
| 391 |
-
num_train_epochs=1,
|
| 392 |
-
learning_rate=float(settings["learning_rate"]),
|
| 393 |
-
per_device_train_batch_size=batch_size,
|
| 394 |
-
per_device_eval_batch_size=int(settings.get("per_device_eval_batch_size", 256)),
|
| 395 |
-
gradient_accumulation_steps=int(settings.get("gradient_accumulation_steps", 1)),
|
| 396 |
-
warmup_steps=int(settings["warmup_steps"]),
|
| 397 |
-
weight_decay=float(settings["weight_decay"]),
|
| 398 |
-
lr_scheduler_type=scheduler_type,
|
| 399 |
-
lr_scheduler_kwargs=scheduler_kwargs,
|
| 400 |
-
eval_strategy="steps" if eval_dataset is not None else "no",
|
| 401 |
-
eval_steps=int(settings.get("eval_steps", 100)),
|
| 402 |
-
save_strategy="steps",
|
| 403 |
-
save_steps=int(settings.get("save_steps", 1000)),
|
| 404 |
-
save_total_limit=None,
|
| 405 |
-
logging_steps=int(settings.get("logging_steps", 1)),
|
| 406 |
-
report_to=report_to,
|
| 407 |
-
bf16=bf16,
|
| 408 |
-
bf16_full_eval=bf16,
|
| 409 |
-
dataloader_drop_last=bool(settings.get("dataloader_drop_last", False)),
|
| 410 |
-
dataloader_num_workers=int(settings.get("dataloader_num_workers", 0)),
|
| 411 |
-
dataloader_pin_memory=True,
|
| 412 |
-
load_best_model_at_end=False,
|
| 413 |
-
batch_sampler=BatchSamplers.NO_DUPLICATES,
|
| 414 |
-
multi_dataset_batch_sampler=MultiDatasetBatchSamplers.PROPORTIONAL,
|
| 415 |
-
seed=int(config.get("seed", 20260509)),
|
| 416 |
-
data_seed=int(config.get("seed", 20260509)),
|
| 417 |
-
remove_unused_columns=False,
|
| 418 |
-
accelerator_config={"dispatch_batches": False},
|
| 419 |
-
)
|
| 420 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 421 |
-
public_config = {
|
| 422 |
-
"run_name": config["run_name"],
|
| 423 |
-
"objective": objective,
|
| 424 |
-
"primary_dataset": primary_dataset,
|
| 425 |
-
"sources": [
|
| 426 |
-
{
|
| 427 |
-
"name": spec["name"],
|
| 428 |
-
"parquet": spec["parquet"],
|
| 429 |
-
"anchor_column": spec.get("anchor_column", "anchor"),
|
| 430 |
-
"positive_column": spec.get("positive_column", "positive"),
|
| 431 |
-
"max_rows": spec.get("max_rows"),
|
| 432 |
-
}
|
| 433 |
-
for spec in config["sources"]
|
| 434 |
-
],
|
| 435 |
-
"loss": loss_settings,
|
| 436 |
-
"training": {**settings, "max_steps": max_steps, "per_device_train_batch_size": batch_size, "bf16": bf16},
|
| 437 |
-
}
|
| 438 |
-
(output_dir / "run_config.json").write_text(
|
| 439 |
-
json.dumps(public_config, ensure_ascii=False, indent=2) + "\n",
|
| 440 |
-
encoding="utf-8",
|
| 441 |
-
)
|
| 442 |
-
|
| 443 |
-
trainer = StaRSETrainer(
|
| 444 |
-
model=model,
|
| 445 |
-
args=args,
|
| 446 |
-
train_dataset=train_datasets,
|
| 447 |
-
eval_dataset=eval_dataset,
|
| 448 |
-
loss=loss,
|
| 449 |
-
)
|
| 450 |
-
checkpoints = sorted(
|
| 451 |
-
(
|
| 452 |
-
path
|
| 453 |
-
for path in output_dir.glob("checkpoint-*")
|
| 454 |
-
if path.is_dir() and path.name.removeprefix("checkpoint-").isdigit()
|
| 455 |
-
),
|
| 456 |
-
key=lambda path: int(path.name.removeprefix("checkpoint-")),
|
| 457 |
-
)
|
| 458 |
-
resume_from_checkpoint = str(checkpoints[-1]) if auto_resume and checkpoints else None
|
| 459 |
-
result = trainer.train(resume_from_checkpoint=resume_from_checkpoint)
|
| 460 |
-
trainer.save_model()
|
| 461 |
-
metrics = dict(result.metrics)
|
| 462 |
-
(output_dir / "train_result.json").write_text(
|
| 463 |
-
json.dumps(metrics, ensure_ascii=False, indent=2) + "\n",
|
| 464 |
-
encoding="utf-8",
|
| 465 |
-
)
|
| 466 |
-
return metrics
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
| 470 |
-
parser = argparse.ArgumentParser(description=__doc__)
|
| 471 |
-
parser.add_argument("--config", type=Path, required=True)
|
| 472 |
-
parser.add_argument("--data-root", type=Path, required=True)
|
| 473 |
-
parser.add_argument("--model", required=True, help="Initialization model or previous-stage checkpoint")
|
| 474 |
-
parser.add_argument("--output-dir", type=Path, required=True)
|
| 475 |
-
parser.add_argument("--eval-parquet", default=None, help="Override the config's eval Parquet path")
|
| 476 |
-
parser.add_argument("--max-steps", type=int, default=None)
|
| 477 |
-
parser.add_argument("--batch-size", type=int, default=None)
|
| 478 |
-
parser.add_argument("--comet", action=argparse.BooleanOptionalAction, default=True)
|
| 479 |
-
parser.add_argument("--resume", action=argparse.BooleanOptionalAction, default=True)
|
| 480 |
-
parser.add_argument("--no-bf16", action="store_true")
|
| 481 |
-
parser.add_argument("--print-config", action="store_true")
|
| 482 |
-
return parser.parse_args(argv)
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
def main(argv: list[str] | None = None) -> int:
|
| 486 |
-
args = parse_args(argv)
|
| 487 |
-
config = load_config(args.config.resolve())
|
| 488 |
-
if args.print_config:
|
| 489 |
-
print(yaml.safe_dump(config, allow_unicode=True, sort_keys=False))
|
| 490 |
-
return 0
|
| 491 |
-
metrics = train(
|
| 492 |
-
config=config,
|
| 493 |
-
data_root=args.data_root.resolve(),
|
| 494 |
-
model_path=args.model,
|
| 495 |
-
output_dir=args.output_dir.resolve(),
|
| 496 |
-
eval_parquet=args.eval_parquet,
|
| 497 |
-
max_steps_override=args.max_steps,
|
| 498 |
-
batch_size_override=args.batch_size,
|
| 499 |
-
comet_enabled=args.comet,
|
| 500 |
-
disable_bf16=args.no_bf16,
|
| 501 |
-
auto_resume=args.resume,
|
| 502 |
-
)
|
| 503 |
-
print(json.dumps({"status": "ok", "metrics": metrics}, ensure_ascii=False))
|
| 504 |
-
return 0
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
if __name__ == "__main__":
|
| 508 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/train_multilingual.py
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
starse/train_progressive.py
DELETED
|
@@ -1,340 +0,0 @@
|
|
| 1 |
-
"""Grow StaRSE with one frozen-prefix ModernBERT layer per converged stage."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import argparse
|
| 6 |
-
import json
|
| 7 |
-
import os
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
from typing import Any
|
| 10 |
-
|
| 11 |
-
import yaml
|
| 12 |
-
from transformers import EarlyStoppingCallback
|
| 13 |
-
|
| 14 |
-
from starse.progressive_model import ProgressiveEncoder
|
| 15 |
-
from starse.train import CyclingRoundRobinBatchSampler, load_pair_dataset, resolve_files
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def load_config(path: Path) -> dict[str, Any]:
|
| 19 |
-
config = yaml.safe_load(path.read_text(encoding="utf-8"))
|
| 20 |
-
if not isinstance(config, dict):
|
| 21 |
-
raise TypeError("Training config must be a YAML object")
|
| 22 |
-
for key in ("run_name", "primary_dataset", "sources", "loss", "convergence", "training"):
|
| 23 |
-
if key not in config:
|
| 24 |
-
raise ValueError(f"Missing required config key: {key}")
|
| 25 |
-
if not isinstance(config["sources"], list) or not config["sources"]:
|
| 26 |
-
raise ValueError("sources must be a non-empty list")
|
| 27 |
-
return config
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def load_balanced_eval_dataset(
|
| 31 |
-
pattern: str,
|
| 32 |
-
*,
|
| 33 |
-
data_root: Path,
|
| 34 |
-
source_count: int,
|
| 35 |
-
rows_per_source: int,
|
| 36 |
-
) -> Any:
|
| 37 |
-
"""Take the same prefix from each contiguous source block in the article eval set."""
|
| 38 |
-
|
| 39 |
-
from datasets import Dataset
|
| 40 |
-
|
| 41 |
-
files = resolve_files(pattern, data_root)
|
| 42 |
-
dataset = Dataset.from_parquet([str(path) for path in files], columns=["anchor", "positive"])
|
| 43 |
-
if len(dataset) % source_count != 0:
|
| 44 |
-
raise ValueError("The balanced validation dataset cannot be split into equal source blocks")
|
| 45 |
-
block_size = len(dataset) // source_count
|
| 46 |
-
if rows_per_source > block_size:
|
| 47 |
-
raise ValueError(f"Requested {rows_per_source} validation rows per source, but only {block_size} exist")
|
| 48 |
-
indices = [
|
| 49 |
-
index
|
| 50 |
-
for source_index in range(source_count)
|
| 51 |
-
for index in range(source_index * block_size, source_index * block_size + rows_per_source)
|
| 52 |
-
]
|
| 53 |
-
return dataset.select(indices)
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def configure_comet(config: dict[str, Any], *, stage_depth: int, enabled: bool) -> str | list[str]:
|
| 57 |
-
if not enabled:
|
| 58 |
-
return "none"
|
| 59 |
-
if not os.environ.get("COMET_API_KEY"):
|
| 60 |
-
raise RuntimeError("COMET_API_KEY is required unless --no-comet is used")
|
| 61 |
-
comet = dict(config.get("comet") or {})
|
| 62 |
-
os.environ.setdefault("COMET_PROJECT_NAME", str(comet.get("project_name", "starse-progressive")))
|
| 63 |
-
os.environ["COMET_EXPERIMENT_NAME"] = f"{config['run_name']}-depth-{stage_depth:02d}"
|
| 64 |
-
if comet.get("workspace"):
|
| 65 |
-
os.environ.setdefault("COMET_WORKSPACE", str(comet["workspace"]))
|
| 66 |
-
os.environ.setdefault("COMET_START_ONLINE", "1")
|
| 67 |
-
return ["comet_ml"]
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def build_stage_model(
|
| 71 |
-
*,
|
| 72 |
-
stage_depth: int,
|
| 73 |
-
previous_model: str | None,
|
| 74 |
-
base_model: str,
|
| 75 |
-
donor_model: str,
|
| 76 |
-
max_seq_length: int,
|
| 77 |
-
pca_components: int,
|
| 78 |
-
pca_device: str,
|
| 79 |
-
) -> Any:
|
| 80 |
-
from sentence_transformers import SentenceTransformer
|
| 81 |
-
from sentence_transformers.sentence_transformer.modules import Normalize
|
| 82 |
-
|
| 83 |
-
if stage_depth == 1:
|
| 84 |
-
if previous_model is not None:
|
| 85 |
-
raise ValueError("Stage 1 must start from --base-model, not --previous-model")
|
| 86 |
-
encoder = ProgressiveEncoder.initialize_from_models(
|
| 87 |
-
base_model_name=base_model,
|
| 88 |
-
donor_model_path=donor_model,
|
| 89 |
-
max_seq_length=max_seq_length,
|
| 90 |
-
pca_components=pca_components,
|
| 91 |
-
pca_device=pca_device,
|
| 92 |
-
)
|
| 93 |
-
return SentenceTransformer(modules=[encoder, Normalize()])
|
| 94 |
-
|
| 95 |
-
if previous_model is None:
|
| 96 |
-
raise ValueError("Stages above 1 require --previous-model")
|
| 97 |
-
model = SentenceTransformer(previous_model, trust_remote_code=True)
|
| 98 |
-
encoder = model[0]
|
| 99 |
-
if not isinstance(encoder, ProgressiveEncoder):
|
| 100 |
-
raise TypeError("The previous checkpoint does not contain ProgressiveEncoder")
|
| 101 |
-
if encoder.active_depth != stage_depth - 1:
|
| 102 |
-
raise ValueError(
|
| 103 |
-
f"Previous checkpoint has depth {encoder.active_depth}; stage {stage_depth} requires depth {stage_depth - 1}"
|
| 104 |
-
)
|
| 105 |
-
encoder.add_donor_layer(donor_model)
|
| 106 |
-
return model
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def train_stage(
|
| 110 |
-
*,
|
| 111 |
-
config: dict[str, Any],
|
| 112 |
-
data_root: Path,
|
| 113 |
-
stage_depth: int,
|
| 114 |
-
previous_model: str | None,
|
| 115 |
-
base_model: str,
|
| 116 |
-
donor_model: str,
|
| 117 |
-
output_dir: Path,
|
| 118 |
-
max_steps_override: int | None,
|
| 119 |
-
batch_size_override: int | None,
|
| 120 |
-
resume_from_checkpoint: str | None,
|
| 121 |
-
comet_enabled: bool,
|
| 122 |
-
disable_bf16: bool,
|
| 123 |
-
pca_device: str,
|
| 124 |
-
) -> dict[str, Any]:
|
| 125 |
-
import torch
|
| 126 |
-
from sentence_transformers.sentence_transformer.losses import CachedMultipleNegativesRankingLoss
|
| 127 |
-
from sentence_transformers.sentence_transformer.trainer import SentenceTransformerTrainer
|
| 128 |
-
from sentence_transformers.sentence_transformer.training_args import (
|
| 129 |
-
BatchSamplers,
|
| 130 |
-
MultiDatasetBatchSamplers,
|
| 131 |
-
SentenceTransformerTrainingArguments,
|
| 132 |
-
)
|
| 133 |
-
|
| 134 |
-
settings = dict(config["training"])
|
| 135 |
-
convergence = dict(config["convergence"])
|
| 136 |
-
loss_settings = dict(config["loss"])
|
| 137 |
-
model = build_stage_model(
|
| 138 |
-
stage_depth=stage_depth,
|
| 139 |
-
previous_model=previous_model,
|
| 140 |
-
base_model=base_model,
|
| 141 |
-
donor_model=donor_model,
|
| 142 |
-
max_seq_length=int(settings.get("max_seq_length", 128)),
|
| 143 |
-
pca_components=int(settings.get("pca_components", 384)),
|
| 144 |
-
pca_device=pca_device,
|
| 145 |
-
)
|
| 146 |
-
encoder = model[0]
|
| 147 |
-
counts = encoder.trainable_parameter_counts()
|
| 148 |
-
total_depth = len(encoder.source_layer_types)
|
| 149 |
-
if stage_depth > total_depth:
|
| 150 |
-
raise ValueError(f"Requested depth {stage_depth}, but the donor has only {total_depth} layers")
|
| 151 |
-
|
| 152 |
-
train_datasets = {str(spec["name"]): load_pair_dataset(spec, data_root) for spec in config["sources"]}
|
| 153 |
-
primary_dataset = str(config["primary_dataset"])
|
| 154 |
-
if primary_dataset not in train_datasets:
|
| 155 |
-
raise ValueError(f"primary_dataset={primary_dataset!r} is not present in sources")
|
| 156 |
-
eval_dataset = load_balanced_eval_dataset(
|
| 157 |
-
str(convergence["eval_parquet"]),
|
| 158 |
-
data_root=data_root,
|
| 159 |
-
source_count=len(config["sources"]),
|
| 160 |
-
rows_per_source=int(convergence["rows_per_source"]),
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
training_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 164 |
-
model.to(training_device)
|
| 165 |
-
loss = CachedMultipleNegativesRankingLoss(
|
| 166 |
-
model=model,
|
| 167 |
-
scale=float(loss_settings.get("scale", 20.0)),
|
| 168 |
-
mini_batch_size=int(loss_settings.get("mini_batch_size", 64)),
|
| 169 |
-
directions=("query_to_doc", "doc_to_query"),
|
| 170 |
-
partition_mode="per_direction",
|
| 171 |
-
)
|
| 172 |
-
max_steps = int(max_steps_override or settings.get("safety_max_steps", 20_000))
|
| 173 |
-
batch_size = int(batch_size_override or settings["per_device_train_batch_size"])
|
| 174 |
-
bf16 = bool(settings.get("bf16", True)) and not disable_bf16
|
| 175 |
-
eval_steps = int(convergence.get("eval_steps", 100))
|
| 176 |
-
report_to = configure_comet(config, stage_depth=stage_depth, enabled=comet_enabled)
|
| 177 |
-
early_stopping = EarlyStoppingCallback(
|
| 178 |
-
early_stopping_patience=int(convergence.get("early_stopping_patience", 20)),
|
| 179 |
-
early_stopping_threshold=float(convergence.get("early_stopping_threshold", 0.0)),
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
class ProgressiveTrainer(SentenceTransformerTrainer):
|
| 183 |
-
def get_multi_dataset_batch_sampler(
|
| 184 |
-
self,
|
| 185 |
-
dataset: Any,
|
| 186 |
-
batch_samplers: list[Any],
|
| 187 |
-
generator: Any | None = None,
|
| 188 |
-
seed: int | None = 0,
|
| 189 |
-
) -> Any:
|
| 190 |
-
del generator, seed
|
| 191 |
-
return CyclingRoundRobinBatchSampler(
|
| 192 |
-
dataset=dataset,
|
| 193 |
-
batch_samplers=batch_samplers,
|
| 194 |
-
dataset_names=list(self.train_dataset.keys()),
|
| 195 |
-
primary_dataset=primary_dataset,
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
trainer_dir = output_dir / "trainer"
|
| 199 |
-
model_dir = output_dir / "model"
|
| 200 |
-
args = SentenceTransformerTrainingArguments(
|
| 201 |
-
output_dir=str(trainer_dir),
|
| 202 |
-
run_name=f"{config['run_name']}-depth-{stage_depth:02d}",
|
| 203 |
-
max_steps=max_steps,
|
| 204 |
-
num_train_epochs=1,
|
| 205 |
-
learning_rate=float(settings["learning_rate"]),
|
| 206 |
-
learning_rate_mapping={
|
| 207 |
-
# Sentence-Transformers applies these regexes to the model held by
|
| 208 |
-
# CachedMultipleNegativesRankingLoss (hence the ``model.`` prefix).
|
| 209 |
-
r"^model\.0\.output_gate$": float(settings.get("gate_learning_rate", settings["learning_rate"])),
|
| 210 |
-
r"^model\.0\.backbone\.layers\.\d+\.gate$": float(
|
| 211 |
-
settings.get("gate_learning_rate", settings["learning_rate"])
|
| 212 |
-
),
|
| 213 |
-
},
|
| 214 |
-
per_device_train_batch_size=batch_size,
|
| 215 |
-
per_device_eval_batch_size=int(settings.get("per_device_eval_batch_size", 512)),
|
| 216 |
-
gradient_accumulation_steps=1,
|
| 217 |
-
warmup_steps=int(settings.get("warmup_steps", 100)),
|
| 218 |
-
weight_decay=float(settings.get("weight_decay", 0.01)),
|
| 219 |
-
lr_scheduler_type=str(settings.get("lr_scheduler_type", "constant_with_warmup")),
|
| 220 |
-
eval_strategy="steps",
|
| 221 |
-
eval_steps=eval_steps,
|
| 222 |
-
save_strategy="steps",
|
| 223 |
-
save_steps=eval_steps,
|
| 224 |
-
save_total_limit=int(settings.get("save_total_limit", 2)),
|
| 225 |
-
logging_steps=int(settings.get("logging_steps", 10)),
|
| 226 |
-
report_to=report_to,
|
| 227 |
-
bf16=bf16,
|
| 228 |
-
bf16_full_eval=bf16,
|
| 229 |
-
dataloader_drop_last=True,
|
| 230 |
-
dataloader_num_workers=int(settings.get("dataloader_num_workers", 0)),
|
| 231 |
-
dataloader_pin_memory=True,
|
| 232 |
-
load_best_model_at_end=True,
|
| 233 |
-
metric_for_best_model="eval_loss",
|
| 234 |
-
greater_is_better=False,
|
| 235 |
-
batch_sampler=BatchSamplers.NO_DUPLICATES,
|
| 236 |
-
multi_dataset_batch_sampler=MultiDatasetBatchSamplers.PROPORTIONAL,
|
| 237 |
-
seed=int(config.get("seed", 20260715)),
|
| 238 |
-
data_seed=int(config.get("seed", 20260715)),
|
| 239 |
-
remove_unused_columns=False,
|
| 240 |
-
accelerator_config={"dispatch_batches": False},
|
| 241 |
-
)
|
| 242 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 243 |
-
public_config = {
|
| 244 |
-
"run_name": config["run_name"],
|
| 245 |
-
"stage_depth": stage_depth,
|
| 246 |
-
"total_depth": total_depth,
|
| 247 |
-
"primary_dataset": primary_dataset,
|
| 248 |
-
"sources": config["sources"],
|
| 249 |
-
"loss": loss_settings,
|
| 250 |
-
"convergence": convergence,
|
| 251 |
-
"training": {
|
| 252 |
-
**settings,
|
| 253 |
-
"safety_max_steps": max_steps,
|
| 254 |
-
"per_device_train_batch_size": batch_size,
|
| 255 |
-
"bf16": bf16,
|
| 256 |
-
},
|
| 257 |
-
"parameters": counts,
|
| 258 |
-
"training_device": training_device.type,
|
| 259 |
-
}
|
| 260 |
-
(output_dir / "run_config.json").write_text(
|
| 261 |
-
json.dumps(public_config, ensure_ascii=False, indent=2) + "\n",
|
| 262 |
-
encoding="utf-8",
|
| 263 |
-
)
|
| 264 |
-
|
| 265 |
-
trainer = ProgressiveTrainer(
|
| 266 |
-
model=model,
|
| 267 |
-
args=args,
|
| 268 |
-
train_dataset=train_datasets,
|
| 269 |
-
eval_dataset=eval_dataset,
|
| 270 |
-
loss=loss,
|
| 271 |
-
callbacks=[early_stopping],
|
| 272 |
-
)
|
| 273 |
-
result = trainer.train(resume_from_checkpoint=resume_from_checkpoint)
|
| 274 |
-
trainer.save_model(str(model_dir))
|
| 275 |
-
metrics = {
|
| 276 |
-
**dict(result.metrics),
|
| 277 |
-
"stage_depth": stage_depth,
|
| 278 |
-
"total_depth": total_depth,
|
| 279 |
-
"best_eval_loss": trainer.state.best_metric,
|
| 280 |
-
"stopped_at_step": int(trainer.state.global_step),
|
| 281 |
-
"early_stopping_patience": early_stopping.early_stopping_patience,
|
| 282 |
-
"early_stopping_threshold": early_stopping.early_stopping_threshold,
|
| 283 |
-
"trainable_parameters": counts["trainable"],
|
| 284 |
-
"total_parameters": counts["total"],
|
| 285 |
-
"output_gate": float(trainer.model[0].output_gate.detach().float().cpu()),
|
| 286 |
-
"layer_gates": [float(layer.gate.detach().float().cpu()) for layer in trainer.model[0].backbone.layers],
|
| 287 |
-
}
|
| 288 |
-
(output_dir / "train_result.json").write_text(
|
| 289 |
-
json.dumps(metrics, ensure_ascii=False, indent=2) + "\n",
|
| 290 |
-
encoding="utf-8",
|
| 291 |
-
)
|
| 292 |
-
return metrics
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
| 296 |
-
parser = argparse.ArgumentParser(description=__doc__)
|
| 297 |
-
parser.add_argument("--config", type=Path, required=True)
|
| 298 |
-
parser.add_argument("--data-root", type=Path, required=True)
|
| 299 |
-
parser.add_argument("--stage-depth", type=int, required=True)
|
| 300 |
-
parser.add_argument("--previous-model", default=None)
|
| 301 |
-
parser.add_argument("--base-model", default="BorisTM/starse")
|
| 302 |
-
parser.add_argument("--donor-model", required=True)
|
| 303 |
-
parser.add_argument("--output-dir", type=Path, required=True)
|
| 304 |
-
parser.add_argument("--max-steps", type=int, default=None)
|
| 305 |
-
parser.add_argument("--batch-size", type=int, default=None)
|
| 306 |
-
parser.add_argument("--resume-from-checkpoint", default=None)
|
| 307 |
-
parser.add_argument("--pca-device", default="cpu")
|
| 308 |
-
parser.add_argument("--comet", action=argparse.BooleanOptionalAction, default=True)
|
| 309 |
-
parser.add_argument("--no-bf16", action="store_true")
|
| 310 |
-
parser.add_argument("--print-config", action="store_true")
|
| 311 |
-
return parser.parse_args(argv)
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
def main(argv: list[str] | None = None) -> int:
|
| 315 |
-
args = parse_args(argv)
|
| 316 |
-
config = load_config(args.config.resolve())
|
| 317 |
-
if args.print_config:
|
| 318 |
-
print(yaml.safe_dump(config, allow_unicode=True, sort_keys=False))
|
| 319 |
-
return 0
|
| 320 |
-
metrics = train_stage(
|
| 321 |
-
config=config,
|
| 322 |
-
data_root=args.data_root.resolve(),
|
| 323 |
-
stage_depth=args.stage_depth,
|
| 324 |
-
previous_model=args.previous_model,
|
| 325 |
-
base_model=args.base_model,
|
| 326 |
-
donor_model=args.donor_model,
|
| 327 |
-
output_dir=args.output_dir.resolve(),
|
| 328 |
-
max_steps_override=args.max_steps,
|
| 329 |
-
batch_size_override=args.batch_size,
|
| 330 |
-
resume_from_checkpoint=args.resume_from_checkpoint,
|
| 331 |
-
comet_enabled=args.comet,
|
| 332 |
-
disable_bf16=args.no_bf16,
|
| 333 |
-
pca_device=args.pca_device,
|
| 334 |
-
)
|
| 335 |
-
print(json.dumps({"status": "ok", "metrics": metrics}, ensure_ascii=False))
|
| 336 |
-
return 0
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
if __name__ == "__main__":
|
| 340 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/translation_edit.py
DELETED
|
@@ -1,142 +0,0 @@
|
|
| 1 |
-
"""Translation-Edit Invariance: one aligned phrase swap, everything else frozen.
|
| 2 |
-
|
| 3 |
-
Why a full-sentence bitext loss is not enough for an additive encoder
|
| 4 |
-
--------------------------------------------------------------------
|
| 5 |
-
A static sentence vector is a sum over rows, `s(x) = sum_t E_t`. For a parallel
|
| 6 |
-
pair with `m` aligned phrase groups, the sentence-level objective only ever sees
|
| 7 |
-
|
| 8 |
-
s(x) - s(y) = sum_k [ phi(p_k) - phi(q_k) ] + r_unaligned,
|
| 9 |
-
|
| 10 |
-
which is `d` constraints against roughly `m*d` local degrees of freedom. Lexical
|
| 11 |
-
errors are free to cancel: `delta_1 + ... + delta_m` can sit near zero while every
|
| 12 |
-
individual `delta_k` is large. A Transformer can repair a badly-placed row later
|
| 13 |
-
using context; a static table cannot, because the row *is* the representation.
|
| 14 |
-
|
| 15 |
-
The edit objective
|
| 16 |
-
------------------
|
| 17 |
-
Replace exactly one aligned phrase and hold the rest of the sentence fixed:
|
| 18 |
-
|
| 19 |
-
c_x = stopgrad( s(x) - phi(p) )
|
| 20 |
-
L = 1 - cos( c_x + phi(p), c_x + phi(q) )
|
| 21 |
-
|
| 22 |
-
`c_x + phi(p)` is `s(x)`; `c_x + phi(q)` is the code-switched hybrid. Both are
|
| 23 |
-
sums over table rows, so no extra forward pass through anything is needed — the
|
| 24 |
-
whole term is arithmetic on the embedding matrix.
|
| 25 |
-
|
| 26 |
-
The stop-gradient is the mechanism, not a detail. Without it this is ordinary
|
| 27 |
-
code-switched consistency and every row in the sentence is updated, which
|
| 28 |
-
reintroduces the cancellation the objective exists to remove. With it, one
|
| 29 |
-
parallel sentence yields several *independent* local constraints.
|
| 30 |
-
|
| 31 |
-
The term is added to the existing objective and shares its optimiser; the edits
|
| 32 |
-
are sampled from a buffer precomputed by
|
| 33 |
-
`scripts/multilingual_suite/build_translation_edits.py`.
|
| 34 |
-
"""
|
| 35 |
-
|
| 36 |
-
from __future__ import annotations
|
| 37 |
-
|
| 38 |
-
from pathlib import Path
|
| 39 |
-
from typing import Any
|
| 40 |
-
|
| 41 |
-
import numpy as np
|
| 42 |
-
import torch
|
| 43 |
-
import torch.nn.functional as F
|
| 44 |
-
|
| 45 |
-
SIDES = ("src_context", "src_phrase", "tgt_context", "tgt_phrase")
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def _segment_sum(table: torch.Tensor, ids: torch.Tensor, segment: torch.Tensor,
|
| 49 |
-
groups: int) -> torch.Tensor:
|
| 50 |
-
"""Sum the rows named by `ids` into `groups` buckets given by `segment`."""
|
| 51 |
-
out = table.new_zeros((groups, table.shape[1]))
|
| 52 |
-
out.index_add_(0, segment, table[ids])
|
| 53 |
-
return out
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
class TranslationEditLoss(torch.nn.Module):
|
| 57 |
-
def __init__(
|
| 58 |
-
self,
|
| 59 |
-
loss: torch.nn.Module,
|
| 60 |
-
module: torch.nn.Module,
|
| 61 |
-
edits_path: str | Path,
|
| 62 |
-
*,
|
| 63 |
-
weight: float,
|
| 64 |
-
edits_per_step: int = 2048,
|
| 65 |
-
tangent: bool = False,
|
| 66 |
-
seed: int = 0,
|
| 67 |
-
) -> None:
|
| 68 |
-
super().__init__()
|
| 69 |
-
self.loss = loss
|
| 70 |
-
self.module = module
|
| 71 |
-
self.weight = float(weight)
|
| 72 |
-
self.edits_per_step = int(edits_per_step)
|
| 73 |
-
self.tangent = bool(tangent)
|
| 74 |
-
self.generator = torch.Generator().manual_seed(int(seed))
|
| 75 |
-
|
| 76 |
-
data = np.load(str(edits_path))
|
| 77 |
-
self.n_edits = int(data["languages"].shape[0])
|
| 78 |
-
for side in SIDES:
|
| 79 |
-
self.register_buffer(f"{side}_ids",
|
| 80 |
-
torch.from_numpy(data[f"{side}_ids"].astype(np.int64)),
|
| 81 |
-
persistent=False)
|
| 82 |
-
self.register_buffer(f"{side}_offsets",
|
| 83 |
-
torch.from_numpy(data[f"{side}_offsets"].astype(np.int64)),
|
| 84 |
-
persistent=False)
|
| 85 |
-
self.codes = [str(code) for code in data["codes"]]
|
| 86 |
-
|
| 87 |
-
def _gather(self, side: str, index: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 88 |
-
"""Flat row ids and their group index for the sampled edits."""
|
| 89 |
-
offsets = getattr(self, f"{side}_offsets")
|
| 90 |
-
ids = getattr(self, f"{side}_ids")
|
| 91 |
-
starts = offsets[index]
|
| 92 |
-
lengths = offsets[index + 1] - starts
|
| 93 |
-
total = int(lengths.sum())
|
| 94 |
-
segment = torch.repeat_interleave(
|
| 95 |
-
torch.arange(index.shape[0], device=ids.device), lengths)
|
| 96 |
-
exclusive = torch.cumsum(lengths, 0) - lengths
|
| 97 |
-
within = (torch.arange(total, device=ids.device)
|
| 98 |
-
- torch.repeat_interleave(exclusive, lengths))
|
| 99 |
-
return ids[torch.repeat_interleave(starts, lengths) + within], segment
|
| 100 |
-
|
| 101 |
-
def edit_term(self) -> torch.Tensor:
|
| 102 |
-
table = self.module.embedding.weight
|
| 103 |
-
device = table.device
|
| 104 |
-
count = min(self.edits_per_step, self.n_edits)
|
| 105 |
-
index = torch.randint(0, self.n_edits, (count,), generator=self.generator).to(device)
|
| 106 |
-
|
| 107 |
-
summed = {}
|
| 108 |
-
for side in SIDES:
|
| 109 |
-
ids, segment = self._gather(side, index)
|
| 110 |
-
summed[side] = _segment_sum(table, ids, segment, count)
|
| 111 |
-
|
| 112 |
-
source_context = summed["src_context"].detach()
|
| 113 |
-
target_context = summed["tgt_context"].detach()
|
| 114 |
-
phrase, translation = summed["src_phrase"], summed["tgt_phrase"]
|
| 115 |
-
|
| 116 |
-
if self.tangent:
|
| 117 |
-
# Only the component of the swap that survives L2 normalisation
|
| 118 |
-
# matters; the radial part is removed by the metric anyway.
|
| 119 |
-
total = 0.0
|
| 120 |
-
for context, kept, swapped in ((source_context, phrase, translation),
|
| 121 |
-
(target_context, translation, phrase)):
|
| 122 |
-
sentence = context + kept
|
| 123 |
-
direction = F.normalize(sentence.detach(), dim=1)
|
| 124 |
-
difference = swapped - kept
|
| 125 |
-
radial = (difference * direction).sum(dim=1, keepdim=True) * direction
|
| 126 |
-
scale = sentence.detach().pow(2).sum(dim=1, keepdim=True).clamp_min(1e-8)
|
| 127 |
-
total = total + ((difference - radial).pow(2).sum(dim=1, keepdim=True)
|
| 128 |
-
/ scale).mean()
|
| 129 |
-
return total
|
| 130 |
-
|
| 131 |
-
total = 0.0
|
| 132 |
-
for context, kept, swapped in ((source_context, phrase, translation),
|
| 133 |
-
(target_context, translation, phrase)):
|
| 134 |
-
total = total + (1.0 - F.cosine_similarity(context + kept,
|
| 135 |
-
context + swapped, dim=1)).mean()
|
| 136 |
-
return total
|
| 137 |
-
|
| 138 |
-
def forward(self, sentence_features: Any, labels: Any = None) -> torch.Tensor:
|
| 139 |
-
value = self.loss(sentence_features, labels)
|
| 140 |
-
if self.weight:
|
| 141 |
-
value = value + self.weight * self.edit_term()
|
| 142 |
-
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/vocab_assignment.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
| 1 |
-
"""K vocabularies, and a learned assignment of languages to them.
|
| 2 |
-
|
| 3 |
-
The earlier fusion attempt merged *per token*: two languages could share a row
|
| 4 |
-
for one word and keep separate rows for the next. That is cell-level sharing,
|
| 5 |
-
not vocabulary sharing, and it is why the readout came out as mush — the merge
|
| 6 |
-
decision was never coupled across a language's tokens.
|
| 7 |
-
|
| 8 |
-
Here the decision lives at the language level. Each language holds one
|
| 9 |
-
distribution over K vocabularies, used for every token it emits:
|
| 10 |
-
|
| 11 |
-
E[l, w] = E_shared[w] + sum_k pi[l, k] * Delta_k[w]
|
| 12 |
-
|
| 13 |
-
`pi` is L x K — 48 numbers for twelve languages and four vocabularies — so the
|
| 14 |
-
grouping is read directly off the parameter instead of being inferred from row
|
| 15 |
-
distances. An entropy penalty pushes each language toward one vocabulary, and K
|
| 16 |
-
is the explicit axis: K=1 is today's single shared vocabulary, K=L with a hard
|
| 17 |
-
assignment is one per language, and where quality stops improving is the answer
|
| 18 |
-
to how many a multilingual encoder actually needs.
|
| 19 |
-
|
| 20 |
-
Offsets start at zero so the model is numerically identical to the shared one at
|
| 21 |
-
step zero. The assignment logits carry the small random asymmetry instead: with
|
| 22 |
-
identical offsets and a uniform assignment every expert would receive the same
|
| 23 |
-
gradient and the K vocabularies would collapse back into one by symmetry.
|
| 24 |
-
"""
|
| 25 |
-
|
| 26 |
-
from __future__ import annotations
|
| 27 |
-
|
| 28 |
-
import numpy as np
|
| 29 |
-
import torch
|
| 30 |
-
from torch import nn
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
class VocabAssignment(nn.Module):
|
| 34 |
-
def __init__(self, index_path: str, vocab_size: int, dim: int,
|
| 35 |
-
n_languages: int, n_vocabs: int = 4, seed: int = 20260727) -> None:
|
| 36 |
-
super().__init__()
|
| 37 |
-
data = np.load(index_path)
|
| 38 |
-
tokens = np.unique(data["token_index"].astype(np.int64))
|
| 39 |
-
self.n_tokens = int(tokens.size)
|
| 40 |
-
self.n_vocabs = int(n_vocabs)
|
| 41 |
-
|
| 42 |
-
slot = torch.full((vocab_size,), -1, dtype=torch.long)
|
| 43 |
-
slot[torch.tensor(tokens)] = torch.arange(self.n_tokens)
|
| 44 |
-
self.register_buffer("slot", slot, persistent=False)
|
| 45 |
-
|
| 46 |
-
self.offsets = nn.Parameter(torch.zeros(self.n_vocabs, self.n_tokens, dim))
|
| 47 |
-
generator = torch.Generator().manual_seed(seed)
|
| 48 |
-
self.logits = nn.Parameter(
|
| 49 |
-
torch.randn(n_languages, self.n_vocabs, generator=generator) * 0.01)
|
| 50 |
-
|
| 51 |
-
def assignment(self) -> torch.Tensor:
|
| 52 |
-
return torch.softmax(self.logits, dim=-1)
|
| 53 |
-
|
| 54 |
-
def entropy(self) -> torch.Tensor:
|
| 55 |
-
"""Mean assignment entropy; penalising it drives each language to one slot."""
|
| 56 |
-
pi = self.assignment()
|
| 57 |
-
return -(pi * torch.log(pi.clamp_min(1e-9))).sum(-1).mean()
|
| 58 |
-
|
| 59 |
-
def forward(self, token_ids: torch.Tensor, lang_per_token: torch.Tensor,
|
| 60 |
-
shared: torch.Tensor) -> torch.Tensor:
|
| 61 |
-
index = self.slot[token_ids]
|
| 62 |
-
has = index >= 0
|
| 63 |
-
if not bool(has.any()):
|
| 64 |
-
return shared
|
| 65 |
-
safe = index.clamp(min=0)
|
| 66 |
-
# (K, N, dim) gathered offsets weighted by each token's language row of pi
|
| 67 |
-
gathered = self.offsets[:, safe]
|
| 68 |
-
weights = self.assignment()[lang_per_token]
|
| 69 |
-
delta = torch.einsum("nk,knd->nd", weights, gathered)
|
| 70 |
-
return shared + delta * has.unsqueeze(1)
|
| 71 |
-
|
| 72 |
-
def extra_repr(self) -> str:
|
| 73 |
-
return f"tokens={self.n_tokens}, vocabs={self.n_vocabs}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/vocabulary_growth.py
DELETED
|
@@ -1,598 +0,0 @@
|
|
| 1 |
-
"""Training-only selectors for monotone exact-pair vocabulary growth."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from dataclasses import dataclass
|
| 6 |
-
import math
|
| 7 |
-
from typing import Any, Iterable
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@dataclass(frozen=True)
|
| 13 |
-
class LanguageGrowthRecord:
|
| 14 |
-
language_index: int
|
| 15 |
-
utility: float
|
| 16 |
-
support: int
|
| 17 |
-
probability_sum: float
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@dataclass(frozen=True)
|
| 21 |
-
class PairGrowthRecord:
|
| 22 |
-
pair_key: int
|
| 23 |
-
utility: float
|
| 24 |
-
support: int
|
| 25 |
-
probability_sum: float
|
| 26 |
-
languages: tuple[LanguageGrowthRecord, ...]
|
| 27 |
-
|
| 28 |
-
@property
|
| 29 |
-
def mean_probability(self) -> float:
|
| 30 |
-
return self.probability_sum / self.support if self.support else 0.0
|
| 31 |
-
|
| 32 |
-
def has_supported_positive_language(self, minimum_support: int) -> bool:
|
| 33 |
-
return any(
|
| 34 |
-
row.utility > 0 and row.support >= minimum_support
|
| 35 |
-
for row in self.languages
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
@dataclass(frozen=True)
|
| 40 |
-
class GrowthWindow:
|
| 41 |
-
n_languages: int
|
| 42 |
-
pairs: tuple[PairGrowthRecord, ...]
|
| 43 |
-
|
| 44 |
-
@classmethod
|
| 45 |
-
def from_discovery(cls, discovery: Any) -> "GrowthWindow":
|
| 46 |
-
n_languages = int(getattr(discovery, "n_languages", 0))
|
| 47 |
-
if n_languages <= 0:
|
| 48 |
-
raise ValueError("growth discovery has no valid language universe")
|
| 49 |
-
rows = tuple(discovery.snapshot_records())
|
| 50 |
-
seen_composites: set[int] = set()
|
| 51 |
-
by_pair: dict[int, list[LanguageGrowthRecord]] = {}
|
| 52 |
-
for raw in rows:
|
| 53 |
-
composite = int(raw["composite_key"])
|
| 54 |
-
pair_key = int(raw["pair_key"])
|
| 55 |
-
language = int(raw["language_index"])
|
| 56 |
-
utility = float(raw["utility"])
|
| 57 |
-
probability_sum = float(raw["probability_sum"])
|
| 58 |
-
support = int(raw["captured_support"])
|
| 59 |
-
if composite in seen_composites:
|
| 60 |
-
raise ValueError("duplicate composite growth identity")
|
| 61 |
-
seen_composites.add(composite)
|
| 62 |
-
if composite != pair_key * n_languages + language:
|
| 63 |
-
raise ValueError("growth composite identity is inconsistent")
|
| 64 |
-
if (
|
| 65 |
-
pair_key < 0
|
| 66 |
-
or language < 0
|
| 67 |
-
or language >= n_languages
|
| 68 |
-
or support <= 0
|
| 69 |
-
or not math.isfinite(utility)
|
| 70 |
-
or not math.isfinite(probability_sum)
|
| 71 |
-
or probability_sum < 0
|
| 72 |
-
or probability_sum > support
|
| 73 |
-
):
|
| 74 |
-
raise ValueError("growth discovery record is invalid")
|
| 75 |
-
by_pair.setdefault(pair_key, []).append(
|
| 76 |
-
LanguageGrowthRecord(
|
| 77 |
-
language_index=language,
|
| 78 |
-
utility=utility,
|
| 79 |
-
support=support,
|
| 80 |
-
probability_sum=probability_sum,
|
| 81 |
-
)
|
| 82 |
-
)
|
| 83 |
-
pairs = []
|
| 84 |
-
for pair_key, language_rows in sorted(by_pair.items()):
|
| 85 |
-
language_rows.sort(key=lambda row: row.language_index)
|
| 86 |
-
if len({row.language_index for row in language_rows}) != len(language_rows):
|
| 87 |
-
raise ValueError("duplicate pair/language growth identity")
|
| 88 |
-
pairs.append(
|
| 89 |
-
PairGrowthRecord(
|
| 90 |
-
pair_key=pair_key,
|
| 91 |
-
utility=math.fsum(row.utility for row in language_rows),
|
| 92 |
-
support=sum(row.support for row in language_rows),
|
| 93 |
-
probability_sum=math.fsum(
|
| 94 |
-
row.probability_sum for row in language_rows
|
| 95 |
-
),
|
| 96 |
-
languages=tuple(language_rows),
|
| 97 |
-
)
|
| 98 |
-
)
|
| 99 |
-
return cls(n_languages=n_languages, pairs=tuple(pairs))
|
| 100 |
-
|
| 101 |
-
def by_key(self) -> dict[int, PairGrowthRecord]:
|
| 102 |
-
return {row.pair_key: row for row in self.pairs}
|
| 103 |
-
|
| 104 |
-
def state_dict(self) -> dict[str, Any]:
|
| 105 |
-
"""Return a weights-only-safe, schema-versioned checkpoint payload."""
|
| 106 |
-
|
| 107 |
-
return {
|
| 108 |
-
"version": 1,
|
| 109 |
-
"n_languages": self.n_languages,
|
| 110 |
-
"pairs": [
|
| 111 |
-
{
|
| 112 |
-
"pair_key": row.pair_key,
|
| 113 |
-
"utility": row.utility,
|
| 114 |
-
"support": row.support,
|
| 115 |
-
"probability_sum": row.probability_sum,
|
| 116 |
-
"languages": [
|
| 117 |
-
{
|
| 118 |
-
"language_index": language.language_index,
|
| 119 |
-
"utility": language.utility,
|
| 120 |
-
"support": language.support,
|
| 121 |
-
"probability_sum": language.probability_sum,
|
| 122 |
-
}
|
| 123 |
-
for language in row.languages
|
| 124 |
-
],
|
| 125 |
-
}
|
| 126 |
-
for row in self.pairs
|
| 127 |
-
],
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
@classmethod
|
| 131 |
-
def load_state_dict(cls, state: dict[str, Any]) -> "GrowthWindow":
|
| 132 |
-
if set(state) != {"version", "n_languages", "pairs"} or state["version"] != 1:
|
| 133 |
-
raise ValueError("unknown growth-window checkpoint schema")
|
| 134 |
-
n_languages = int(state["n_languages"])
|
| 135 |
-
if n_languages <= 0 or not isinstance(state["pairs"], list):
|
| 136 |
-
raise ValueError("invalid growth-window checkpoint")
|
| 137 |
-
pairs = []
|
| 138 |
-
previous_key = -1
|
| 139 |
-
for raw in state["pairs"]:
|
| 140 |
-
if not isinstance(raw, dict) or set(raw) != {
|
| 141 |
-
"pair_key", "utility", "support", "probability_sum", "languages"
|
| 142 |
-
}:
|
| 143 |
-
raise ValueError("invalid growth-window pair checkpoint")
|
| 144 |
-
key = int(raw["pair_key"])
|
| 145 |
-
if key <= previous_key or not isinstance(raw["languages"], list):
|
| 146 |
-
raise ValueError("growth-window keys must be unique and sorted")
|
| 147 |
-
previous_key = key
|
| 148 |
-
languages = tuple(
|
| 149 |
-
LanguageGrowthRecord(
|
| 150 |
-
language_index=int(language["language_index"]),
|
| 151 |
-
utility=float(language["utility"]),
|
| 152 |
-
support=int(language["support"]),
|
| 153 |
-
probability_sum=float(language["probability_sum"]),
|
| 154 |
-
)
|
| 155 |
-
for language in raw["languages"]
|
| 156 |
-
)
|
| 157 |
-
row = PairGrowthRecord(
|
| 158 |
-
pair_key=key,
|
| 159 |
-
utility=float(raw["utility"]),
|
| 160 |
-
support=int(raw["support"]),
|
| 161 |
-
probability_sum=float(raw["probability_sum"]),
|
| 162 |
-
languages=languages,
|
| 163 |
-
)
|
| 164 |
-
if (
|
| 165 |
-
row.support <= 0
|
| 166 |
-
or not math.isfinite(row.utility)
|
| 167 |
-
or not math.isfinite(row.probability_sum)
|
| 168 |
-
or tuple(sorted({item.language_index for item in languages}))
|
| 169 |
-
!= tuple(item.language_index for item in languages)
|
| 170 |
-
or any(
|
| 171 |
-
item.language_index < 0
|
| 172 |
-
or item.language_index >= n_languages
|
| 173 |
-
or item.support <= 0
|
| 174 |
-
or not math.isfinite(item.utility)
|
| 175 |
-
or not math.isfinite(item.probability_sum)
|
| 176 |
-
for item in languages
|
| 177 |
-
)
|
| 178 |
-
or row.support != sum(item.support for item in languages)
|
| 179 |
-
or not math.isclose(
|
| 180 |
-
row.utility,
|
| 181 |
-
math.fsum(item.utility for item in languages),
|
| 182 |
-
rel_tol=1e-12,
|
| 183 |
-
abs_tol=1e-12,
|
| 184 |
-
)
|
| 185 |
-
or not math.isclose(
|
| 186 |
-
row.probability_sum,
|
| 187 |
-
math.fsum(item.probability_sum for item in languages),
|
| 188 |
-
rel_tol=1e-12,
|
| 189 |
-
abs_tol=1e-12,
|
| 190 |
-
)
|
| 191 |
-
):
|
| 192 |
-
raise ValueError("inconsistent growth-window checkpoint")
|
| 193 |
-
pairs.append(row)
|
| 194 |
-
return cls(n_languages=n_languages, pairs=tuple(pairs))
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
@dataclass(frozen=True)
|
| 198 |
-
class PairSignEvidence:
|
| 199 |
-
pair_key: int
|
| 200 |
-
utility: float
|
| 201 |
-
support: int
|
| 202 |
-
positive_support: int
|
| 203 |
-
|
| 204 |
-
def lower_positive_rate(self, *, hypotheses: int, alpha: float) -> float:
|
| 205 |
-
radius = math.sqrt(
|
| 206 |
-
math.log((2.0 * max(1, hypotheses)) / alpha) / (2.0 * self.support)
|
| 207 |
-
)
|
| 208 |
-
return (self.positive_support / self.support) - radius
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
@dataclass(frozen=True)
|
| 212 |
-
class SignEvidenceWindow:
|
| 213 |
-
n_languages: int
|
| 214 |
-
pairs: tuple[PairSignEvidence, ...]
|
| 215 |
-
|
| 216 |
-
@classmethod
|
| 217 |
-
def from_discovery(cls, discovery: Any) -> "SignEvidenceWindow":
|
| 218 |
-
n_languages = int(getattr(discovery, "n_languages", 0))
|
| 219 |
-
if n_languages <= 0:
|
| 220 |
-
raise ValueError("sign discovery has no valid language universe")
|
| 221 |
-
by_pair: dict[int, list[float | int]] = {}
|
| 222 |
-
seen_composites: set[int] = set()
|
| 223 |
-
for raw in discovery.snapshot_records():
|
| 224 |
-
composite = int(raw["composite_key"])
|
| 225 |
-
pair_key = int(raw["pair_key"])
|
| 226 |
-
language = int(raw["language_index"])
|
| 227 |
-
support = int(raw["captured_support"])
|
| 228 |
-
positive = int(raw.get("positive_utility_support", -1))
|
| 229 |
-
utility = float(raw["utility"])
|
| 230 |
-
if composite in seen_composites:
|
| 231 |
-
raise ValueError("duplicate composite sign identity")
|
| 232 |
-
seen_composites.add(composite)
|
| 233 |
-
if (
|
| 234 |
-
composite != pair_key * n_languages + language
|
| 235 |
-
or pair_key < 0
|
| 236 |
-
or language < 0
|
| 237 |
-
or language >= n_languages
|
| 238 |
-
or support <= 0
|
| 239 |
-
or positive < 0
|
| 240 |
-
or positive > support
|
| 241 |
-
or not math.isfinite(utility)
|
| 242 |
-
):
|
| 243 |
-
raise ValueError("sign discovery record is invalid")
|
| 244 |
-
row = by_pair.setdefault(pair_key, [0.0, 0, 0])
|
| 245 |
-
row[0] = float(row[0]) + utility
|
| 246 |
-
row[1] = int(row[1]) + support
|
| 247 |
-
row[2] = int(row[2]) + positive
|
| 248 |
-
pairs = tuple(
|
| 249 |
-
PairSignEvidence(
|
| 250 |
-
pair_key=pair_key,
|
| 251 |
-
utility=float(values[0]),
|
| 252 |
-
support=int(values[1]),
|
| 253 |
-
positive_support=int(values[2]),
|
| 254 |
-
)
|
| 255 |
-
for pair_key, values in sorted(by_pair.items())
|
| 256 |
-
)
|
| 257 |
-
return cls(n_languages=n_languages, pairs=pairs)
|
| 258 |
-
|
| 259 |
-
def by_key(self) -> dict[int, PairSignEvidence]:
|
| 260 |
-
return {row.pair_key: row for row in self.pairs}
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
@dataclass(frozen=True)
|
| 264 |
-
class GrowthSelection:
|
| 265 |
-
pair_keys: tuple[int, ...]
|
| 266 |
-
candidate_count: int
|
| 267 |
-
rejections: dict[str, int]
|
| 268 |
-
language_allocations: tuple[tuple[int, int], ...] = ()
|
| 269 |
-
global_backfill: int = 0
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
def _validate_support(min_support: int, per_language_min_support: int) -> None:
|
| 273 |
-
if min_support <= 0 or per_language_min_support <= 0:
|
| 274 |
-
raise ValueError("growth support thresholds must be positive")
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
def select_stable_growth(
|
| 278 |
-
previous: GrowthWindow,
|
| 279 |
-
current: GrowthWindow,
|
| 280 |
-
existing_keys: Iterable[int],
|
| 281 |
-
*,
|
| 282 |
-
min_support: int = 32,
|
| 283 |
-
per_language_min_support: int = 8,
|
| 284 |
-
) -> GrowthSelection:
|
| 285 |
-
_validate_support(min_support, per_language_min_support)
|
| 286 |
-
if previous.n_languages != current.n_languages:
|
| 287 |
-
raise ValueError("growth windows use different language universes")
|
| 288 |
-
old = previous.by_key()
|
| 289 |
-
existing = {int(key) for key in existing_keys}
|
| 290 |
-
rejected = {
|
| 291 |
-
"not_consecutive": 0,
|
| 292 |
-
"nonpositive_utility": 0,
|
| 293 |
-
"insufficient_support": 0,
|
| 294 |
-
"no_supported_positive_language": 0,
|
| 295 |
-
"already_exact": 0,
|
| 296 |
-
}
|
| 297 |
-
selected = []
|
| 298 |
-
for row in current.pairs:
|
| 299 |
-
prior = old.get(row.pair_key)
|
| 300 |
-
if row.pair_key in existing:
|
| 301 |
-
rejected["already_exact"] += 1
|
| 302 |
-
elif prior is None:
|
| 303 |
-
rejected["not_consecutive"] += 1
|
| 304 |
-
elif row.utility <= 0 or prior.utility <= 0:
|
| 305 |
-
rejected["nonpositive_utility"] += 1
|
| 306 |
-
elif row.support < min_support or prior.support < min_support:
|
| 307 |
-
rejected["insufficient_support"] += 1
|
| 308 |
-
elif not (
|
| 309 |
-
row.has_supported_positive_language(per_language_min_support)
|
| 310 |
-
and prior.has_supported_positive_language(per_language_min_support)
|
| 311 |
-
):
|
| 312 |
-
rejected["no_supported_positive_language"] += 1
|
| 313 |
-
else:
|
| 314 |
-
selected.append(row.pair_key)
|
| 315 |
-
return GrowthSelection(
|
| 316 |
-
pair_keys=tuple(selected),
|
| 317 |
-
candidate_count=len(current.pairs),
|
| 318 |
-
rejections=rejected,
|
| 319 |
-
)
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
def select_language_balanced_growth(
|
| 323 |
-
previous: GrowthWindow,
|
| 324 |
-
current: GrowthWindow,
|
| 325 |
-
existing_keys: Iterable[int],
|
| 326 |
-
*,
|
| 327 |
-
limit: int,
|
| 328 |
-
min_support: int = 1,
|
| 329 |
-
per_language_min_support: int = 1,
|
| 330 |
-
) -> GrowthSelection:
|
| 331 |
-
"""Fill an active-row target with stable, language-balanced candidates.
|
| 332 |
-
|
| 333 |
-
Utilities are normalized inside each language and half-window before equal
|
| 334 |
-
language reservations are applied. The token rows themselves remain
|
| 335 |
-
shared: language identity affects admission only, not representation.
|
| 336 |
-
"""
|
| 337 |
-
|
| 338 |
-
_validate_support(min_support, per_language_min_support)
|
| 339 |
-
if isinstance(limit, bool) or int(limit) < 0:
|
| 340 |
-
raise ValueError("growth selection limit must be nonnegative")
|
| 341 |
-
limit = int(limit)
|
| 342 |
-
if previous.n_languages != current.n_languages:
|
| 343 |
-
raise ValueError("growth windows use different language universes")
|
| 344 |
-
old = previous.by_key()
|
| 345 |
-
existing = {int(key) for key in existing_keys}
|
| 346 |
-
rejected = {
|
| 347 |
-
"not_consecutive": 0,
|
| 348 |
-
"nonpositive_utility": 0,
|
| 349 |
-
"insufficient_support": 0,
|
| 350 |
-
"no_stable_positive_language": 0,
|
| 351 |
-
"already_exact": 0,
|
| 352 |
-
"outside_active_target": 0,
|
| 353 |
-
}
|
| 354 |
-
eligible: list[tuple[PairGrowthRecord, PairGrowthRecord, dict[int, tuple[float, float]]]] = []
|
| 355 |
-
for row in current.pairs:
|
| 356 |
-
prior = old.get(row.pair_key)
|
| 357 |
-
if row.pair_key in existing:
|
| 358 |
-
rejected["already_exact"] += 1
|
| 359 |
-
continue
|
| 360 |
-
if prior is None:
|
| 361 |
-
rejected["not_consecutive"] += 1
|
| 362 |
-
continue
|
| 363 |
-
if row.utility <= 0 or prior.utility <= 0:
|
| 364 |
-
rejected["nonpositive_utility"] += 1
|
| 365 |
-
continue
|
| 366 |
-
if row.support < min_support or prior.support < min_support:
|
| 367 |
-
rejected["insufficient_support"] += 1
|
| 368 |
-
continue
|
| 369 |
-
prior_languages = {item.language_index: item for item in prior.languages}
|
| 370 |
-
stable_languages = {}
|
| 371 |
-
for item in row.languages:
|
| 372 |
-
old_item = prior_languages.get(item.language_index)
|
| 373 |
-
if (
|
| 374 |
-
old_item is not None
|
| 375 |
-
and item.utility > 0
|
| 376 |
-
and old_item.utility > 0
|
| 377 |
-
and item.support >= per_language_min_support
|
| 378 |
-
and old_item.support >= per_language_min_support
|
| 379 |
-
):
|
| 380 |
-
stable_languages[item.language_index] = (
|
| 381 |
-
old_item.utility,
|
| 382 |
-
item.utility,
|
| 383 |
-
)
|
| 384 |
-
if not stable_languages:
|
| 385 |
-
rejected["no_stable_positive_language"] += 1
|
| 386 |
-
continue
|
| 387 |
-
eligible.append((prior, row, stable_languages))
|
| 388 |
-
|
| 389 |
-
previous_totals = [0.0] * previous.n_languages
|
| 390 |
-
current_totals = [0.0] * current.n_languages
|
| 391 |
-
for _, _, languages in eligible:
|
| 392 |
-
for language, (old_utility, new_utility) in languages.items():
|
| 393 |
-
previous_totals[language] += old_utility
|
| 394 |
-
current_totals[language] += new_utility
|
| 395 |
-
|
| 396 |
-
by_language: dict[int, list[tuple[float, int]]] = {}
|
| 397 |
-
global_rows: list[tuple[float, int]] = []
|
| 398 |
-
for prior, row, languages in eligible:
|
| 399 |
-
language_scores = []
|
| 400 |
-
for language, (old_utility, new_utility) in languages.items():
|
| 401 |
-
score = min(
|
| 402 |
-
old_utility / previous_totals[language],
|
| 403 |
-
new_utility / current_totals[language],
|
| 404 |
-
)
|
| 405 |
-
by_language.setdefault(language, []).append((score, row.pair_key))
|
| 406 |
-
language_scores.append(score)
|
| 407 |
-
global_rows.append((max(language_scores), row.pair_key))
|
| 408 |
-
for rows in by_language.values():
|
| 409 |
-
rows.sort(key=lambda value: (-value[0], value[1]))
|
| 410 |
-
global_rows.sort(key=lambda value: (-value[0], value[1]))
|
| 411 |
-
|
| 412 |
-
languages = sorted(by_language)
|
| 413 |
-
selected: list[int] = []
|
| 414 |
-
selected_set: set[int] = set()
|
| 415 |
-
allocations: list[tuple[int, int]] = []
|
| 416 |
-
if languages and limit:
|
| 417 |
-
quotient, remainder = divmod(limit, len(languages))
|
| 418 |
-
for position, language in enumerate(languages):
|
| 419 |
-
quota = quotient + int(position < remainder)
|
| 420 |
-
admitted = 0
|
| 421 |
-
for _, pair_key in by_language[language]:
|
| 422 |
-
if admitted >= quota or len(selected) >= limit:
|
| 423 |
-
break
|
| 424 |
-
if pair_key in selected_set:
|
| 425 |
-
continue
|
| 426 |
-
selected.append(pair_key)
|
| 427 |
-
selected_set.add(pair_key)
|
| 428 |
-
admitted += 1
|
| 429 |
-
allocations.append((language, admitted))
|
| 430 |
-
backfill = 0
|
| 431 |
-
for _, pair_key in global_rows:
|
| 432 |
-
if len(selected) >= limit:
|
| 433 |
-
break
|
| 434 |
-
if pair_key in selected_set:
|
| 435 |
-
continue
|
| 436 |
-
selected.append(pair_key)
|
| 437 |
-
selected_set.add(pair_key)
|
| 438 |
-
backfill += 1
|
| 439 |
-
rejected["outside_active_target"] = len(eligible) - len(selected)
|
| 440 |
-
return GrowthSelection(
|
| 441 |
-
pair_keys=tuple(selected),
|
| 442 |
-
candidate_count=len(current.pairs),
|
| 443 |
-
rejections=rejected,
|
| 444 |
-
language_allocations=tuple(allocations),
|
| 445 |
-
global_backfill=backfill,
|
| 446 |
-
)
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
def select_familywise_sign_growth(
|
| 450 |
-
previous: SignEvidenceWindow,
|
| 451 |
-
current: SignEvidenceWindow,
|
| 452 |
-
existing_keys: Iterable[int],
|
| 453 |
-
*,
|
| 454 |
-
alpha: float = 0.05,
|
| 455 |
-
) -> GrowthSelection:
|
| 456 |
-
"""Select pairs whose positive-utility rate clears a two-window FWER bound."""
|
| 457 |
-
|
| 458 |
-
if (
|
| 459 |
-
not math.isfinite(alpha)
|
| 460 |
-
or alpha <= 0
|
| 461 |
-
or alpha > 1
|
| 462 |
-
or previous.n_languages != current.n_languages
|
| 463 |
-
):
|
| 464 |
-
raise ValueError("invalid familywise sign-growth settings")
|
| 465 |
-
old = previous.by_key()
|
| 466 |
-
existing = {int(key) for key in existing_keys}
|
| 467 |
-
previous_hypotheses = len(previous.pairs)
|
| 468 |
-
current_hypotheses = len(current.pairs)
|
| 469 |
-
rejected = {
|
| 470 |
-
"already_exact": 0,
|
| 471 |
-
"not_consecutive": 0,
|
| 472 |
-
"nonpositive_utility": 0,
|
| 473 |
-
"insufficient_familywise_sign_evidence": 0,
|
| 474 |
-
}
|
| 475 |
-
selected = []
|
| 476 |
-
for row in current.pairs:
|
| 477 |
-
prior = old.get(row.pair_key)
|
| 478 |
-
if row.pair_key in existing:
|
| 479 |
-
rejected["already_exact"] += 1
|
| 480 |
-
elif prior is None:
|
| 481 |
-
rejected["not_consecutive"] += 1
|
| 482 |
-
elif row.utility <= 0 or prior.utility <= 0:
|
| 483 |
-
rejected["nonpositive_utility"] += 1
|
| 484 |
-
elif (
|
| 485 |
-
row.lower_positive_rate(
|
| 486 |
-
hypotheses=current_hypotheses, alpha=alpha
|
| 487 |
-
)
|
| 488 |
-
<= 0.5
|
| 489 |
-
or prior.lower_positive_rate(
|
| 490 |
-
hypotheses=previous_hypotheses, alpha=alpha
|
| 491 |
-
)
|
| 492 |
-
<= 0.5
|
| 493 |
-
):
|
| 494 |
-
rejected["insufficient_familywise_sign_evidence"] += 1
|
| 495 |
-
else:
|
| 496 |
-
selected.append(row.pair_key)
|
| 497 |
-
return GrowthSelection(
|
| 498 |
-
pair_keys=tuple(selected),
|
| 499 |
-
candidate_count=current_hypotheses,
|
| 500 |
-
rejections=rejected,
|
| 501 |
-
)
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
def hard_concrete_open_probability(
|
| 505 |
-
probability: float,
|
| 506 |
-
*,
|
| 507 |
-
beta: float = 2.0 / 3.0,
|
| 508 |
-
gamma: float = -0.1,
|
| 509 |
-
zeta: float = 1.1,
|
| 510 |
-
) -> float:
|
| 511 |
-
probability = float(probability)
|
| 512 |
-
if (
|
| 513 |
-
not math.isfinite(probability)
|
| 514 |
-
or probability < 0
|
| 515 |
-
or probability > 1
|
| 516 |
-
or not math.isfinite(beta)
|
| 517 |
-
or beta <= 0
|
| 518 |
-
or not math.isfinite(gamma)
|
| 519 |
-
or not math.isfinite(zeta)
|
| 520 |
-
or not gamma < 0 < 1 < zeta
|
| 521 |
-
):
|
| 522 |
-
raise ValueError("invalid hard-concrete probability or constants")
|
| 523 |
-
if probability == 0:
|
| 524 |
-
return 0.0
|
| 525 |
-
if probability == 1:
|
| 526 |
-
return 1.0
|
| 527 |
-
logit = math.log(probability / (1.0 - probability))
|
| 528 |
-
shifted = logit - beta * math.log(-gamma / zeta)
|
| 529 |
-
return 1.0 / (1.0 + math.exp(-shifted))
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
def select_l0_growth(
|
| 533 |
-
current: GrowthWindow,
|
| 534 |
-
existing_keys: Iterable[int],
|
| 535 |
-
*,
|
| 536 |
-
min_support: int = 32,
|
| 537 |
-
per_language_min_support: int = 8,
|
| 538 |
-
open_threshold: float = 0.5,
|
| 539 |
-
beta: float = 2.0 / 3.0,
|
| 540 |
-
gamma: float = -0.1,
|
| 541 |
-
zeta: float = 1.1,
|
| 542 |
-
) -> GrowthSelection:
|
| 543 |
-
_validate_support(min_support, per_language_min_support)
|
| 544 |
-
if not math.isfinite(open_threshold) or not 0 < open_threshold < 1:
|
| 545 |
-
raise ValueError("growth open threshold must be between zero and one")
|
| 546 |
-
existing = {int(key) for key in existing_keys}
|
| 547 |
-
rejected = {
|
| 548 |
-
"below_open_threshold": 0,
|
| 549 |
-
"nonpositive_utility": 0,
|
| 550 |
-
"insufficient_support": 0,
|
| 551 |
-
"no_supported_positive_language": 0,
|
| 552 |
-
"already_exact": 0,
|
| 553 |
-
}
|
| 554 |
-
selected = []
|
| 555 |
-
for row in current.pairs:
|
| 556 |
-
if row.pair_key in existing:
|
| 557 |
-
rejected["already_exact"] += 1
|
| 558 |
-
elif row.utility <= 0:
|
| 559 |
-
rejected["nonpositive_utility"] += 1
|
| 560 |
-
elif row.support < min_support:
|
| 561 |
-
rejected["insufficient_support"] += 1
|
| 562 |
-
elif not row.has_supported_positive_language(per_language_min_support):
|
| 563 |
-
rejected["no_supported_positive_language"] += 1
|
| 564 |
-
elif hard_concrete_open_probability(
|
| 565 |
-
row.mean_probability, beta=beta, gamma=gamma, zeta=zeta
|
| 566 |
-
) < open_threshold:
|
| 567 |
-
rejected["below_open_threshold"] += 1
|
| 568 |
-
else:
|
| 569 |
-
selected.append(row.pair_key)
|
| 570 |
-
return GrowthSelection(
|
| 571 |
-
pair_keys=tuple(selected),
|
| 572 |
-
candidate_count=len(current.pairs),
|
| 573 |
-
rejections=rejected,
|
| 574 |
-
)
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
def expected_hard_concrete_l0(
|
| 578 |
-
collapse: Any,
|
| 579 |
-
*,
|
| 580 |
-
beta: float = 2.0 / 3.0,
|
| 581 |
-
gamma: float = -0.1,
|
| 582 |
-
zeta: float = 1.1,
|
| 583 |
-
) -> torch.Tensor:
|
| 584 |
-
if not beta > 0 or not gamma < 0 < 1 < zeta:
|
| 585 |
-
raise ValueError("invalid hard-concrete constants")
|
| 586 |
-
if int(getattr(collapse, "residual_buckets", 0)):
|
| 587 |
-
logits = collapse.residual_score
|
| 588 |
-
elif not int(collapse.pair_keys.numel()):
|
| 589 |
-
logits = collapse.score
|
| 590 |
-
else:
|
| 591 |
-
raise ValueError("exact-only collapse has no proposal channel")
|
| 592 |
-
if not logits.numel() or not bool(torch.isfinite(logits).all()):
|
| 593 |
-
raise ValueError("proposal logits are empty or nonfinite")
|
| 594 |
-
if not bool(torch.isfinite(collapse.language_bias).all()):
|
| 595 |
-
raise ValueError("language bias is nonfinite")
|
| 596 |
-
shift = float(beta) * math.log(-float(gamma) / float(zeta))
|
| 597 |
-
combined = logits.float() + collapse.language_bias.float().mean()
|
| 598 |
-
return torch.sigmoid(combined - shift).mean()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/vocabulary_probe.py
DELETED
|
@@ -1,720 +0,0 @@
|
|
| 1 |
-
"""Frozen training-only vocabulary probe inventory and checkpoint audit."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import base64
|
| 6 |
-
from collections import defaultdict
|
| 7 |
-
import hashlib
|
| 8 |
-
import json
|
| 9 |
-
import math
|
| 10 |
-
import os
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
import struct
|
| 13 |
-
from typing import Mapping
|
| 14 |
-
import zlib
|
| 15 |
-
|
| 16 |
-
import pyarrow.parquet as pq
|
| 17 |
-
import torch
|
| 18 |
-
|
| 19 |
-
from starse.conditioned_module import ConditionedStaticEmbedding
|
| 20 |
-
from starse.structured_tokenizer import batched_matching
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def sha256_file(path: Path) -> str:
|
| 24 |
-
digest = hashlib.sha256()
|
| 25 |
-
with path.open("rb") as handle:
|
| 26 |
-
for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
| 27 |
-
digest.update(chunk)
|
| 28 |
-
return digest.hexdigest()
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def canonical_json_sha256(payload: object) -> str:
|
| 32 |
-
encoded = json.dumps(
|
| 33 |
-
payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
| 34 |
-
).encode("utf-8")
|
| 35 |
-
return hashlib.sha256(encoded).hexdigest()
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def _is_hdd(path: Path) -> bool:
|
| 39 |
-
resolved = path.expanduser().resolve()
|
| 40 |
-
return resolved == Path("/hdd") or str(resolved).startswith("/hdd/")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def _is_benchmark_or_evaluation(path: Path) -> bool:
|
| 44 |
-
forbidden = {"eval", "evaluation", "validation", "benchmark", "mteb"}
|
| 45 |
-
return any(part.casefold() in forbidden for part in path.resolve().parts)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def _resolve_file(
|
| 49 |
-
record: Mapping[str, object], source: str, roots: Mapping[str, Path]
|
| 50 |
-
) -> Path:
|
| 51 |
-
direct = Path(str(record.get("path", ""))).expanduser()
|
| 52 |
-
if direct.is_file():
|
| 53 |
-
resolved = direct.resolve()
|
| 54 |
-
if _is_benchmark_or_evaluation(resolved):
|
| 55 |
-
raise ValueError(f"benchmark/evaluation source is forbidden: {resolved}")
|
| 56 |
-
return resolved
|
| 57 |
-
if source not in roots:
|
| 58 |
-
raise FileNotFoundError(direct)
|
| 59 |
-
resolved = (roots[source] / str(record.get("relative_path", ""))).resolve()
|
| 60 |
-
if not resolved.is_file():
|
| 61 |
-
raise FileNotFoundError(resolved)
|
| 62 |
-
if _is_benchmark_or_evaluation(resolved):
|
| 63 |
-
raise ValueError(f"benchmark/evaluation source is forbidden: {resolved}")
|
| 64 |
-
return resolved
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def _atomic_json(path: Path, payload: dict[str, object]) -> None:
|
| 68 |
-
if path.exists():
|
| 69 |
-
raise FileExistsError(path)
|
| 70 |
-
path.parent.mkdir(parents=True, exist_ok=True)
|
| 71 |
-
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 72 |
-
if temporary.exists():
|
| 73 |
-
raise FileExistsError(temporary)
|
| 74 |
-
data = json.dumps(
|
| 75 |
-
payload, ensure_ascii=False, indent=2, sort_keys=True
|
| 76 |
-
).encode("utf-8") + b"\n"
|
| 77 |
-
with temporary.open("xb") as handle:
|
| 78 |
-
handle.write(data)
|
| 79 |
-
handle.flush()
|
| 80 |
-
os.fsync(handle.fileno())
|
| 81 |
-
os.replace(temporary, path)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def build_probe_inventory(
|
| 85 |
-
corpus_manifest: Path,
|
| 86 |
-
roots: Mapping[str, Path],
|
| 87 |
-
output: Path,
|
| 88 |
-
*,
|
| 89 |
-
sides_per_group: int = 64,
|
| 90 |
-
holdout_row_groups: int = 0,
|
| 91 |
-
) -> dict[str, object]:
|
| 92 |
-
"""Select stable train-row coordinates without copying their text."""
|
| 93 |
-
|
| 94 |
-
if sides_per_group <= 0:
|
| 95 |
-
raise ValueError("sides_per_group must be positive")
|
| 96 |
-
if holdout_row_groups < 0:
|
| 97 |
-
raise ValueError("holdout_row_groups must be non-negative")
|
| 98 |
-
corpus_manifest = Path(corpus_manifest).expanduser().resolve()
|
| 99 |
-
output = Path(output).expanduser().resolve()
|
| 100 |
-
if output.exists():
|
| 101 |
-
raise FileExistsError(output)
|
| 102 |
-
if _is_hdd(output):
|
| 103 |
-
raise ValueError("vocabulary probe output may not use /hdd")
|
| 104 |
-
resolved_roots = {
|
| 105 |
-
str(name): Path(path).expanduser().resolve()
|
| 106 |
-
for name, path in sorted(roots.items())
|
| 107 |
-
}
|
| 108 |
-
if not resolved_roots or any(_is_hdd(path) for path in resolved_roots.values()):
|
| 109 |
-
raise ValueError("vocabulary probe roots must be non-empty and avoid /hdd")
|
| 110 |
-
manifest = json.loads(corpus_manifest.read_text(encoding="utf-8"))
|
| 111 |
-
if manifest.get("protocol") != "multilingual-sota-corpus-manifest-v1":
|
| 112 |
-
raise ValueError("unknown vocabulary probe corpus manifest")
|
| 113 |
-
groups = manifest.get("groups")
|
| 114 |
-
if not isinstance(groups, list) or not groups:
|
| 115 |
-
raise ValueError("vocabulary probe corpus manifest has no groups")
|
| 116 |
-
|
| 117 |
-
records: list[dict[str, object]] = []
|
| 118 |
-
selected_files: dict[Path, dict[str, object]] = {}
|
| 119 |
-
for raw_group in groups:
|
| 120 |
-
if not isinstance(raw_group, dict) or not bool(raw_group.get("trainable")):
|
| 121 |
-
continue
|
| 122 |
-
source = str(raw_group["source"])
|
| 123 |
-
group_language = str(raw_group["language"])
|
| 124 |
-
columns = (
|
| 125 |
-
(str(raw_group["anchor_column"]), str(raw_group.get("anchor_language") or group_language)),
|
| 126 |
-
(str(raw_group["positive_column"]), group_language),
|
| 127 |
-
)
|
| 128 |
-
files = raw_group.get("files")
|
| 129 |
-
if not isinstance(files, list) or not files:
|
| 130 |
-
raise ValueError("trainable vocabulary probe group has no files")
|
| 131 |
-
selected_for_group = 0
|
| 132 |
-
for raw_file in files:
|
| 133 |
-
if not isinstance(raw_file, dict):
|
| 134 |
-
raise ValueError("vocabulary probe file record must be an object")
|
| 135 |
-
path = _resolve_file(raw_file, source, resolved_roots)
|
| 136 |
-
parquet = pq.ParquetFile(path)
|
| 137 |
-
usable_row_groups = max(
|
| 138 |
-
0, parquet.metadata.num_row_groups - holdout_row_groups
|
| 139 |
-
)
|
| 140 |
-
requested_columns = list(dict.fromkeys(column for column, _ in columns))
|
| 141 |
-
for row_group in range(usable_row_groups):
|
| 142 |
-
batch = parquet.read_row_group(row_group, columns=requested_columns)
|
| 143 |
-
values = batch.to_pydict()
|
| 144 |
-
for row_index in range(batch.num_rows):
|
| 145 |
-
for column, actual_language in columns:
|
| 146 |
-
text = values[column][row_index]
|
| 147 |
-
if not isinstance(text, str) or not text.strip():
|
| 148 |
-
continue
|
| 149 |
-
file_identity = selected_files.get(path)
|
| 150 |
-
if file_identity is None:
|
| 151 |
-
file_identity = {
|
| 152 |
-
"source": source,
|
| 153 |
-
"path": str(path),
|
| 154 |
-
"relative_path": str(raw_file.get("relative_path", "")),
|
| 155 |
-
"manifest_metadata_sha256": str(
|
| 156 |
-
raw_file.get("metadata_sha256", "")
|
| 157 |
-
),
|
| 158 |
-
"observed_bytes": path.stat().st_size,
|
| 159 |
-
}
|
| 160 |
-
file_identity["file_identity_sha256"] = (
|
| 161 |
-
canonical_json_sha256(file_identity)
|
| 162 |
-
)
|
| 163 |
-
selected_files[path] = file_identity
|
| 164 |
-
records.append(
|
| 165 |
-
{
|
| 166 |
-
"source": source,
|
| 167 |
-
"group_language": group_language,
|
| 168 |
-
"actual_language": actual_language,
|
| 169 |
-
"path": str(path),
|
| 170 |
-
"relative_path": str(raw_file.get("relative_path", "")),
|
| 171 |
-
"file_identity_sha256": file_identity[
|
| 172 |
-
"file_identity_sha256"
|
| 173 |
-
],
|
| 174 |
-
"text_sha256": hashlib.sha256(
|
| 175 |
-
text.encode("utf-8")
|
| 176 |
-
).hexdigest(),
|
| 177 |
-
"row_group": row_group,
|
| 178 |
-
"row": row_index,
|
| 179 |
-
"column": column,
|
| 180 |
-
}
|
| 181 |
-
)
|
| 182 |
-
selected_for_group += 1
|
| 183 |
-
if selected_for_group >= sides_per_group:
|
| 184 |
-
break
|
| 185 |
-
if selected_for_group >= sides_per_group:
|
| 186 |
-
break
|
| 187 |
-
if selected_for_group >= sides_per_group:
|
| 188 |
-
break
|
| 189 |
-
if selected_for_group >= sides_per_group:
|
| 190 |
-
break
|
| 191 |
-
if selected_for_group == 0:
|
| 192 |
-
raise ValueError(
|
| 193 |
-
f"trainable vocabulary probe group is empty: {source}/{group_language}"
|
| 194 |
-
)
|
| 195 |
-
|
| 196 |
-
if not records:
|
| 197 |
-
raise ValueError("vocabulary probe inventory is empty")
|
| 198 |
-
report: dict[str, object] = {
|
| 199 |
-
"protocol": "vocabulary-probe-inventory-v1",
|
| 200 |
-
"corpus_manifest": str(corpus_manifest),
|
| 201 |
-
"corpus_manifest_sha256": sha256_file(corpus_manifest),
|
| 202 |
-
"roots": {name: str(path) for name, path in resolved_roots.items()},
|
| 203 |
-
"roots_sha256": canonical_json_sha256(
|
| 204 |
-
{name: str(path) for name, path in resolved_roots.items()}
|
| 205 |
-
),
|
| 206 |
-
"sides_per_group": int(sides_per_group),
|
| 207 |
-
"holdout_row_groups": int(holdout_row_groups),
|
| 208 |
-
"selected_files": list(selected_files.values()),
|
| 209 |
-
"records": records,
|
| 210 |
-
}
|
| 211 |
-
report["inventory_sha256"] = canonical_json_sha256(report)
|
| 212 |
-
_atomic_json(output, report)
|
| 213 |
-
return report
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
def _quantiles(values: list[float]) -> dict[str, float | None]:
|
| 217 |
-
if not values:
|
| 218 |
-
return {name: None for name in ("minimum", "q25", "median", "q75", "maximum")}
|
| 219 |
-
ordered = sorted(float(value) for value in values)
|
| 220 |
-
|
| 221 |
-
def at(fraction: float) -> float:
|
| 222 |
-
position = fraction * (len(ordered) - 1)
|
| 223 |
-
lower = math.floor(position)
|
| 224 |
-
upper = math.ceil(position)
|
| 225 |
-
if lower == upper:
|
| 226 |
-
return ordered[lower]
|
| 227 |
-
weight = position - lower
|
| 228 |
-
return ordered[lower] * (1.0 - weight) + ordered[upper] * weight
|
| 229 |
-
|
| 230 |
-
return {
|
| 231 |
-
"minimum": ordered[0],
|
| 232 |
-
"q25": at(0.25),
|
| 233 |
-
"median": at(0.5),
|
| 234 |
-
"q75": at(0.75),
|
| 235 |
-
"maximum": ordered[-1],
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
class _ProbeAggregate:
|
| 240 |
-
"""Exact bounded-probe statistics for one source/language slice."""
|
| 241 |
-
|
| 242 |
-
def __init__(self, arm: str) -> None:
|
| 243 |
-
self.arm = arm
|
| 244 |
-
self.base_token_occurrences = 0
|
| 245 |
-
self.base_token_ids: set[int] = set()
|
| 246 |
-
self.pair_occurrences = 0
|
| 247 |
-
self.pair_types: set[int] = set()
|
| 248 |
-
self.pair_languages: set[tuple[int, int]] = set()
|
| 249 |
-
self.positive_pair_languages: set[tuple[int, int]] = set()
|
| 250 |
-
self.positive_pair_types: set[int] = set()
|
| 251 |
-
self.bucket_types: dict[int, set[int]] = defaultdict(set)
|
| 252 |
-
self.scores: list[float] = []
|
| 253 |
-
self.language_biases: list[float] = []
|
| 254 |
-
self.probabilities: list[float] = []
|
| 255 |
-
self.expected_merge_mass = 0.0
|
| 256 |
-
self.hard_selected_occurrences = 0
|
| 257 |
-
self.hard_selected_types: set[int] = set()
|
| 258 |
-
self.exact_hit_occurrences = 0
|
| 259 |
-
|
| 260 |
-
def add_sentence(
|
| 261 |
-
self,
|
| 262 |
-
*,
|
| 263 |
-
ids: list[int],
|
| 264 |
-
language_id: int,
|
| 265 |
-
pairs: list[int],
|
| 266 |
-
buckets: list[int],
|
| 267 |
-
scores: list[float],
|
| 268 |
-
language_bias: float,
|
| 269 |
-
probabilities: list[float],
|
| 270 |
-
selected: list[bool],
|
| 271 |
-
exact_hits: list[bool] | None = None,
|
| 272 |
-
) -> None:
|
| 273 |
-
self.base_token_occurrences += len(ids)
|
| 274 |
-
self.base_token_ids.update(ids)
|
| 275 |
-
self.pair_occurrences += len(pairs)
|
| 276 |
-
self.language_biases.extend([float(language_bias)] * len(pairs))
|
| 277 |
-
hits = exact_hits if exact_hits is not None else [False] * len(pairs)
|
| 278 |
-
for key, bucket, score, probability, is_selected, exact_hit in zip(
|
| 279 |
-
pairs, buckets, scores, probabilities, selected, hits, strict=True
|
| 280 |
-
):
|
| 281 |
-
identity = (int(key), int(language_id))
|
| 282 |
-
self.pair_types.add(int(key))
|
| 283 |
-
self.pair_languages.add(identity)
|
| 284 |
-
self.bucket_types[int(bucket)].add(int(key))
|
| 285 |
-
self.scores.append(float(score))
|
| 286 |
-
self.probabilities.append(float(probability))
|
| 287 |
-
self.expected_merge_mass += float(probability)
|
| 288 |
-
if score > 0.0:
|
| 289 |
-
self.positive_pair_languages.add(identity)
|
| 290 |
-
self.positive_pair_types.add(int(key))
|
| 291 |
-
if is_selected:
|
| 292 |
-
self.hard_selected_occurrences += 1
|
| 293 |
-
self.hard_selected_types.add(int(key))
|
| 294 |
-
if exact_hit:
|
| 295 |
-
self.exact_hit_occurrences += 1
|
| 296 |
-
|
| 297 |
-
def summary(self) -> dict[str, object]:
|
| 298 |
-
bucket_loads = [len(keys) for keys in self.bucket_types.values()]
|
| 299 |
-
collisions = sum(max(0, count - 1) for count in bucket_loads)
|
| 300 |
-
result: dict[str, object] = {
|
| 301 |
-
"base_token_occurrences": self.base_token_occurrences,
|
| 302 |
-
"unique_base_token_ids": len(self.base_token_ids),
|
| 303 |
-
"eligible_pair_occurrences": self.pair_occurrences,
|
| 304 |
-
"unique_exact_pair_types": len(self.pair_types),
|
| 305 |
-
"unique_pair_language_records": len(self.pair_languages),
|
| 306 |
-
"unique_hash_buckets": len(self.bucket_types),
|
| 307 |
-
"hash_collision_count": collisions,
|
| 308 |
-
"pair_types_per_bucket": _quantiles(
|
| 309 |
-
[float(value) for value in bucket_loads]
|
| 310 |
-
),
|
| 311 |
-
"positive_pair_language_records": len(self.positive_pair_languages),
|
| 312 |
-
"positive_pair_types_any_language": len(self.positive_pair_types),
|
| 313 |
-
"score": _quantiles(self.scores),
|
| 314 |
-
"language_bias": _quantiles(self.language_biases),
|
| 315 |
-
}
|
| 316 |
-
if self.arm == "online":
|
| 317 |
-
emitted = self.base_token_occurrences - self.hard_selected_occurrences
|
| 318 |
-
result.update(
|
| 319 |
-
{
|
| 320 |
-
"hard_selected_occurrences": self.hard_selected_occurrences,
|
| 321 |
-
"hard_selected_pair_types": len(self.hard_selected_types),
|
| 322 |
-
"hard_merge_rate": (
|
| 323 |
-
self.hard_selected_occurrences / self.pair_occurrences
|
| 324 |
-
if self.pair_occurrences
|
| 325 |
-
else 0.0
|
| 326 |
-
),
|
| 327 |
-
"token_compression_ratio": (
|
| 328 |
-
emitted / self.base_token_occurrences
|
| 329 |
-
if self.base_token_occurrences
|
| 330 |
-
else 1.0
|
| 331 |
-
),
|
| 332 |
-
"mean_edge_marginal": (
|
| 333 |
-
self.expected_merge_mass / self.pair_occurrences
|
| 334 |
-
if self.pair_occurrences
|
| 335 |
-
else 0.0
|
| 336 |
-
),
|
| 337 |
-
}
|
| 338 |
-
)
|
| 339 |
-
else:
|
| 340 |
-
expected_tokens = self.base_token_occurrences - self.expected_merge_mass
|
| 341 |
-
result.update(
|
| 342 |
-
{
|
| 343 |
-
"merge_probability": _quantiles(self.probabilities),
|
| 344 |
-
"mean_merge_probability": (
|
| 345 |
-
self.expected_merge_mass / self.pair_occurrences
|
| 346 |
-
if self.pair_occurrences
|
| 347 |
-
else 0.0
|
| 348 |
-
),
|
| 349 |
-
"expected_merge_mass": self.expected_merge_mass,
|
| 350 |
-
"exact_hit_occurrences": self.exact_hit_occurrences,
|
| 351 |
-
"exact_hit_rate": (
|
| 352 |
-
self.exact_hit_occurrences / self.pair_occurrences
|
| 353 |
-
if self.pair_occurrences
|
| 354 |
-
else 0.0
|
| 355 |
-
),
|
| 356 |
-
"expected_compression_ratio": (
|
| 357 |
-
expected_tokens / self.base_token_occurrences
|
| 358 |
-
if self.base_token_occurrences
|
| 359 |
-
else 1.0
|
| 360 |
-
),
|
| 361 |
-
}
|
| 362 |
-
)
|
| 363 |
-
return result
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
def _load_inventory(path: Path) -> dict[str, object]:
|
| 367 |
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
| 368 |
-
if payload.get("protocol") != "vocabulary-probe-inventory-v1":
|
| 369 |
-
raise ValueError("unknown vocabulary probe inventory protocol")
|
| 370 |
-
claimed = str(payload.get("inventory_sha256", ""))
|
| 371 |
-
unsigned = dict(payload)
|
| 372 |
-
unsigned.pop("inventory_sha256", None)
|
| 373 |
-
if claimed != canonical_json_sha256(unsigned):
|
| 374 |
-
raise ValueError("vocabulary probe inventory identity mismatch")
|
| 375 |
-
manifest_path = Path(str(payload.get("corpus_manifest", ""))).resolve()
|
| 376 |
-
if not manifest_path.is_file() or sha256_file(manifest_path) != payload.get(
|
| 377 |
-
"corpus_manifest_sha256"
|
| 378 |
-
):
|
| 379 |
-
raise ValueError("vocabulary probe corpus manifest hash mismatch")
|
| 380 |
-
roots = payload.get("roots")
|
| 381 |
-
if not isinstance(roots, dict) or canonical_json_sha256(roots) != payload.get(
|
| 382 |
-
"roots_sha256"
|
| 383 |
-
):
|
| 384 |
-
raise ValueError("vocabulary probe roots identity mismatch")
|
| 385 |
-
return payload
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
def _audit_checkpoint(module: ConditionedStaticEmbedding) -> dict[str, object]:
|
| 389 |
-
state = module.state_dict()
|
| 390 |
-
for name, tensor in state.items():
|
| 391 |
-
if not bool(torch.isfinite(tensor).all()):
|
| 392 |
-
raise FloatingPointError(f"nonfinite checkpoint tensor: {name}")
|
| 393 |
-
return {"all_finite": True, "tensor_count": len(state)}
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
def _pack_bits(values: list[bool]) -> str:
|
| 397 |
-
packed = bytearray((len(values) + 7) // 8)
|
| 398 |
-
for index, value in enumerate(values):
|
| 399 |
-
if value:
|
| 400 |
-
packed[index // 8] |= 1 << (index % 8)
|
| 401 |
-
return base64.b64encode(zlib.compress(bytes(packed), level=9)).decode("ascii")
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
def _unpack_bits(encoded: str, count: int) -> list[bool]:
|
| 405 |
-
try:
|
| 406 |
-
packed = zlib.decompress(base64.b64decode(encoded, validate=True))
|
| 407 |
-
except Exception as error:
|
| 408 |
-
raise ValueError("invalid vocabulary decision bitset") from error
|
| 409 |
-
if len(packed) != (count + 7) // 8:
|
| 410 |
-
raise ValueError("vocabulary decision bitset length mismatch")
|
| 411 |
-
if count % 8 and packed and packed[-1] >> (count % 8):
|
| 412 |
-
raise ValueError("vocabulary decision bitset has nonzero padding")
|
| 413 |
-
return [bool(packed[index // 8] & (1 << (index % 8))) for index in range(count)]
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
def _pack_identities(identities: list[tuple[int, int]]) -> str:
|
| 417 |
-
raw = b"".join(struct.pack("<QI", key, language) for key, language in identities)
|
| 418 |
-
return base64.b64encode(zlib.compress(raw, level=9)).decode("ascii")
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
def _load_probe_texts(
|
| 422 |
-
inventory: Mapping[str, object],
|
| 423 |
-
) -> list[tuple[Mapping[str, object], str]]:
|
| 424 |
-
selected_files = inventory.get("selected_files")
|
| 425 |
-
records = inventory.get("records")
|
| 426 |
-
if not isinstance(selected_files, list) or not isinstance(records, list):
|
| 427 |
-
raise ValueError("vocabulary probe inventory payload is malformed")
|
| 428 |
-
expected_identities: dict[str, str] = {}
|
| 429 |
-
for raw in selected_files:
|
| 430 |
-
if not isinstance(raw, dict):
|
| 431 |
-
raise ValueError("vocabulary probe selected file is malformed")
|
| 432 |
-
path = str(Path(str(raw.get("path", ""))).resolve())
|
| 433 |
-
if _is_benchmark_or_evaluation(Path(path)):
|
| 434 |
-
raise ValueError(f"benchmark/evaluation source is forbidden: {path}")
|
| 435 |
-
file_path = Path(path)
|
| 436 |
-
if not file_path.is_file():
|
| 437 |
-
raise FileNotFoundError(file_path)
|
| 438 |
-
observed_bytes = int(raw.get("observed_bytes", -1))
|
| 439 |
-
if file_path.stat().st_size != observed_bytes:
|
| 440 |
-
raise ValueError(f"source file size mismatch: {path}")
|
| 441 |
-
unsigned = dict(raw)
|
| 442 |
-
claimed_identity = str(unsigned.pop("file_identity_sha256", ""))
|
| 443 |
-
if claimed_identity != canonical_json_sha256(unsigned):
|
| 444 |
-
raise ValueError(f"source file identity mismatch: {path}")
|
| 445 |
-
expected_identities[path] = claimed_identity
|
| 446 |
-
|
| 447 |
-
row_group_cache: dict[tuple[str, int, str], list[object]] = {}
|
| 448 |
-
loaded: list[tuple[Mapping[str, object], str]] = []
|
| 449 |
-
for raw in records:
|
| 450 |
-
if not isinstance(raw, dict):
|
| 451 |
-
raise ValueError("vocabulary probe record is malformed")
|
| 452 |
-
path = str(Path(str(raw.get("path", ""))).resolve())
|
| 453 |
-
if expected_identities.get(path) != raw.get("file_identity_sha256"):
|
| 454 |
-
raise ValueError(f"source file identity binding mismatch: {path}")
|
| 455 |
-
row_group = int(raw["row_group"])
|
| 456 |
-
column = str(raw["column"])
|
| 457 |
-
cache_key = (path, row_group, column)
|
| 458 |
-
if cache_key not in row_group_cache:
|
| 459 |
-
table = pq.ParquetFile(path).read_row_group(row_group, columns=[column])
|
| 460 |
-
row_group_cache[cache_key] = table[column].to_pylist()
|
| 461 |
-
row = int(raw["row"])
|
| 462 |
-
values = row_group_cache[cache_key]
|
| 463 |
-
if row < 0 or row >= len(values) or not isinstance(values[row], str):
|
| 464 |
-
raise ValueError("vocabulary probe row coordinate is invalid")
|
| 465 |
-
text = str(values[row])
|
| 466 |
-
if hashlib.sha256(text.encode("utf-8")).hexdigest() != raw.get(
|
| 467 |
-
"text_sha256"
|
| 468 |
-
):
|
| 469 |
-
raise ValueError("vocabulary probe selected text hash mismatch")
|
| 470 |
-
loaded.append((raw, text))
|
| 471 |
-
return loaded
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
def evaluate_probe(
|
| 475 |
-
checkpoint: Path,
|
| 476 |
-
inventory: Path,
|
| 477 |
-
output: Path,
|
| 478 |
-
*,
|
| 479 |
-
previous_report: Path | None = None,
|
| 480 |
-
) -> dict[str, object]:
|
| 481 |
-
"""Evaluate exact vocabulary decisions on a frozen training-only sample."""
|
| 482 |
-
|
| 483 |
-
checkpoint = Path(checkpoint).expanduser().resolve()
|
| 484 |
-
inventory = Path(inventory).expanduser().resolve()
|
| 485 |
-
output = Path(output).expanduser().resolve()
|
| 486 |
-
if output.exists():
|
| 487 |
-
raise FileExistsError(output)
|
| 488 |
-
if _is_hdd(output):
|
| 489 |
-
raise ValueError("vocabulary probe output may not use /hdd")
|
| 490 |
-
model_path = checkpoint / "model.safetensors"
|
| 491 |
-
run_config_path = checkpoint / "run_config.json"
|
| 492 |
-
if not model_path.is_file() or not run_config_path.is_file():
|
| 493 |
-
raise FileNotFoundError("checkpoint model or run_config.json is missing")
|
| 494 |
-
|
| 495 |
-
frozen = _load_inventory(inventory)
|
| 496 |
-
module = ConditionedStaticEmbedding.load(str(checkpoint)).cpu().eval()
|
| 497 |
-
if module.mode == "collapse_dynamic":
|
| 498 |
-
kernel = "q15"
|
| 499 |
-
table = module.collapse
|
| 500 |
-
elif module.mode == "collapse_online":
|
| 501 |
-
kernel = "online"
|
| 502 |
-
table = module.online_collapse
|
| 503 |
-
else:
|
| 504 |
-
raise ValueError(f"unsupported vocabulary probe model mode: {module.mode}")
|
| 505 |
-
if table is None:
|
| 506 |
-
raise ValueError("checkpoint has no active vocabulary table")
|
| 507 |
-
tensor_audit = _audit_checkpoint(module)
|
| 508 |
-
|
| 509 |
-
run_config = json.loads(run_config_path.read_text(encoding="utf-8"))
|
| 510 |
-
training = run_config.get("training")
|
| 511 |
-
telemetry = run_config.get("vocabulary_telemetry")
|
| 512 |
-
growth = run_config.get("vocabulary_growth")
|
| 513 |
-
arm = kernel
|
| 514 |
-
if growth:
|
| 515 |
-
if (
|
| 516 |
-
kernel != "q15"
|
| 517 |
-
or not isinstance(growth, dict)
|
| 518 |
-
or growth.get("protocol") != "adaptive-exact-growth-v1"
|
| 519 |
-
or growth.get("policy") not in {"stable", "l0"}
|
| 520 |
-
):
|
| 521 |
-
raise ValueError("checkpoint adaptive-growth identity is invalid")
|
| 522 |
-
arm = str(growth["policy"])
|
| 523 |
-
if not isinstance(training, dict) or int(training.get("max_tokens", 0)) != 128:
|
| 524 |
-
raise ValueError("vocabulary probe requires frozen max_tokens=128")
|
| 525 |
-
if (
|
| 526 |
-
not isinstance(telemetry, dict)
|
| 527 |
-
or telemetry.get("protocol") != "vocabulary-telemetry-v1"
|
| 528 |
-
or telemetry.get("arm") != kernel
|
| 529 |
-
):
|
| 530 |
-
raise ValueError("checkpoint vocabulary telemetry identity mismatch")
|
| 531 |
-
module.tokenizer.enable_truncation(max_length=128)
|
| 532 |
-
language_index = {name: index for index, name in enumerate(module.languages)}
|
| 533 |
-
if len(language_index) != len(module.languages):
|
| 534 |
-
raise ValueError("checkpoint languages are not unique")
|
| 535 |
-
|
| 536 |
-
loaded = _load_probe_texts(frozen)
|
| 537 |
-
overall = _ProbeAggregate(kernel)
|
| 538 |
-
per_source: dict[str, _ProbeAggregate] = {}
|
| 539 |
-
per_language: dict[str, _ProbeAggregate] = {}
|
| 540 |
-
decisions: dict[tuple[int, int], tuple[float, bool]] = {}
|
| 541 |
-
key_base = int(table.pair_key_base)
|
| 542 |
-
if key_base <= 0:
|
| 543 |
-
raise ValueError("vocabulary table has no exact pair-key base")
|
| 544 |
-
|
| 545 |
-
with torch.inference_mode():
|
| 546 |
-
for record, text in loaded:
|
| 547 |
-
language = str(record["actual_language"])
|
| 548 |
-
if language not in language_index:
|
| 549 |
-
raise ValueError(f"probe language absent from checkpoint: {language}")
|
| 550 |
-
language_id = language_index[language]
|
| 551 |
-
ids = module.tokenizer.encode(text, add_special_tokens=False).ids[:128]
|
| 552 |
-
content = torch.as_tensor(ids, dtype=torch.long)
|
| 553 |
-
left = content[:-1]
|
| 554 |
-
right = content[1:]
|
| 555 |
-
pair_keys = (left * key_base + right).tolist()
|
| 556 |
-
language_ids = torch.full_like(left, language_id)
|
| 557 |
-
if kernel == "q15":
|
| 558 |
-
if table.pair_keys.numel():
|
| 559 |
-
found, indices = table._explicit_lookup(left * key_base + right)
|
| 560 |
-
if not int(getattr(table, "residual_buckets", 0)) and not bool(found.all()):
|
| 561 |
-
raise ValueError("Q15 probe requires an unpromoted or complete table")
|
| 562 |
-
if int(getattr(table, "residual_buckets", 0)):
|
| 563 |
-
residual = table._hash_residual(left[~found], right[~found])
|
| 564 |
-
score_tensor = torch.empty(
|
| 565 |
-
left.shape, dtype=table.score.dtype, device=left.device
|
| 566 |
-
)
|
| 567 |
-
buckets_tensor = torch.empty_like(left)
|
| 568 |
-
score_tensor[found] = (
|
| 569 |
-
table.score[indices[found]]
|
| 570 |
-
+ table.language_bias[language_ids[found]]
|
| 571 |
-
)
|
| 572 |
-
score_tensor[~found] = (
|
| 573 |
-
table.residual_score[residual]
|
| 574 |
-
+ table.language_bias[language_ids[~found]]
|
| 575 |
-
)
|
| 576 |
-
buckets_tensor[found] = indices[found]
|
| 577 |
-
buckets_tensor[~found] = residual + int(table.buckets)
|
| 578 |
-
buckets = buckets_tensor.tolist()
|
| 579 |
-
exact_hits = found.tolist()
|
| 580 |
-
else:
|
| 581 |
-
indices = indices[found]
|
| 582 |
-
score_tensor = table.score.index_select(0, indices) + table.language_bias[
|
| 583 |
-
language_ids
|
| 584 |
-
]
|
| 585 |
-
buckets = indices.tolist()
|
| 586 |
-
exact_hits = found.tolist()
|
| 587 |
-
else:
|
| 588 |
-
indices = table._hash(left, right)
|
| 589 |
-
score_tensor = table.score.index_select(0, indices) + table.language_bias[
|
| 590 |
-
language_ids
|
| 591 |
-
]
|
| 592 |
-
buckets = indices.tolist()
|
| 593 |
-
exact_hits = [False] * len(pair_keys)
|
| 594 |
-
probability_tensor = torch.sigmoid(score_tensor.float())
|
| 595 |
-
selected_tensor = torch.zeros_like(score_tensor, dtype=torch.bool)
|
| 596 |
-
else:
|
| 597 |
-
score_tensor = table.edge_scores(
|
| 598 |
-
left, right, language_ids, module.embedding.weight
|
| 599 |
-
)
|
| 600 |
-
buckets = table._hash(left, right).tolist()
|
| 601 |
-
if score_tensor.numel():
|
| 602 |
-
matching = batched_matching(
|
| 603 |
-
score_tensor.unsqueeze(0),
|
| 604 |
-
torch.ones_like(score_tensor, dtype=torch.bool).unsqueeze(0),
|
| 605 |
-
)
|
| 606 |
-
probability_tensor = matching.marginals.squeeze(0)
|
| 607 |
-
selected_tensor = matching.map_edges.squeeze(0)
|
| 608 |
-
else:
|
| 609 |
-
probability_tensor = score_tensor.float()
|
| 610 |
-
selected_tensor = torch.zeros_like(score_tensor, dtype=torch.bool)
|
| 611 |
-
|
| 612 |
-
scores = score_tensor.float().tolist()
|
| 613 |
-
probabilities = probability_tensor.float().tolist()
|
| 614 |
-
selected = selected_tensor.tolist()
|
| 615 |
-
bias = float(table.language_bias[language_id])
|
| 616 |
-
targets = (
|
| 617 |
-
overall,
|
| 618 |
-
per_source.setdefault(str(record["source"]), _ProbeAggregate(kernel)),
|
| 619 |
-
per_language.setdefault(language, _ProbeAggregate(kernel)),
|
| 620 |
-
)
|
| 621 |
-
for aggregate in targets:
|
| 622 |
-
aggregate.add_sentence(
|
| 623 |
-
ids=ids,
|
| 624 |
-
language_id=language_id,
|
| 625 |
-
pairs=pair_keys,
|
| 626 |
-
buckets=buckets,
|
| 627 |
-
scores=scores,
|
| 628 |
-
language_bias=bias,
|
| 629 |
-
probabilities=probabilities,
|
| 630 |
-
selected=selected,
|
| 631 |
-
exact_hits=exact_hits if kernel == "q15" else None,
|
| 632 |
-
)
|
| 633 |
-
for key, score, is_selected in zip(
|
| 634 |
-
pair_keys, scores, selected, strict=True
|
| 635 |
-
):
|
| 636 |
-
identity = (int(key), int(language_id))
|
| 637 |
-
value = (float(score), bool(is_selected))
|
| 638 |
-
previous = decisions.setdefault(identity, value)
|
| 639 |
-
if previous[0] != value[0]:
|
| 640 |
-
raise ValueError("checkpoint score varies for one pair-language identity")
|
| 641 |
-
# A hard matching decision is contextual: the same edge can be
|
| 642 |
-
# blocked by a selected neighbour in one sentence and selected
|
| 643 |
-
# in another. Its pair-language decision is therefore the
|
| 644 |
-
# deterministic union over frozen probe occurrences.
|
| 645 |
-
decisions[identity] = (previous[0], previous[1] or value[1])
|
| 646 |
-
|
| 647 |
-
identities = sorted(decisions)
|
| 648 |
-
positive = [decisions[identity][0] > 0.0 for identity in identities]
|
| 649 |
-
selected = [decisions[identity][1] for identity in identities]
|
| 650 |
-
decision_universe: dict[str, object] = {
|
| 651 |
-
"encoding": "zlib-base64-le-u64-u32-and-bitsets-v1",
|
| 652 |
-
"records": len(identities),
|
| 653 |
-
"identities_b64": _pack_identities(identities),
|
| 654 |
-
"positive_bits_b64": _pack_bits(positive),
|
| 655 |
-
"map_selected_bits_b64": _pack_bits(selected) if kernel == "online" else None,
|
| 656 |
-
}
|
| 657 |
-
|
| 658 |
-
flips: dict[str, int | None] | None = None
|
| 659 |
-
if previous_report is not None:
|
| 660 |
-
previous_path = Path(previous_report).expanduser().resolve()
|
| 661 |
-
previous = json.loads(previous_path.read_text(encoding="utf-8"))
|
| 662 |
-
previous_universe = previous.get("decision_universe")
|
| 663 |
-
if (
|
| 664 |
-
previous.get("protocol") != "vocabulary-checkpoint-probe-v1"
|
| 665 |
-
or previous.get("arm") != arm
|
| 666 |
-
or previous.get("probe_identity_sha256") != frozen["inventory_sha256"]
|
| 667 |
-
or not isinstance(previous_universe, dict)
|
| 668 |
-
or previous_universe.get("records") != len(identities)
|
| 669 |
-
or previous_universe.get("identities_b64")
|
| 670 |
-
!= decision_universe["identities_b64"]
|
| 671 |
-
):
|
| 672 |
-
raise ValueError("previous vocabulary probe decision universe differs")
|
| 673 |
-
prior_positive = _unpack_bits(
|
| 674 |
-
str(previous_universe.get("positive_bits_b64", "")), len(identities)
|
| 675 |
-
)
|
| 676 |
-
positive_flips = sum(a != b for a, b in zip(prior_positive, positive, strict=True))
|
| 677 |
-
selected_flips: int | None = None
|
| 678 |
-
if kernel == "online":
|
| 679 |
-
prior_selected = _unpack_bits(
|
| 680 |
-
str(previous_universe.get("map_selected_bits_b64", "")),
|
| 681 |
-
len(identities),
|
| 682 |
-
)
|
| 683 |
-
selected_flips = sum(
|
| 684 |
-
a != b for a, b in zip(prior_selected, selected, strict=True)
|
| 685 |
-
)
|
| 686 |
-
flips = {"positive": positive_flips, "map_selected": selected_flips}
|
| 687 |
-
|
| 688 |
-
report: dict[str, object] = {
|
| 689 |
-
"protocol": "vocabulary-checkpoint-probe-v1",
|
| 690 |
-
"arm": arm,
|
| 691 |
-
"kernel": kernel,
|
| 692 |
-
"checkpoint": str(checkpoint),
|
| 693 |
-
"checkpoint_model_sha256": sha256_file(model_path),
|
| 694 |
-
"run_config_sha256": sha256_file(run_config_path),
|
| 695 |
-
"probe_inventory": str(inventory),
|
| 696 |
-
"probe_inventory_sha256": sha256_file(inventory),
|
| 697 |
-
"probe_identity_sha256": frozen["inventory_sha256"],
|
| 698 |
-
"corpus_manifest": frozen["corpus_manifest"],
|
| 699 |
-
"corpus_manifest_sha256": frozen["corpus_manifest_sha256"],
|
| 700 |
-
"source_files": frozen["selected_files"],
|
| 701 |
-
"tensor_audit": tensor_audit,
|
| 702 |
-
"table_structure": {
|
| 703 |
-
"exact_rows": int(
|
| 704 |
-
getattr(table, "pair_keys", torch.empty(0, dtype=torch.long)).numel()
|
| 705 |
-
),
|
| 706 |
-
"residual_rows": int(getattr(table, "residual_buckets", 0)),
|
| 707 |
-
},
|
| 708 |
-
"overall": overall.summary(),
|
| 709 |
-
"per_source": {
|
| 710 |
-
name: aggregate.summary() for name, aggregate in sorted(per_source.items())
|
| 711 |
-
},
|
| 712 |
-
"per_language": {
|
| 713 |
-
name: aggregate.summary()
|
| 714 |
-
for name, aggregate in sorted(per_language.items())
|
| 715 |
-
},
|
| 716 |
-
"decision_universe": decision_universe,
|
| 717 |
-
"decision_flips": flips,
|
| 718 |
-
}
|
| 719 |
-
_atomic_json(output, report)
|
| 720 |
-
return report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
starse/vocabulary_telemetry.py
DELETED
|
@@ -1,627 +0,0 @@
|
|
| 1 |
-
"""Detached, bounded training-window vocabulary telemetry."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import json
|
| 6 |
-
import hashlib
|
| 7 |
-
import math
|
| 8 |
-
import os
|
| 9 |
-
from dataclasses import dataclass
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
import re
|
| 12 |
-
from typing import Any
|
| 13 |
-
|
| 14 |
-
import torch
|
| 15 |
-
from transformers import TrainerCallback
|
| 16 |
-
|
| 17 |
-
from starse.gradient_safety import GradientReport, stable_gradient_target
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
class VocabularyWindow:
|
| 21 |
-
"""Accumulate one logging window without retaining text or exact pair keys."""
|
| 22 |
-
|
| 23 |
-
def __init__(self) -> None:
|
| 24 |
-
self._phase: str | None = None
|
| 25 |
-
self.reset()
|
| 26 |
-
|
| 27 |
-
def begin_record(self) -> None:
|
| 28 |
-
self._begin_phase("record")
|
| 29 |
-
|
| 30 |
-
def begin_replay(self) -> None:
|
| 31 |
-
self._begin_phase("replay")
|
| 32 |
-
|
| 33 |
-
def _begin_phase(self, phase: str) -> None:
|
| 34 |
-
if self._phase is not None:
|
| 35 |
-
raise RuntimeError("vocabulary telemetry phase is already active")
|
| 36 |
-
self._phase = phase
|
| 37 |
-
|
| 38 |
-
def end_phase(self) -> None:
|
| 39 |
-
if self._phase is None:
|
| 40 |
-
raise RuntimeError("no telemetry phase is active")
|
| 41 |
-
self._phase = None
|
| 42 |
-
|
| 43 |
-
def abort_phase(self) -> None:
|
| 44 |
-
self._phase = None
|
| 45 |
-
|
| 46 |
-
def _should_record(self) -> bool:
|
| 47 |
-
if self._phase is None:
|
| 48 |
-
raise RuntimeError("no telemetry phase is active")
|
| 49 |
-
return self._phase == "record"
|
| 50 |
-
|
| 51 |
-
def _set_kind(self, kind: str) -> None:
|
| 52 |
-
if self._kind is None:
|
| 53 |
-
self._kind = kind
|
| 54 |
-
elif self._kind != kind:
|
| 55 |
-
raise RuntimeError("vocabulary telemetry window mixed model methods")
|
| 56 |
-
|
| 57 |
-
def observe_online(
|
| 58 |
-
self,
|
| 59 |
-
*,
|
| 60 |
-
base_tokens: int,
|
| 61 |
-
edge_scores: torch.Tensor,
|
| 62 |
-
marginals: torch.Tensor,
|
| 63 |
-
map_edges: torch.Tensor,
|
| 64 |
-
hash_indices: torch.Tensor,
|
| 65 |
-
edge_mask: torch.Tensor,
|
| 66 |
-
) -> None:
|
| 67 |
-
if not self._should_record():
|
| 68 |
-
return
|
| 69 |
-
self._set_kind("online")
|
| 70 |
-
if base_tokens < 0:
|
| 71 |
-
raise ValueError("base_tokens must be non-negative")
|
| 72 |
-
if not (
|
| 73 |
-
edge_scores.shape == marginals.shape == map_edges.shape == edge_mask.shape
|
| 74 |
-
and edge_scores.ndim == 2
|
| 75 |
-
):
|
| 76 |
-
raise ValueError("online edge telemetry tensors must have aligned rank-two shapes")
|
| 77 |
-
if map_edges.dtype != torch.bool or edge_mask.dtype != torch.bool:
|
| 78 |
-
raise TypeError("online MAP and mask telemetry tensors must be boolean")
|
| 79 |
-
valid_count = int(edge_mask.sum().item())
|
| 80 |
-
if hash_indices.ndim != 1 or hash_indices.numel() != valid_count:
|
| 81 |
-
raise ValueError("hash_indices must align with valid online edges")
|
| 82 |
-
valid_scores = edge_scores.detach()[edge_mask]
|
| 83 |
-
valid_marginals = marginals.detach()[edge_mask]
|
| 84 |
-
valid_map = map_edges.detach()[edge_mask]
|
| 85 |
-
if not bool(torch.isfinite(valid_scores).all()):
|
| 86 |
-
raise FloatingPointError("nonfinite online edge scores")
|
| 87 |
-
if not bool(torch.isfinite(valid_marginals).all()):
|
| 88 |
-
raise FloatingPointError("nonfinite online marginals")
|
| 89 |
-
if valid_marginals.numel() and bool(
|
| 90 |
-
((valid_marginals < 0) | (valid_marginals > 1)).any()
|
| 91 |
-
):
|
| 92 |
-
raise ValueError("online marginals must lie in [0, 1]")
|
| 93 |
-
|
| 94 |
-
selected = int(valid_map.sum().item())
|
| 95 |
-
hashes = hash_indices.detach().to(device="cpu", dtype=torch.long)
|
| 96 |
-
self._base_tokens += int(base_tokens)
|
| 97 |
-
self._effective_tokens += float(base_tokens - selected)
|
| 98 |
-
self._eligible_edges += valid_count
|
| 99 |
-
self._selected_merges += selected
|
| 100 |
-
self._positive_edges += int((valid_scores > 0).sum().item())
|
| 101 |
-
self._marginal_sum += float(valid_marginals.float().sum().item())
|
| 102 |
-
self._touched_buckets.update(int(value) for value in hashes.tolist())
|
| 103 |
-
if selected:
|
| 104 |
-
self._selected_buckets.update(
|
| 105 |
-
int(value) for value in hashes[valid_map.to(device="cpu")].tolist()
|
| 106 |
-
)
|
| 107 |
-
|
| 108 |
-
def observe_q15(
|
| 109 |
-
self,
|
| 110 |
-
*,
|
| 111 |
-
base_tokens: int,
|
| 112 |
-
probabilities: torch.Tensor,
|
| 113 |
-
hash_indices: torch.Tensor,
|
| 114 |
-
effective_tokens: torch.Tensor,
|
| 115 |
-
exact_hits: torch.Tensor | None = None,
|
| 116 |
-
) -> None:
|
| 117 |
-
if not self._should_record():
|
| 118 |
-
return
|
| 119 |
-
self._set_kind("q15")
|
| 120 |
-
if base_tokens < 0:
|
| 121 |
-
raise ValueError("base_tokens must be non-negative")
|
| 122 |
-
if probabilities.ndim != 1 or hash_indices.ndim != 1:
|
| 123 |
-
raise ValueError("Q15 probabilities and hash_indices must be one-dimensional")
|
| 124 |
-
if probabilities.numel() != hash_indices.numel():
|
| 125 |
-
raise ValueError("Q15 probabilities and hash_indices must align")
|
| 126 |
-
if exact_hits is not None and (
|
| 127 |
-
exact_hits.dtype != torch.bool
|
| 128 |
-
or exact_hits.ndim != 1
|
| 129 |
-
or exact_hits.numel() != probabilities.numel()
|
| 130 |
-
):
|
| 131 |
-
raise ValueError("Q15 exact hits must be an aligned boolean vector")
|
| 132 |
-
detached = probabilities.detach()
|
| 133 |
-
if not bool(torch.isfinite(detached).all()):
|
| 134 |
-
raise FloatingPointError("nonfinite Q15 probabilities")
|
| 135 |
-
if detached.numel() and bool(((detached < 0) | (detached > 1)).any()):
|
| 136 |
-
raise ValueError("Q15 probabilities must lie in [0, 1]")
|
| 137 |
-
effective = float(effective_tokens.detach().float().item())
|
| 138 |
-
if not math.isfinite(effective):
|
| 139 |
-
raise FloatingPointError("nonfinite Q15 effective token count")
|
| 140 |
-
if effective < 0.0 or effective > float(base_tokens):
|
| 141 |
-
raise ValueError("Q15 effective token count is outside the base count")
|
| 142 |
-
|
| 143 |
-
self._base_tokens += int(base_tokens)
|
| 144 |
-
self._effective_tokens += effective
|
| 145 |
-
self._eligible_edges += int(detached.numel())
|
| 146 |
-
self._probability_sum += float(detached.float().sum().item())
|
| 147 |
-
self._above_half += int((detached > 0.5).sum().item())
|
| 148 |
-
self._exact_hits += (
|
| 149 |
-
0 if exact_hits is None else int(exact_hits.detach().sum().item())
|
| 150 |
-
)
|
| 151 |
-
self._touched_buckets.update(
|
| 152 |
-
int(value)
|
| 153 |
-
for value in hash_indices.detach().to(device="cpu", dtype=torch.long).tolist()
|
| 154 |
-
)
|
| 155 |
-
|
| 156 |
-
def snapshot(self) -> dict[str, float | int]:
|
| 157 |
-
if self._phase is not None:
|
| 158 |
-
raise RuntimeError("vocabulary telemetry phase is active")
|
| 159 |
-
base = self._base_tokens
|
| 160 |
-
edges = self._eligible_edges
|
| 161 |
-
touched = len(self._touched_buckets)
|
| 162 |
-
metrics: dict[str, float | int] = {
|
| 163 |
-
"vocab/base_token_occurrences": base,
|
| 164 |
-
"vocab/effective_token_count": self._effective_tokens,
|
| 165 |
-
"vocab/token_compression_ratio": (
|
| 166 |
-
self._effective_tokens / base if base else 0.0
|
| 167 |
-
),
|
| 168 |
-
"vocab/eligible_edge_occurrences": edges,
|
| 169 |
-
"vocab/distinct_touched_hash_buckets": touched,
|
| 170 |
-
"vocab/touched_bucket_occurrence_pressure": (
|
| 171 |
-
edges / touched if touched else 0.0
|
| 172 |
-
),
|
| 173 |
-
}
|
| 174 |
-
if self._kind == "online":
|
| 175 |
-
metrics.update(
|
| 176 |
-
{
|
| 177 |
-
"vocab/hard_selected_merges": self._selected_merges,
|
| 178 |
-
"vocab/hard_merge_rate": (
|
| 179 |
-
self._selected_merges / edges if edges else 0.0
|
| 180 |
-
),
|
| 181 |
-
"vocab/positive_edge_occurrences": self._positive_edges,
|
| 182 |
-
"vocab/positive_edge_fraction": (
|
| 183 |
-
self._positive_edges / edges if edges else 0.0
|
| 184 |
-
),
|
| 185 |
-
"vocab/exact_marginal_mean": (
|
| 186 |
-
self._marginal_sum / edges if edges else 0.0
|
| 187 |
-
),
|
| 188 |
-
"vocab/map_selected_distinct_hash_buckets": len(
|
| 189 |
-
self._selected_buckets
|
| 190 |
-
),
|
| 191 |
-
}
|
| 192 |
-
)
|
| 193 |
-
elif self._kind == "q15":
|
| 194 |
-
metrics.update(
|
| 195 |
-
{
|
| 196 |
-
"vocab/mean_merge_probability": (
|
| 197 |
-
self._probability_sum / edges if edges else 0.0
|
| 198 |
-
),
|
| 199 |
-
"vocab/expected_merge_mass": self._probability_sum,
|
| 200 |
-
"vocab/above_half_occurrences": self._above_half,
|
| 201 |
-
"vocab/above_half_fraction": (
|
| 202 |
-
self._above_half / edges if edges else 0.0
|
| 203 |
-
),
|
| 204 |
-
"vocab/exact_hit_occurrences": self._exact_hits,
|
| 205 |
-
"vocab/exact_hit_rate": self._exact_hits / edges if edges else 0.0,
|
| 206 |
-
}
|
| 207 |
-
)
|
| 208 |
-
return metrics
|
| 209 |
-
|
| 210 |
-
def reset(self) -> None:
|
| 211 |
-
if self._phase is not None:
|
| 212 |
-
raise RuntimeError("vocabulary telemetry phase is active")
|
| 213 |
-
self._kind: str | None = None
|
| 214 |
-
self._base_tokens = 0
|
| 215 |
-
self._effective_tokens = 0.0
|
| 216 |
-
self._eligible_edges = 0
|
| 217 |
-
self._selected_merges = 0
|
| 218 |
-
self._positive_edges = 0
|
| 219 |
-
self._marginal_sum = 0.0
|
| 220 |
-
self._probability_sum = 0.0
|
| 221 |
-
self._above_half = 0
|
| 222 |
-
self._exact_hits = 0
|
| 223 |
-
self._touched_buckets: set[int] = set()
|
| 224 |
-
self._selected_buckets: set[int] = set()
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
_SHA256 = re.compile(r"[0-9a-f]{64}")
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
@dataclass(frozen=True)
|
| 231 |
-
class VocabularyTelemetrySettings:
|
| 232 |
-
arm: str
|
| 233 |
-
interval_steps: int
|
| 234 |
-
stable_max_grad_norm: float
|
| 235 |
-
merge_sample_rows: int
|
| 236 |
-
merge_sample_seed: int
|
| 237 |
-
corpus_manifest: Path
|
| 238 |
-
corpus_manifest_sha256: str
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
def canonical_config_sha256(config: dict[str, Any]) -> str:
|
| 242 |
-
payload = json.dumps(
|
| 243 |
-
config, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
| 244 |
-
).encode("utf-8")
|
| 245 |
-
return hashlib.sha256(payload).hexdigest()
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
def _sha256(path: Path) -> str:
|
| 249 |
-
digest = hashlib.sha256()
|
| 250 |
-
with path.open("rb") as handle:
|
| 251 |
-
for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
| 252 |
-
digest.update(chunk)
|
| 253 |
-
return digest.hexdigest()
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
def validate_vocabulary_telemetry_config(
|
| 257 |
-
config: dict[str, Any], module: Any
|
| 258 |
-
) -> VocabularyTelemetrySettings | None:
|
| 259 |
-
raw = config.get("vocabulary_telemetry")
|
| 260 |
-
if not raw:
|
| 261 |
-
return None
|
| 262 |
-
if not isinstance(raw, dict):
|
| 263 |
-
raise ValueError("vocabulary telemetry settings must be an object")
|
| 264 |
-
required = {
|
| 265 |
-
"protocol",
|
| 266 |
-
"arm",
|
| 267 |
-
"interval_steps",
|
| 268 |
-
"stable_max_grad_norm",
|
| 269 |
-
"merge_sample_rows",
|
| 270 |
-
"merge_sample_seed",
|
| 271 |
-
"corpus_manifest",
|
| 272 |
-
"corpus_manifest_sha256",
|
| 273 |
-
}
|
| 274 |
-
if set(raw) != required:
|
| 275 |
-
raise ValueError("vocabulary telemetry settings have an invalid schema")
|
| 276 |
-
target = stable_gradient_target(config)
|
| 277 |
-
assert target is not None
|
| 278 |
-
arm = str(raw["arm"])
|
| 279 |
-
mode = str(getattr(module, "mode", ""))
|
| 280 |
-
if arm == "q15":
|
| 281 |
-
table = getattr(module, "collapse", None)
|
| 282 |
-
if mode != "collapse_dynamic" or table is None:
|
| 283 |
-
raise ValueError("Q15 telemetry requires collapse_dynamic")
|
| 284 |
-
pair_keys = getattr(table, "pair_keys", torch.empty(0, dtype=torch.long))
|
| 285 |
-
growth = config.get("vocabulary_growth")
|
| 286 |
-
if pair_keys.numel() and not growth:
|
| 287 |
-
raise ValueError("Q15 telemetry requires an unpromoted hash table")
|
| 288 |
-
if (
|
| 289 |
-
(getattr(table, "discovery", None) is not None or getattr(
|
| 290 |
-
table, "diagnostic_discovery", None
|
| 291 |
-
) is not None)
|
| 292 |
-
and not growth
|
| 293 |
-
):
|
| 294 |
-
raise ValueError("Q15 telemetry forbids discovery observers")
|
| 295 |
-
if config.get("dynamic_vocabulary") or config.get("reclaimed_vocabulary"):
|
| 296 |
-
raise ValueError("Q15 telemetry forbids dynamic vocabulary settings")
|
| 297 |
-
if config.get("online_tokenizer"):
|
| 298 |
-
raise ValueError("Q15 telemetry forbids online tokenizer settings")
|
| 299 |
-
elif arm == "q29":
|
| 300 |
-
table = getattr(module, "collapse", None)
|
| 301 |
-
if mode != "collapse_dynamic" or table is None:
|
| 302 |
-
raise ValueError("Q29 telemetry requires collapse_dynamic")
|
| 303 |
-
pair_keys = getattr(table, "pair_keys", torch.empty(0, dtype=torch.long))
|
| 304 |
-
if not pair_keys.numel():
|
| 305 |
-
raise ValueError("Q29 telemetry requires a promoted exact table")
|
| 306 |
-
if (
|
| 307 |
-
getattr(table, "discovery", None) is not None
|
| 308 |
-
or getattr(table, "diagnostic_discovery", None) is not None
|
| 309 |
-
):
|
| 310 |
-
raise ValueError("Q29 telemetry forbids discovery observers")
|
| 311 |
-
if (
|
| 312 |
-
config.get("dynamic_vocabulary")
|
| 313 |
-
or config.get("reclaimed_vocabulary")
|
| 314 |
-
or config.get("vocabulary_growth")
|
| 315 |
-
or config.get("online_tokenizer")
|
| 316 |
-
):
|
| 317 |
-
raise ValueError("Q29 telemetry requires a fixed exact vocabulary")
|
| 318 |
-
elif arm == "online":
|
| 319 |
-
if mode != "collapse_online" or getattr(module, "online_collapse", None) is None:
|
| 320 |
-
raise ValueError("online telemetry requires collapse_online")
|
| 321 |
-
if config.get("dynamic_vocabulary") or config.get("reclaimed_vocabulary"):
|
| 322 |
-
raise ValueError("online telemetry forbids dynamic vocabulary settings")
|
| 323 |
-
else:
|
| 324 |
-
raise ValueError("vocabulary telemetry arm must be q15, q29 or online")
|
| 325 |
-
|
| 326 |
-
interval_steps = int(raw["interval_steps"])
|
| 327 |
-
merge_sample_rows = int(raw["merge_sample_rows"])
|
| 328 |
-
merge_sample_seed = int(raw["merge_sample_seed"])
|
| 329 |
-
smoke_identity = (
|
| 330 |
-
str(config.get("run_name", ""))
|
| 331 |
-
== f"multilingual-sota-expanded10k-{arm}-smoke-v1"
|
| 332 |
-
and str(config.get("experiment_arm", "")) == f"{arm}-smoke-v1"
|
| 333 |
-
and int(dict(config.get("training") or {}).get("max_steps", 0)) == 1
|
| 334 |
-
)
|
| 335 |
-
growth_smoke = bool(config.get("vocabulary_growth")) and int(
|
| 336 |
-
dict(config.get("training") or {}).get("max_steps", 0)
|
| 337 |
-
) <= 3
|
| 338 |
-
if interval_steps != 100 and not (
|
| 339 |
-
interval_steps == 1 and (smoke_identity or growth_smoke)
|
| 340 |
-
):
|
| 341 |
-
raise ValueError("vocabulary telemetry interval must be 100 steps")
|
| 342 |
-
if merge_sample_rows != 4096:
|
| 343 |
-
raise ValueError("vocabulary telemetry merge sample must contain 4096 rows")
|
| 344 |
-
if merge_sample_seed != 20260805:
|
| 345 |
-
raise ValueError("vocabulary telemetry merge sample seed must be 20260805")
|
| 346 |
-
corpus_manifest = Path(str(raw["corpus_manifest"])).expanduser().resolve()
|
| 347 |
-
if not corpus_manifest.is_file():
|
| 348 |
-
raise ValueError("vocabulary telemetry corpus manifest is missing")
|
| 349 |
-
expected_digest = str(raw["corpus_manifest_sha256"])
|
| 350 |
-
if _SHA256.fullmatch(expected_digest) is None:
|
| 351 |
-
raise ValueError("vocabulary telemetry manifest hash is malformed")
|
| 352 |
-
if _sha256(corpus_manifest) != expected_digest:
|
| 353 |
-
raise ValueError("vocabulary telemetry manifest hash mismatch")
|
| 354 |
-
return VocabularyTelemetrySettings(
|
| 355 |
-
arm=arm,
|
| 356 |
-
interval_steps=interval_steps,
|
| 357 |
-
stable_max_grad_norm=target,
|
| 358 |
-
merge_sample_rows=merge_sample_rows,
|
| 359 |
-
merge_sample_seed=merge_sample_seed,
|
| 360 |
-
corpus_manifest=corpus_manifest,
|
| 361 |
-
corpus_manifest_sha256=expected_digest,
|
| 362 |
-
)
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
def _tensor_statistics(
|
| 366 |
-
values: torch.Tensor, *, prefix: str
|
| 367 |
-
) -> dict[str, float]:
|
| 368 |
-
detached = values.detach().float().reshape(-1)
|
| 369 |
-
if not detached.numel():
|
| 370 |
-
raise ValueError(f"{prefix} statistics require at least one value")
|
| 371 |
-
if not bool(torch.isfinite(detached).all()):
|
| 372 |
-
raise FloatingPointError(f"nonfinite {prefix} values")
|
| 373 |
-
return {
|
| 374 |
-
f"{prefix}_mean": float(detached.mean().item()),
|
| 375 |
-
f"{prefix}_std": float(detached.std(unbiased=False).item()),
|
| 376 |
-
f"{prefix}_min": float(detached.min().item()),
|
| 377 |
-
f"{prefix}_median": float(torch.quantile(detached, 0.5).item()),
|
| 378 |
-
f"{prefix}_max": float(detached.max().item()),
|
| 379 |
-
f"{prefix}_positive_fraction": float((detached > 0).float().mean().item()),
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
class VocabularyTelemetryCoordinator:
|
| 384 |
-
"""Combine window, parameter and gradient metrics into durable JSONL."""
|
| 385 |
-
|
| 386 |
-
def __init__(
|
| 387 |
-
self,
|
| 388 |
-
*,
|
| 389 |
-
module: Any,
|
| 390 |
-
window: VocabularyWindow,
|
| 391 |
-
gradient_callback: Any,
|
| 392 |
-
output_path: Path,
|
| 393 |
-
arm: str,
|
| 394 |
-
config_sha256: str,
|
| 395 |
-
corpus_manifest_sha256: str,
|
| 396 |
-
merge_sample_rows: int,
|
| 397 |
-
merge_sample_seed: int,
|
| 398 |
-
growth_callback: Any | None = None,
|
| 399 |
-
) -> None:
|
| 400 |
-
if arm not in {"q15", "q29", "online"}:
|
| 401 |
-
raise ValueError("telemetry arm must be q15, q29 or online")
|
| 402 |
-
if _SHA256.fullmatch(config_sha256) is None:
|
| 403 |
-
raise ValueError("config_sha256 must be lowercase hexadecimal")
|
| 404 |
-
if _SHA256.fullmatch(corpus_manifest_sha256) is None:
|
| 405 |
-
raise ValueError("corpus_manifest_sha256 must be lowercase hexadecimal")
|
| 406 |
-
if merge_sample_rows <= 0:
|
| 407 |
-
raise ValueError("merge_sample_rows must be positive")
|
| 408 |
-
mode = str(getattr(module, "mode", ""))
|
| 409 |
-
if arm == "online":
|
| 410 |
-
if mode != "collapse_online" or getattr(module, "online_collapse", None) is None:
|
| 411 |
-
raise ValueError("online telemetry requires collapse_online")
|
| 412 |
-
table = module.online_collapse
|
| 413 |
-
scores = table.score_hash
|
| 414 |
-
else:
|
| 415 |
-
if mode not in {"collapse", "collapse_shared", "collapse_dynamic"}:
|
| 416 |
-
raise ValueError("Q15 telemetry requires an uncompiled collapse table")
|
| 417 |
-
table = getattr(module, "collapse", None)
|
| 418 |
-
if table is None:
|
| 419 |
-
raise ValueError("Q15 telemetry requires a collapse table")
|
| 420 |
-
scores = table.score
|
| 421 |
-
merged = table.merged
|
| 422 |
-
if merged.ndim != 2 or not merged.shape[0]:
|
| 423 |
-
raise ValueError("telemetry merge table must be a non-empty matrix")
|
| 424 |
-
|
| 425 |
-
generator = torch.Generator(device="cpu")
|
| 426 |
-
generator.manual_seed(int(merge_sample_seed))
|
| 427 |
-
sample_count = min(int(merge_sample_rows), int(merged.shape[0]))
|
| 428 |
-
self.sampled_merge_rows = tuple(
|
| 429 |
-
int(value)
|
| 430 |
-
for value in torch.randperm(
|
| 431 |
-
int(merged.shape[0]), generator=generator, device="cpu"
|
| 432 |
-
)[:sample_count].tolist()
|
| 433 |
-
)
|
| 434 |
-
self.module = module
|
| 435 |
-
self.table = table
|
| 436 |
-
self.scores = scores
|
| 437 |
-
self.merge_sample_rows = int(merge_sample_rows)
|
| 438 |
-
self.merge_sample_seed = int(merge_sample_seed)
|
| 439 |
-
self.growth_callback = growth_callback
|
| 440 |
-
self.window = window
|
| 441 |
-
self.gradient_callback = gradient_callback
|
| 442 |
-
self.output_path = Path(output_path)
|
| 443 |
-
self.arm = arm
|
| 444 |
-
self.config_sha256 = config_sha256
|
| 445 |
-
self.corpus_manifest_sha256 = corpus_manifest_sha256
|
| 446 |
-
|
| 447 |
-
def _parameter_metrics(self) -> dict[str, float | int]:
|
| 448 |
-
embedding = self.module.embedding.weight
|
| 449 |
-
exact_rows = int(getattr(self.table, "pair_keys", torch.empty(0)).numel())
|
| 450 |
-
residual_rows = int(getattr(self.table, "residual_buckets", 0))
|
| 451 |
-
if residual_rows:
|
| 452 |
-
merged = self.table.residual_merged
|
| 453 |
-
scores = self.table.residual_score
|
| 454 |
-
else:
|
| 455 |
-
merged = self.table.merged
|
| 456 |
-
scores = self.table.score if self.arm == "q15" else self.scores
|
| 457 |
-
generator = torch.Generator(device="cpu")
|
| 458 |
-
generator.manual_seed(self.merge_sample_seed)
|
| 459 |
-
sample_index = torch.randperm(
|
| 460 |
-
int(merged.shape[0]), generator=generator, device="cpu"
|
| 461 |
-
)[: min(self.merge_sample_rows, int(merged.shape[0]))].to(merged.device)
|
| 462 |
-
sampled_norms = merged.index_select(0, sample_index).detach().float().norm(dim=1)
|
| 463 |
-
metrics: dict[str, float | int] = {
|
| 464 |
-
"vocab/base_rows": int(embedding.shape[0]),
|
| 465 |
-
"vocab/merge_capacity_rows": int(getattr(self.table, "buckets")),
|
| 466 |
-
"vocab/exact_rows": exact_rows,
|
| 467 |
-
"vocab/residual_rows": residual_rows,
|
| 468 |
-
"vocab/total_learned_pair_rows": exact_rows + (
|
| 469 |
-
residual_rows if residual_rows else int(getattr(self.table, "buckets"))
|
| 470 |
-
),
|
| 471 |
-
}
|
| 472 |
-
metrics.update(
|
| 473 |
-
_tensor_statistics(scores, prefix="vocab/global_score")
|
| 474 |
-
)
|
| 475 |
-
metrics.update(
|
| 476 |
-
_tensor_statistics(
|
| 477 |
-
self.table.language_bias, prefix="vocab/language_bias"
|
| 478 |
-
)
|
| 479 |
-
)
|
| 480 |
-
merge_statistics = _tensor_statistics(
|
| 481 |
-
sampled_norms, prefix="vocab/merge_vector_norm"
|
| 482 |
-
)
|
| 483 |
-
merge_statistics.pop("vocab/merge_vector_norm_positive_fraction")
|
| 484 |
-
metrics.update(merge_statistics)
|
| 485 |
-
if self.growth_callback is not None:
|
| 486 |
-
metrics.update(
|
| 487 |
-
{
|
| 488 |
-
"vocab/last_growth_additions": int(
|
| 489 |
-
self.growth_callback.last_additions
|
| 490 |
-
),
|
| 491 |
-
"vocab/cumulative_growth_additions": int(
|
| 492 |
-
self.growth_callback.cumulative_additions
|
| 493 |
-
),
|
| 494 |
-
"vocab/selector_candidate_pairs": int(
|
| 495 |
-
self.growth_callback.selector_candidates
|
| 496 |
-
),
|
| 497 |
-
"vocab/selector_eligible_pairs": int(
|
| 498 |
-
self.growth_callback.selector_eligible
|
| 499 |
-
),
|
| 500 |
-
}
|
| 501 |
-
)
|
| 502 |
-
return metrics
|
| 503 |
-
|
| 504 |
-
def _gradient_metrics(self) -> dict[str, float | int]:
|
| 505 |
-
report = getattr(self.gradient_callback, "last_report", None)
|
| 506 |
-
if not isinstance(report, GradientReport):
|
| 507 |
-
raise RuntimeError("vocabulary telemetry has no stable gradient report")
|
| 508 |
-
metrics: dict[str, float | int] = {
|
| 509 |
-
"gradient/stable_total_norm": report.total_norm,
|
| 510 |
-
"gradient/clip_coefficient": report.clip_coefficient,
|
| 511 |
-
"gradient/max_abs": report.max_abs,
|
| 512 |
-
"gradient/parameter_count": report.parameter_count,
|
| 513 |
-
}
|
| 514 |
-
metrics.update(
|
| 515 |
-
{
|
| 516 |
-
f"gradient/group/{name}_norm": value
|
| 517 |
-
for name, value in sorted(report.group_norms.items())
|
| 518 |
-
}
|
| 519 |
-
)
|
| 520 |
-
return metrics
|
| 521 |
-
|
| 522 |
-
def _existing_bytes(self, *, step: int) -> bytes:
|
| 523 |
-
if not self.output_path.exists():
|
| 524 |
-
return b""
|
| 525 |
-
existing = self.output_path.read_bytes()
|
| 526 |
-
if existing and not existing.endswith(b"\n"):
|
| 527 |
-
raise ValueError("vocabulary metrics JSONL has a partial final line")
|
| 528 |
-
previous_step = 0
|
| 529 |
-
for raw_line in existing.splitlines():
|
| 530 |
-
record = json.loads(raw_line)
|
| 531 |
-
if (
|
| 532 |
-
record.get("protocol") != "vocabulary-training-metrics-v1"
|
| 533 |
-
or record.get("arm") != self.arm
|
| 534 |
-
or record.get("config_sha256") != self.config_sha256
|
| 535 |
-
or record.get("corpus_manifest_sha256")
|
| 536 |
-
!= self.corpus_manifest_sha256
|
| 537 |
-
):
|
| 538 |
-
raise ValueError("existing vocabulary metrics identity differs")
|
| 539 |
-
current = int(record.get("optimizer_step", -1))
|
| 540 |
-
if current <= previous_step:
|
| 541 |
-
raise ValueError("existing vocabulary metric steps are not increasing")
|
| 542 |
-
previous_step = current
|
| 543 |
-
if step <= previous_step:
|
| 544 |
-
raise ValueError("vocabulary metric step is not newer than existing records")
|
| 545 |
-
return existing
|
| 546 |
-
|
| 547 |
-
def _replace_jsonl(self, record: dict[str, object]) -> None:
|
| 548 |
-
step = int(record["optimizer_step"])
|
| 549 |
-
existing = self._existing_bytes(step=step)
|
| 550 |
-
self.output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 551 |
-
temporary = self.output_path.with_suffix(self.output_path.suffix + ".tmp")
|
| 552 |
-
if temporary.exists():
|
| 553 |
-
raise FileExistsError(f"stale vocabulary metrics temporary file: {temporary}")
|
| 554 |
-
line = json.dumps(record, ensure_ascii=False, sort_keys=True).encode("utf-8") + b"\n"
|
| 555 |
-
with temporary.open("xb") as handle:
|
| 556 |
-
handle.write(existing)
|
| 557 |
-
handle.write(line)
|
| 558 |
-
handle.flush()
|
| 559 |
-
os.fsync(handle.fileno())
|
| 560 |
-
os.replace(temporary, self.output_path)
|
| 561 |
-
directory = os.open(self.output_path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
| 562 |
-
try:
|
| 563 |
-
os.fsync(directory)
|
| 564 |
-
finally:
|
| 565 |
-
os.close(directory)
|
| 566 |
-
|
| 567 |
-
def flush(self, step: int) -> dict[str, float | int]:
|
| 568 |
-
if isinstance(step, bool) or not isinstance(step, int) or step <= 0:
|
| 569 |
-
raise ValueError("optimizer step must be a positive integer")
|
| 570 |
-
metrics = self.window.snapshot()
|
| 571 |
-
metrics.update(self._parameter_metrics())
|
| 572 |
-
metrics.update(self._gradient_metrics())
|
| 573 |
-
if any(
|
| 574 |
-
isinstance(value, float) and not math.isfinite(value)
|
| 575 |
-
for value in metrics.values()
|
| 576 |
-
):
|
| 577 |
-
raise FloatingPointError("nonfinite vocabulary telemetry metric")
|
| 578 |
-
record: dict[str, object] = {
|
| 579 |
-
"protocol": "vocabulary-training-metrics-v1",
|
| 580 |
-
"arm": self.arm,
|
| 581 |
-
"optimizer_step": step,
|
| 582 |
-
"config_sha256": self.config_sha256,
|
| 583 |
-
"corpus_manifest_sha256": self.corpus_manifest_sha256,
|
| 584 |
-
"metrics": metrics,
|
| 585 |
-
}
|
| 586 |
-
self._replace_jsonl(record)
|
| 587 |
-
self.window.reset()
|
| 588 |
-
return metrics
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
class VocabularyLoggingCallback(TrainerCallback):
|
| 592 |
-
"""Merge one durable vocabulary record into the Trainer log dictionary."""
|
| 593 |
-
|
| 594 |
-
def __init__(
|
| 595 |
-
self,
|
| 596 |
-
coordinator: VocabularyTelemetryCoordinator,
|
| 597 |
-
*,
|
| 598 |
-
interval_steps: int,
|
| 599 |
-
) -> None:
|
| 600 |
-
if interval_steps <= 0:
|
| 601 |
-
raise ValueError("vocabulary logging interval must be positive")
|
| 602 |
-
self.coordinator = coordinator
|
| 603 |
-
self.interval_steps = int(interval_steps)
|
| 604 |
-
self.last_flushed_step = 0
|
| 605 |
-
|
| 606 |
-
def on_log(self, args, state, control, logs=None, **kwargs):
|
| 607 |
-
del args, control, kwargs
|
| 608 |
-
step = int(state.global_step)
|
| 609 |
-
if (
|
| 610 |
-
logs is None
|
| 611 |
-
or step <= 0
|
| 612 |
-
or step % self.interval_steps
|
| 613 |
-
or step == self.last_flushed_step
|
| 614 |
-
):
|
| 615 |
-
return
|
| 616 |
-
metrics = self.coordinator.flush(step)
|
| 617 |
-
logs.update(metrics)
|
| 618 |
-
self.last_flushed_step = step
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
def install_vocabulary_logging_callback(
|
| 622 |
-
trainer: Any, callback: VocabularyLoggingCallback
|
| 623 |
-
) -> None:
|
| 624 |
-
callbacks = trainer.callback_handler.callbacks
|
| 625 |
-
if any(isinstance(current, VocabularyLoggingCallback) for current in callbacks):
|
| 626 |
-
raise RuntimeError("vocabulary logging callback is already installed")
|
| 627 |
-
callbacks.insert(0, callback)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|