BorisTM commited on
Commit
5db2fad
·
verified ·
1 Parent(s): 35eed1b

Remove internal research files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. evaluation/compact-multilingual-comparison.json +0 -84
  2. evaluation/full-mmteb-summary.json +0 -233
  3. provenance/model-manifest.json +0 -77
  4. run_config.json +0 -753
  5. starse/__init__.py +0 -3
  6. starse/binary_static_embedding.py +0 -160
  7. starse/cached_loss.py +0 -223
  8. starse/checkpoint_precision.py +0 -166
  9. starse/collapse.py +0 -1930
  10. starse/compiled_latent_encoder.py +0 -281
  11. starse/conditioned_module.py +0 -738
  12. starse/gradient_safety.py +0 -161
  13. starse/grove_counterfactual.py +0 -293
  14. starse/grove_selection.py +0 -202
  15. starse/init_model.py +0 -362
  16. starse/language_conditioned.py +0 -165
  17. starse/latent_compilation.py +0 -1095
  18. starse/latent_lexicon.py +0 -1097
  19. starse/latent_profile.py +0 -267
  20. starse/latent_profiling.py +0 -975
  21. starse/latent_streaming.py +0 -1065
  22. starse/latent_training.py +0 -762
  23. starse/loss.py +0 -182
  24. starse/loss_path_vocabulary.py +0 -161
  25. starse/ngram_table.py +0 -86
  26. starse/online_tokenizer.py +0 -378
  27. starse/online_tokenizer_compilation.py +0 -686
  28. starse/openlex_compilation.py +0 -428
  29. starse/openlex_dictionary.py +0 -322
  30. starse/openlex_lattice.py +0 -396
  31. starse/openlex_model.py +0 -1196
  32. starse/openlex_replay.py +0 -604
  33. starse/openlex_replay_checkpoint.py +0 -168
  34. starse/openlex_reuse.py +0 -338
  35. starse/openlex_spans.py +0 -215
  36. starse/openlex_tokenizer.py +0 -180
  37. starse/progressive_model.py +0 -395
  38. starse/project.py +0 -120
  39. starse/recursive_cascade.py +0 -1078
  40. starse/split_vocabulary.py +0 -105
  41. starse/streaming.py +0 -1023
  42. starse/structured_tokenizer.py +0 -151
  43. starse/train.py +0 -508
  44. starse/train_multilingual.py +0 -0
  45. starse/train_progressive.py +0 -340
  46. starse/translation_edit.py +0 -142
  47. starse/vocab_assignment.py +0 -73
  48. starse/vocabulary_growth.py +0 -598
  49. starse/vocabulary_probe.py +0 -720
  50. 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)