Instructions to use appvoid/cortex with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use appvoid/cortex with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="appvoid/cortex", trust_remote_code=True)# Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("appvoid/cortex", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use appvoid/cortex with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "appvoid/cortex" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "appvoid/cortex", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/appvoid/cortex
- SGLang
How to use appvoid/cortex with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "appvoid/cortex" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "appvoid/cortex", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "appvoid/cortex" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "appvoid/cortex", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use appvoid/cortex with Docker Model Runner:
docker model run hf.co/appvoid/cortex
unified-v1-blt-none-s42: state and inference synchronized at checkpoint-000000000000
Browse files- experiments.csv +1 -0
- unified/ablations/unified-v1-blt-none-s42/checkpoint-000000000000/COMPLETE +1 -0
- unified/ablations/unified-v1-blt-none-s42/checkpoint-000000000000/metadata.json +9 -0
- unified/ablations/unified-v1-blt-none-s42/checkpoint-000000000000/training.pt +3 -0
- unified/ablations/unified-v1-blt-none-s42/inference/data_progress.json +47 -0
- unified/ablations/unified-v1-blt-none-s42/inference/experiment_config.json +97 -0
- unified/ablations/unified-v1-blt-none-s42/inference/model.safetensors +3 -0
- unified/ablations/unified-v1-blt-none-s42/source/__init__.py +1 -0
- unified/ablations/unified-v1-blt-none-s42/source/audio_codec.py +101 -0
- unified/ablations/unified-v1-blt-none-s42/source/bet_model.py +488 -0
- unified/ablations/unified-v1-blt-none-s42/source/braille_encoder.py +1532 -0
- unified/ablations/unified-v1-blt-none-s42/source/canvas_flow.py +1025 -0
- unified/ablations/unified-v1-blt-none-s42/source/causal_arch.py +747 -0
- unified/ablations/unified-v1-blt-none-s42/source/checkpoints.py +190 -0
- unified/ablations/unified-v1-blt-none-s42/source/configuration_bet.py +64 -0
- unified/ablations/unified-v1-blt-none-s42/source/cortex_curriculum_v16.py +0 -0
- unified/ablations/unified-v1-blt-none-s42/source/cortex_data.py +132 -0
- unified/ablations/unified-v1-blt-none-s42/source/cortex_oracles.py +250 -0
- unified/ablations/unified-v1-blt-none-s42/source/cortex_source.py +83 -0
- unified/ablations/unified-v1-blt-none-s42/source/cortex_tasks.py +521 -0
- unified/ablations/unified-v1-blt-none-s42/source/data_stream.py +193 -0
- unified/ablations/unified-v1-blt-none-s42/source/dataset_registry.py +46 -0
- unified/ablations/unified-v1-blt-none-s42/source/inference.py +43 -0
- unified/ablations/unified-v1-blt-none-s42/source/media_data.py +88 -0
- unified/ablations/unified-v1-blt-none-s42/source/modeling_bet.py +76 -0
- unified/ablations/unified-v1-blt-none-s42/source/parquet_stream.py +37 -0
- unified/ablations/unified-v1-blt-none-s42/source/prepared_data.py +445 -0
- unified/ablations/unified-v1-blt-none-s42/source/records.py +37 -0
- unified/ablations/unified-v1-blt-none-s42/source/tokenization_bet.py +137 -0
- unified/ablations/unified-v1-blt-none-s42/source/trainer.py +481 -0
- unified/ablations/unified-v1-blt-none-s42/source/unified_data.py +157 -0
- unified/ablations/unified-v1-blt-none-s42/source/unified_models.py +91 -0
- unified/ablations/unified-v1-blt-none-s42/source/unified_runner.py +330 -0
- unified/ablations/unified-v1-blt-none-s42/source/unified_state.py +153 -0
experiments.csv
CHANGED
|
@@ -14,3 +14,4 @@ unified-v1-evabyte-none-s42,ablation,causal,evabyte,none,42,25,0,running,fresh_m
|
|
| 14 |
unified-v1-evabyte-none-s42,ablation,causal,evabyte,none,42,50,0,running,fresh_matched_seed,3644,3644,1.026456674999963,0.0,10668800,1024,3.0752868056297302,6.275834560394287,2420984,20.154760339000404,120119.71163533424,0.12007386237382889,0,bf16,0,269ba207e744573779747b593ffbcd8a87683fd55c83f6b168c658206d9aeb98,af33c522ea4349d5aae0491f7fe085d4bea1b044b08ff57eb064354bbb96435f,"{""climbmix"": {""documents_emitted"": 530, ""epoch"": 0, ""prepared_position"": 1664, ""rows_read"": 530, ""shard"": 0, ""shards"": 6543}, ""rewrite6"": {""documents_emitted"": 214, ""epoch"": 0, ""prepared_position"": 273, ""rows_read"": 215, ""shard"": 0, ""shards"": 1}, ""ultra_qa"": {""documents_emitted"": 109, ""epoch"": 0, ""prepared_position"": 475, ""rows_read"": 110, ""shard"": 0, ""shards"": 616}, ""ultra_style"": {""documents_emitted"": 136, ""epoch"": 0, ""prepared_position"": 481, ""rows_read"": 137, ""shard"": 0, ""shards"": 552}}"
|
| 15 |
unified-v1-evabyte-none-s42,ablation,causal,evabyte,none,42,75,0,running,fresh_matched_seed,5450,5450,1.026456674999963,0.0,10668800,1024,2.74536669254303,6.275834560394287,3637662,29.178130834000626,124670.83723406687,0.1350708305835724,0,bf16,0,269ba207e744573779747b593ffbcd8a87683fd55c83f6b168c658206d9aeb98,af33c522ea4349d5aae0491f7fe085d4bea1b044b08ff57eb064354bbb96435f,"{""climbmix"": {""documents_emitted"": 750, ""epoch"": 0, ""prepared_position"": 2482, ""rows_read"": 751, ""shard"": 0, ""shards"": 6543}, ""rewrite6"": {""documents_emitted"": 307, ""epoch"": 0, ""prepared_position"": 405, ""rows_read"": 308, ""shard"": 0, ""shards"": 1}, ""ultra_qa"": {""documents_emitted"": 163, ""epoch"": 0, ""prepared_position"": 712, ""rows_read"": 164, ""shard"": 0, ""shards"": 616}, ""ultra_style"": {""documents_emitted"": 204, ""epoch"": 0, ""prepared_position"": 720, ""rows_read"": 205, ""shard"": 0, ""shards"": 552}}"
|
| 16 |
unified-v1-evabyte-none-s42,ablation,causal,evabyte,none,42,100,0,complete,fresh_matched_seed,7230,7230,1.026456674999963,0.0,10668800,1024,2.70014488697052,5.0743002961663635,4841990,38.114180579000845,127039.06856829327,0.1096256747841835,0,bf16,0,269ba207e744573779747b593ffbcd8a87683fd55c83f6b168c658206d9aeb98,af33c522ea4349d5aae0491f7fe085d4bea1b044b08ff57eb064354bbb96435f,"{""climbmix"": {""documents_emitted"": 989, ""epoch"": 0, ""prepared_position"": 3299, ""rows_read"": 990, ""shard"": 0, ""shards"": 6543}, ""rewrite6"": {""documents_emitted"": 389, ""epoch"": 0, ""prepared_position"": 522, ""rows_read"": 390, ""shard"": 0, ""shards"": 1}, ""ultra_qa"": {""documents_emitted"": 217, ""epoch"": 0, ""prepared_position"": 950, ""rows_read"": 218, ""shard"": 0, ""shards"": 616}, ""ultra_style"": {""documents_emitted"": 273, ""epoch"": 0, ""prepared_position"": 959, ""rows_read"": 274, ""shard"": 0, ""shards"": 552}}"
|
|
|
|
|
|
| 14 |
unified-v1-evabyte-none-s42,ablation,causal,evabyte,none,42,50,0,running,fresh_matched_seed,3644,3644,1.026456674999963,0.0,10668800,1024,3.0752868056297302,6.275834560394287,2420984,20.154760339000404,120119.71163533424,0.12007386237382889,0,bf16,0,269ba207e744573779747b593ffbcd8a87683fd55c83f6b168c658206d9aeb98,af33c522ea4349d5aae0491f7fe085d4bea1b044b08ff57eb064354bbb96435f,"{""climbmix"": {""documents_emitted"": 530, ""epoch"": 0, ""prepared_position"": 1664, ""rows_read"": 530, ""shard"": 0, ""shards"": 6543}, ""rewrite6"": {""documents_emitted"": 214, ""epoch"": 0, ""prepared_position"": 273, ""rows_read"": 215, ""shard"": 0, ""shards"": 1}, ""ultra_qa"": {""documents_emitted"": 109, ""epoch"": 0, ""prepared_position"": 475, ""rows_read"": 110, ""shard"": 0, ""shards"": 616}, ""ultra_style"": {""documents_emitted"": 136, ""epoch"": 0, ""prepared_position"": 481, ""rows_read"": 137, ""shard"": 0, ""shards"": 552}}"
|
| 15 |
unified-v1-evabyte-none-s42,ablation,causal,evabyte,none,42,75,0,running,fresh_matched_seed,5450,5450,1.026456674999963,0.0,10668800,1024,2.74536669254303,6.275834560394287,3637662,29.178130834000626,124670.83723406687,0.1350708305835724,0,bf16,0,269ba207e744573779747b593ffbcd8a87683fd55c83f6b168c658206d9aeb98,af33c522ea4349d5aae0491f7fe085d4bea1b044b08ff57eb064354bbb96435f,"{""climbmix"": {""documents_emitted"": 750, ""epoch"": 0, ""prepared_position"": 2482, ""rows_read"": 751, ""shard"": 0, ""shards"": 6543}, ""rewrite6"": {""documents_emitted"": 307, ""epoch"": 0, ""prepared_position"": 405, ""rows_read"": 308, ""shard"": 0, ""shards"": 1}, ""ultra_qa"": {""documents_emitted"": 163, ""epoch"": 0, ""prepared_position"": 712, ""rows_read"": 164, ""shard"": 0, ""shards"": 616}, ""ultra_style"": {""documents_emitted"": 204, ""epoch"": 0, ""prepared_position"": 720, ""rows_read"": 205, ""shard"": 0, ""shards"": 552}}"
|
| 16 |
unified-v1-evabyte-none-s42,ablation,causal,evabyte,none,42,100,0,complete,fresh_matched_seed,7230,7230,1.026456674999963,0.0,10668800,1024,2.70014488697052,5.0743002961663635,4841990,38.114180579000845,127039.06856829327,0.1096256747841835,0,bf16,0,269ba207e744573779747b593ffbcd8a87683fd55c83f6b168c658206d9aeb98,af33c522ea4349d5aae0491f7fe085d4bea1b044b08ff57eb064354bbb96435f,"{""climbmix"": {""documents_emitted"": 989, ""epoch"": 0, ""prepared_position"": 3299, ""rows_read"": 990, ""shard"": 0, ""shards"": 6543}, ""rewrite6"": {""documents_emitted"": 389, ""epoch"": 0, ""prepared_position"": 522, ""rows_read"": 390, ""shard"": 0, ""shards"": 1}, ""ultra_qa"": {""documents_emitted"": 217, ""epoch"": 0, ""prepared_position"": 950, ""rows_read"": 218, ""shard"": 0, ""shards"": 616}, ""ultra_style"": {""documents_emitted"": 273, ""epoch"": 0, ""prepared_position"": 959, ""rows_read"": 274, ""shard"": 0, ""shards"": 552}}"
|
| 17 |
+
unified-v1-blt-none-s42,ablation,causal,blt,none,42,0,0,initialized,fresh_matched_seed,0,0,0.8857087290000436,19.13628118099996,10425600,1024,,,0,0.0,0.0,,0,,100,269ba207e744573779747b593ffbcd8a87683fd55c83f6b168c658206d9aeb98,af33c522ea4349d5aae0491f7fe085d4bea1b044b08ff57eb064354bbb96435f,"{""climbmix"": {""documents_emitted"": 0, ""epoch"": 0, ""prepared_position"": null, ""rows_read"": 0, ""shard"": 0, ""shards"": 6543}, ""rewrite6"": {""documents_emitted"": 0, ""epoch"": 0, ""prepared_position"": null, ""rows_read"": 0, ""shard"": 0, ""shards"": 1}, ""ultra_qa"": {""documents_emitted"": 0, ""epoch"": 0, ""prepared_position"": null, ""rows_read"": 0, ""shard"": 0, ""shards"": 616}, ""ultra_style"": {""documents_emitted"": 0, ""epoch"": 0, ""prepared_position"": null, ""rows_read"": 0, ""shard"": 0, ""shards"": 552}}"
|
unified/ablations/unified-v1-blt-none-s42/checkpoint-000000000000/COMPLETE
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
complete
|
unified/ablations/unified-v1-blt-none-s42/checkpoint-000000000000/metadata.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"format": "cortex-unified-v1",
|
| 3 |
+
"step": 0,
|
| 4 |
+
"lineage": "08a9fd42eadb43a3861b2c914ee6cbab",
|
| 5 |
+
"signature": "20afdd7a8d6a2f194a6a1772bd00d11fb1c26476d78881d3364c0bdd8421d331",
|
| 6 |
+
"sha256": "825adbb7a6662705a98219b3cc73321d13d64fe83c7f6c0d09283ec5bceec749",
|
| 7 |
+
"bytes": 42090107,
|
| 8 |
+
"saved_at": 1790265642.8075945
|
| 9 |
+
}
|
unified/ablations/unified-v1-blt-none-s42/checkpoint-000000000000/training.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:825adbb7a6662705a98219b3cc73321d13d64fe83c7f6c0d09283ec5bceec749
|
| 3 |
+
size 42090107
|
unified/ablations/unified-v1-blt-none-s42/inference/data_progress.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"step": 0,
|
| 3 |
+
"prepared_dataset": {
|
| 4 |
+
"repo": "appvoid/rewrite7",
|
| 5 |
+
"revision": "5b26a55d9021b2ab1e3e3ea35e4f0aba8bd5a930",
|
| 6 |
+
"recipe": "794a6de09517a367ca643fda6b9a2a511c975f0a62cb0aa3fbdd0321a6a34e80"
|
| 7 |
+
},
|
| 8 |
+
"observations": {
|
| 9 |
+
"presentations": 0,
|
| 10 |
+
"unique_samples": 0,
|
| 11 |
+
"ranges": {}
|
| 12 |
+
},
|
| 13 |
+
"coverage": {
|
| 14 |
+
"climbmix": {
|
| 15 |
+
"epoch": 0,
|
| 16 |
+
"shard": 0,
|
| 17 |
+
"shards": 6543,
|
| 18 |
+
"rows_read": 0,
|
| 19 |
+
"prepared_position": null,
|
| 20 |
+
"documents_emitted": 0
|
| 21 |
+
},
|
| 22 |
+
"rewrite6": {
|
| 23 |
+
"epoch": 0,
|
| 24 |
+
"shard": 0,
|
| 25 |
+
"shards": 1,
|
| 26 |
+
"rows_read": 0,
|
| 27 |
+
"prepared_position": null,
|
| 28 |
+
"documents_emitted": 0
|
| 29 |
+
},
|
| 30 |
+
"ultra_style": {
|
| 31 |
+
"epoch": 0,
|
| 32 |
+
"shard": 0,
|
| 33 |
+
"shards": 552,
|
| 34 |
+
"rows_read": 0,
|
| 35 |
+
"prepared_position": null,
|
| 36 |
+
"documents_emitted": 0
|
| 37 |
+
},
|
| 38 |
+
"ultra_qa": {
|
| 39 |
+
"epoch": 0,
|
| 40 |
+
"shard": 0,
|
| 41 |
+
"shards": 616,
|
| 42 |
+
"rows_read": 0,
|
| 43 |
+
"prepared_position": null,
|
| 44 |
+
"documents_emitted": 0
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
}
|
unified/ablations/unified-v1-blt-none-s42/inference/experiment_config.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"variant": "blt",
|
| 3 |
+
"config": {
|
| 4 |
+
"dim": 320,
|
| 5 |
+
"ff": 864,
|
| 6 |
+
"heads": 5,
|
| 7 |
+
"context": 1024,
|
| 8 |
+
"body_layers": 6,
|
| 9 |
+
"loops": 1,
|
| 10 |
+
"local_dim": 128,
|
| 11 |
+
"local_ff": 384,
|
| 12 |
+
"local_heads": 2,
|
| 13 |
+
"encoder_layers": 1,
|
| 14 |
+
"decoder_layers": 1,
|
| 15 |
+
"local_window": 128,
|
| 16 |
+
"query_block": 128,
|
| 17 |
+
"eva_window": 256,
|
| 18 |
+
"eva_chunk": 16,
|
| 19 |
+
"eva_heads": 8,
|
| 20 |
+
"hash_buckets": 1024,
|
| 21 |
+
"hash_sizes": [
|
| 22 |
+
3,
|
| 23 |
+
4,
|
| 24 |
+
5,
|
| 25 |
+
6,
|
| 26 |
+
7,
|
| 27 |
+
8
|
| 28 |
+
],
|
| 29 |
+
"cross_slots": 2,
|
| 30 |
+
"entropy_dim": 64,
|
| 31 |
+
"entropy_ff": 192,
|
| 32 |
+
"entropy_heads": 1,
|
| 33 |
+
"entropy_layers": 2,
|
| 34 |
+
"patch_size": 4.0,
|
| 35 |
+
"byteflow_bins": 16,
|
| 36 |
+
"byteflow_eps2": 1.0,
|
| 37 |
+
"byteflow_score": "l2",
|
| 38 |
+
"router_window": 64,
|
| 39 |
+
"grad_checkpoint": false
|
| 40 |
+
},
|
| 41 |
+
"arch": "blt",
|
| 42 |
+
"total_params": 10425600,
|
| 43 |
+
"trainable_params": 10285632,
|
| 44 |
+
"target_params": 10826304,
|
| 45 |
+
"depth": 7,
|
| 46 |
+
"frozen_params": 139968,
|
| 47 |
+
"param_delta_pct": -3.701207725184883,
|
| 48 |
+
"causal_ranking_eligible": true,
|
| 49 |
+
"fidelity": "scaled architecture",
|
| 50 |
+
"data_progress": {
|
| 51 |
+
"step": 0,
|
| 52 |
+
"prepared_dataset": {
|
| 53 |
+
"repo": "appvoid/rewrite7",
|
| 54 |
+
"revision": "5b26a55d9021b2ab1e3e3ea35e4f0aba8bd5a930",
|
| 55 |
+
"recipe": "794a6de09517a367ca643fda6b9a2a511c975f0a62cb0aa3fbdd0321a6a34e80"
|
| 56 |
+
},
|
| 57 |
+
"observations": {
|
| 58 |
+
"presentations": 0,
|
| 59 |
+
"unique_samples": 0,
|
| 60 |
+
"ranges": {}
|
| 61 |
+
},
|
| 62 |
+
"coverage": {
|
| 63 |
+
"climbmix": {
|
| 64 |
+
"epoch": 0,
|
| 65 |
+
"shard": 0,
|
| 66 |
+
"shards": 6543,
|
| 67 |
+
"rows_read": 0,
|
| 68 |
+
"prepared_position": null,
|
| 69 |
+
"documents_emitted": 0
|
| 70 |
+
},
|
| 71 |
+
"rewrite6": {
|
| 72 |
+
"epoch": 0,
|
| 73 |
+
"shard": 0,
|
| 74 |
+
"shards": 1,
|
| 75 |
+
"rows_read": 0,
|
| 76 |
+
"prepared_position": null,
|
| 77 |
+
"documents_emitted": 0
|
| 78 |
+
},
|
| 79 |
+
"ultra_style": {
|
| 80 |
+
"epoch": 0,
|
| 81 |
+
"shard": 0,
|
| 82 |
+
"shards": 552,
|
| 83 |
+
"rows_read": 0,
|
| 84 |
+
"prepared_position": null,
|
| 85 |
+
"documents_emitted": 0
|
| 86 |
+
},
|
| 87 |
+
"ultra_qa": {
|
| 88 |
+
"epoch": 0,
|
| 89 |
+
"shard": 0,
|
| 90 |
+
"shards": 616,
|
| 91 |
+
"rows_read": 0,
|
| 92 |
+
"prepared_position": null,
|
| 93 |
+
"documents_emitted": 0
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
}
|
unified/ablations/unified-v1-blt-none-s42/inference/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:df2b648db355f91327dbe8e2c259e618ff4861a1b186e89909fd2de52a2bab5e
|
| 3 |
+
size 41714820
|
unified/ablations/unified-v1-blt-none-s42/source/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Self-contained SparkBET-9M training runtime and Hugging Face export helpers."""
|
unified/ablations/unified-v1-blt-none-s42/source/audio_codec.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase-preserving spectral Braille codec, version 1.
|
| 2 |
+
|
| 3 |
+
Each quantized spectral byte is one Braille cell (its eight dots are eight bits).
|
| 4 |
+
Two binary raster planes, both <=80x80: 8-bit log magnitude and 8-bit phase.
|
| 5 |
+
This is a lossy, deterministic codec, not a neural audio tokenizer.
|
| 6 |
+
"""
|
| 7 |
+
import io,json,math
|
| 8 |
+
import numpy as np
|
| 9 |
+
from scipy import signal
|
| 10 |
+
from PIL import Image
|
| 11 |
+
from braille_encoder import binary_to_braille,local_contrast,floyd_steinberg
|
| 12 |
+
RATE=16000
|
| 13 |
+
NFFT=128
|
| 14 |
+
HOP=64
|
| 15 |
+
CHUNK=384 # 24 ms; 65 bins x 7 frames per plane fits in 80x48 pixels.
|
| 16 |
+
DB=80.0
|
| 17 |
+
|
| 18 |
+
def canonical_audio(value):
|
| 19 |
+
if isinstance(value,dict) and 'array' in value:
|
| 20 |
+
x=np.asarray(value['array'],dtype=np.float32);sr=int(value['sampling_rate'])
|
| 21 |
+
else:
|
| 22 |
+
import soundfile as sf
|
| 23 |
+
raw=value.get('bytes') if isinstance(value,dict) else None
|
| 24 |
+
path=value.get('path') if isinstance(value,dict) else value
|
| 25 |
+
x,sr=sf.read(io.BytesIO(raw) if raw is not None else path,dtype='float32',always_2d=False)
|
| 26 |
+
if x.ndim==2:x=x.mean(axis=1)
|
| 27 |
+
if x.ndim!=1 or not len(x) or sr<=0 or not np.isfinite(x).all():raise ValueError('Invalid waveform')
|
| 28 |
+
if len(x)>sr*120:raise ValueError('Audio exceeds 120-second decoding bound')
|
| 29 |
+
if sr!=RATE:
|
| 30 |
+
g=math.gcd(sr,RATE);x=signal.resample_poly(x,RATE//g,sr//g).astype(np.float32)
|
| 31 |
+
return np.clip(x,-1,1).astype(np.float32)
|
| 32 |
+
|
| 33 |
+
def byte_plane(values,width=40):
|
| 34 |
+
raw=np.asarray(values,dtype=np.uint8).reshape(-1)
|
| 35 |
+
rows=(len(raw)+width-1)//width
|
| 36 |
+
if rows*4>80 or width*2>80:raise ValueError('Spectral plane exceeds 80x80')
|
| 37 |
+
padded=np.pad(raw,(0,rows*width-len(raw)))
|
| 38 |
+
return '\n'.join(''.join(chr(0x2800+int(v)) for v in row) for row in padded.reshape(rows,width))
|
| 39 |
+
|
| 40 |
+
def read_plane(text,count):
|
| 41 |
+
lines=text.splitlines()
|
| 42 |
+
if not lines or len(lines)>20 or any(len(r)!=40 for r in lines):raise ValueError('Malformed Braille plane shape')
|
| 43 |
+
raw=[ord(c)-0x2800 for row in lines for c in row]
|
| 44 |
+
if any(v<0 or v>255 for v in raw) or len(raw)!=math.ceil(count/40)*40:raise ValueError('Malformed Braille payload')
|
| 45 |
+
if any(raw[count:]):raise ValueError('Nonzero spectral padding')
|
| 46 |
+
return np.array(raw[:count],dtype=np.uint8)
|
| 47 |
+
|
| 48 |
+
def encode_tile(wave):
|
| 49 |
+
x=np.asarray(wave,dtype=np.float32)
|
| 50 |
+
if x.ndim!=1 or not 1<=len(x)<=CHUNK or not np.isfinite(x).all():raise ValueError('Invalid tile')
|
| 51 |
+
n=len(x);padded=np.pad(x,(0,CHUNK-n))
|
| 52 |
+
_,_,z=signal.stft(padded,fs=RATE,window='hann',nperseg=NFFT,noverlap=NFFT-HOP,
|
| 53 |
+
nfft=NFFT,boundary='zeros',padded=True)
|
| 54 |
+
mag=np.abs(z);scale=max(float(mag.max()),1e-12)
|
| 55 |
+
# Code 0 is exact silence; 1..255 represent [-80,0] dB relative to scale.
|
| 56 |
+
db=20*np.log10(np.maximum(mag/scale,1e-12))
|
| 57 |
+
qm=np.where(db < -DB,0,1+np.rint((np.clip(db,-DB,0)+DB)*254/DB)).astype(np.uint8)
|
| 58 |
+
qp=(np.rint((np.angle(z)+np.pi)*(256/(2*np.pi))).astype(np.int32)%256).astype(np.uint8)
|
| 59 |
+
qp[qm==0]=128
|
| 60 |
+
meta=dict(v=1,sr=RATE,n=n,fft=NFFT,hop=HOP,frames=z.shape[1],scale=scale,db=DB)
|
| 61 |
+
return '<audio_tile>'+json.dumps(meta,separators=(',',':'))+'\nMAG\n'+byte_plane(qm)+'\nPHASE\n'+byte_plane(qp)+'\n</audio_tile>'
|
| 62 |
+
|
| 63 |
+
def decode_tile(text):
|
| 64 |
+
if not text.startswith('<audio_tile>') or not text.endswith('\n</audio_tile>'):raise ValueError('Incomplete audio tile')
|
| 65 |
+
header,planes=text[len('<audio_tile>'):].split('\nMAG\n',1)
|
| 66 |
+
meta=json.loads(header)
|
| 67 |
+
if (meta['v'],meta['sr'],meta['fft'],meta['hop'],meta['frames'],meta['db'])!=(1,RATE,NFFT,HOP,7,DB):raise ValueError('Unsupported codec parameters')
|
| 68 |
+
if not 1<=meta['n']<=CHUNK or not 0<float(meta['scale'])<=2:raise ValueError('Invalid audio metadata')
|
| 69 |
+
magtxt,phasetxt=planes[:-len('\n</audio_tile>')].split('\nPHASE\n')
|
| 70 |
+
shape=(NFFT//2+1,meta['frames']);count=math.prod(shape)
|
| 71 |
+
qm=read_plane(magtxt,count).reshape(shape).astype(np.float64)
|
| 72 |
+
qp=read_plane(phasetxt,count).reshape(shape).astype(np.float64)
|
| 73 |
+
mag=np.where(qm==0,0,float(meta['scale'])*10**(((qm-1)*DB/254-DB)/20))
|
| 74 |
+
phase=qp*(2*np.pi/256)-np.pi
|
| 75 |
+
z=mag*np.exp(1j*phase)
|
| 76 |
+
# Real-valued signal: DC and Nyquist bins must be real.
|
| 77 |
+
z[0]=z[0].real;z[-1]=z[-1].real
|
| 78 |
+
_,wave=signal.istft(z,fs=RATE,window='hann',nperseg=NFFT,noverlap=NFFT-HOP,nfft=NFFT,boundary=True)
|
| 79 |
+
return np.clip(wave[:meta['n']],-1,1).astype(np.float32),RATE
|
| 80 |
+
|
| 81 |
+
def encode_audio(wave):
|
| 82 |
+
return [encode_tile(wave[i:i+CHUNK]) for i in range(0,len(wave),CHUNK)]
|
| 83 |
+
|
| 84 |
+
def decode_audio(tiles):
|
| 85 |
+
if not tiles:raise ValueError('No tiles')
|
| 86 |
+
return np.concatenate([decode_tile(t)[0] for t in tiles]),RATE
|
| 87 |
+
|
| 88 |
+
def overview(wave,grid=80):
|
| 89 |
+
"""Whole-clip log spectrum for semantic tasks; never used as reversible audio."""
|
| 90 |
+
x=np.asarray(wave,dtype=np.float32);x=np.pad(x,(0,max(0,256-len(x))))
|
| 91 |
+
_,_,z=signal.stft(x,fs=RATE,nperseg=256,noverlap=128,boundary='zeros')
|
| 92 |
+
power=20*np.log10(np.maximum(np.abs(z),1e-6))
|
| 93 |
+
hi=float(power.max());image=np.clip((power-(hi-70))/70,0,1)
|
| 94 |
+
# Frequency high at top; time left to right. No saliency crop or phase resizing.
|
| 95 |
+
im=Image.fromarray(np.uint8((1-image[::-1])*255)).resize((grid,grid),Image.Resampling.BOX).convert('RGBA')
|
| 96 |
+
enhanced,alpha=local_contrast(im,amount=0)
|
| 97 |
+
return binary_to_braille(floyd_steinberg(enhanced,alpha),alpha)
|
| 98 |
+
|
| 99 |
+
def snr(reference,decoded):
|
| 100 |
+
x=np.asarray(reference,dtype=float);y=np.asarray(decoded,dtype=float)
|
| 101 |
+
return float(10*np.log10((np.sum(x*x)+1e-20)/(np.sum((x-y)**2)+1e-20)))
|
unified/ablations/unified-v1-blt-none-s42/source/bet_model.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SparkBET-9M: min-spark-style looped core with BET phase conditioning."""
|
| 2 |
+
import math, hashlib
|
| 3 |
+
from dataclasses import dataclass, asdict
|
| 4 |
+
import torch
|
| 5 |
+
from torch import nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torch.utils.checkpoint import checkpoint
|
| 8 |
+
|
| 9 |
+
SEQ_LEN = 1024
|
| 10 |
+
MAX_LOOPS = 8
|
| 11 |
+
PAD_ID, BOS_ID, EOS_ID = 256, 257, 258
|
| 12 |
+
VOCAB_SIZE = 259
|
| 13 |
+
EXPECTED_PARAM_COUNT = 9_353_876
|
| 14 |
+
EXPECTED_ARCH_SHAPE_SHA256 = "c85415fc50d03a23f89ccd870ebf53a3a00c5a61017c13a28f6444fdd71b72b9"
|
| 15 |
+
_USE_GRAD_CHECKPOINTING = False
|
| 16 |
+
_STATE_NOISE_SIGMA = 0.0
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def set_gradient_checkpointing(enabled):
|
| 20 |
+
global _USE_GRAD_CHECKPOINTING
|
| 21 |
+
_USE_GRAD_CHECKPOINTING = bool(enabled)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def set_state_noise_sigma(value):
|
| 25 |
+
global _STATE_NOISE_SIGMA
|
| 26 |
+
value = float(value)
|
| 27 |
+
if value < 0:
|
| 28 |
+
raise ValueError("state noise sigma must be >= 0")
|
| 29 |
+
_STATE_NOISE_SIGMA = value
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(frozen=True)
|
| 33 |
+
class BETConfig:
|
| 34 |
+
vocab_size: int = VOCAB_SIZE
|
| 35 |
+
hidden_size: int = 324
|
| 36 |
+
intermediate_size: int = 864
|
| 37 |
+
prelude_layers: int = 1
|
| 38 |
+
body_blocks: int = 6
|
| 39 |
+
coda_layers: int = 1
|
| 40 |
+
num_heads: int = 6
|
| 41 |
+
num_kv_heads: int = 2
|
| 42 |
+
head_dim: int = 54
|
| 43 |
+
lora_rank: int = 16
|
| 44 |
+
hyper_lanes: int = 2
|
| 45 |
+
max_seq_len: int = SEQ_LEN
|
| 46 |
+
max_loops: int = MAX_LOOPS
|
| 47 |
+
rope_theta: float = 10_000.0
|
| 48 |
+
rms_eps: float = 1e-6
|
| 49 |
+
ddl_beta_init: float = 1.0
|
| 50 |
+
ddl_k_eps: float = 1e-2
|
| 51 |
+
ddl_v_sigmoid_scale: float = 4.0
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def q_dim(self):
|
| 55 |
+
return self.num_heads * self.head_dim
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def kv_dim(self):
|
| 59 |
+
return self.num_kv_heads * self.head_dim
|
| 60 |
+
|
| 61 |
+
@property
|
| 62 |
+
def qkv_dim(self):
|
| 63 |
+
return self.q_dim + 2 * self.kv_dim
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
CFG = BETConfig()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class RMSNorm(nn.Module):
|
| 70 |
+
def __init__(self, dim, eps=1e-6):
|
| 71 |
+
super().__init__()
|
| 72 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 73 |
+
self.eps = eps
|
| 74 |
+
|
| 75 |
+
def forward(self, x):
|
| 76 |
+
dtype = x.dtype
|
| 77 |
+
y = x.float()
|
| 78 |
+
y = y * torch.rsqrt(y.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 79 |
+
return (y * self.weight.float()).to(dtype)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def rope_cos_sin(position_ids, dim, theta, dtype):
|
| 83 |
+
inv = 1.0 / (theta ** (torch.arange(0, dim, 2, device=position_ids.device, dtype=torch.float32) / dim))
|
| 84 |
+
f = position_ids.float().unsqueeze(-1) * inv
|
| 85 |
+
return f.cos().unsqueeze(1).to(dtype), f.sin().unsqueeze(1).to(dtype)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def apply_rope(x, cos, sin):
|
| 89 |
+
# RoPE tables may be prepared before the first autocast linear, when the
|
| 90 |
+
# embedding stream is FP32. Cast them to the projected Q/K dtype here so
|
| 91 |
+
# attention remains FP16 on every CUDA profile instead of being promoted.
|
| 92 |
+
cos, sin = cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
|
| 93 |
+
xe, xo = x[..., 0::2], x[..., 1::2]
|
| 94 |
+
return torch.stack((xe * cos - xo * sin, xe * sin + xo * cos), dim=-1).flatten(-2)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def attention_mask_and_positions(input_ids, attention_mask=None):
|
| 98 |
+
b, t = input_ids.shape
|
| 99 |
+
if attention_mask is None:
|
| 100 |
+
pos = torch.arange(t, device=input_ids.device).view(1, t).expand(b, t)
|
| 101 |
+
return None, pos
|
| 102 |
+
mask = attention_mask.to(device=input_ids.device, dtype=torch.bool)
|
| 103 |
+
if mask.shape != input_ids.shape:
|
| 104 |
+
raise ValueError(f"attention_mask shape {tuple(mask.shape)} != input_ids {tuple(input_ids.shape)}")
|
| 105 |
+
pos = (mask.long().cumsum(-1) - 1).clamp_min(0)
|
| 106 |
+
if bool(mask.all()):
|
| 107 |
+
return None, pos
|
| 108 |
+
causal = torch.ones((t, t), device=input_ids.device, dtype=torch.bool).tril()[None, None]
|
| 109 |
+
allowed = causal & mask[:, None, None, :]
|
| 110 |
+
return allowed, pos
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class Attention(nn.Module):
|
| 114 |
+
"""6Q/2KV GQA with per-head QK RMSNorm before RoPE."""
|
| 115 |
+
def __init__(self, c):
|
| 116 |
+
super().__init__()
|
| 117 |
+
if c.hidden_size != c.q_dim:
|
| 118 |
+
raise ValueError("hidden_size must equal num_heads * head_dim")
|
| 119 |
+
if c.num_heads % c.num_kv_heads:
|
| 120 |
+
raise ValueError("num_heads must be divisible by num_kv_heads")
|
| 121 |
+
self.qkv = nn.Linear(c.hidden_size, c.qkv_dim, bias=False)
|
| 122 |
+
self.out = nn.Linear(c.q_dim, c.hidden_size, bias=False)
|
| 123 |
+
self.qn = RMSNorm(c.head_dim, c.rms_eps)
|
| 124 |
+
self.kn = RMSNorm(c.head_dim, c.rms_eps)
|
| 125 |
+
self.nh, self.nkv, self.hd = c.num_heads, c.num_kv_heads, c.head_dim
|
| 126 |
+
self.q_dim, self.kv_dim = c.q_dim, c.kv_dim
|
| 127 |
+
|
| 128 |
+
def forward(self, x, cos, sin, qkv_delta=None, attn_mask=None):
|
| 129 |
+
b, t, _ = x.shape
|
| 130 |
+
qkv = self.qkv(x)
|
| 131 |
+
if qkv_delta is not None:
|
| 132 |
+
if qkv_delta.shape != qkv.shape:
|
| 133 |
+
raise RuntimeError("phase LoRA QKV delta shape mismatch")
|
| 134 |
+
qkv = qkv + qkv_delta
|
| 135 |
+
q, k, v = qkv.split([self.q_dim, self.kv_dim, self.kv_dim], dim=-1)
|
| 136 |
+
q = q.view(b, t, self.nh, self.hd).transpose(1, 2)
|
| 137 |
+
k = k.view(b, t, self.nkv, self.hd).transpose(1, 2)
|
| 138 |
+
v = v.view(b, t, self.nkv, self.hd).transpose(1, 2)
|
| 139 |
+
q = apply_rope(self.qn(q), cos, sin)
|
| 140 |
+
k = apply_rope(self.kn(k), cos, sin)
|
| 141 |
+
if self.nkv != self.nh:
|
| 142 |
+
repeat = self.nh // self.nkv
|
| 143 |
+
k = k.repeat_interleave(repeat, dim=1)
|
| 144 |
+
v = v.repeat_interleave(repeat, dim=1)
|
| 145 |
+
if attn_mask is None:
|
| 146 |
+
z = F.scaled_dot_product_attention(q, k, v, is_causal=True, dropout_p=0.0)
|
| 147 |
+
else:
|
| 148 |
+
z = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, dropout_p=0.0)
|
| 149 |
+
return self.out(z.transpose(1, 2).contiguous().view(b, t, self.q_dim))
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class SwiGLU(nn.Module):
|
| 153 |
+
def __init__(self, c):
|
| 154 |
+
super().__init__()
|
| 155 |
+
self.gate_up = nn.Linear(c.hidden_size, 2 * c.intermediate_size, bias=False)
|
| 156 |
+
self.down = nn.Linear(c.intermediate_size, c.hidden_size, bias=False)
|
| 157 |
+
|
| 158 |
+
def forward(self, x):
|
| 159 |
+
gate, up = self.gate_up(x).chunk(2, dim=-1)
|
| 160 |
+
return self.down(F.silu(gate) * up)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class DeepDeltaResidual(nn.Module):
|
| 164 |
+
"""Scalar Deep-Delta residual update used only in the shared recurrent body."""
|
| 165 |
+
def __init__(self, c):
|
| 166 |
+
super().__init__()
|
| 167 |
+
self.k_eps = c.ddl_k_eps
|
| 168 |
+
self.v_sigmoid_scale = c.ddl_v_sigmoid_scale
|
| 169 |
+
self.beta_init = c.ddl_beta_init
|
| 170 |
+
self.beta = nn.Linear(c.hidden_size, 1, bias=True)
|
| 171 |
+
self.v_proj = nn.Linear(c.hidden_size, 1, bias=True)
|
| 172 |
+
|
| 173 |
+
@torch.no_grad()
|
| 174 |
+
def reset_beta_bias(self):
|
| 175 |
+
p = min(max(self.beta_init, 0.0), 2.0) / 2.0
|
| 176 |
+
p = min(max(p, 1e-6), 1.0 - 1e-6)
|
| 177 |
+
self.beta.bias.fill_(math.log(p) - math.log(1.0 - p))
|
| 178 |
+
|
| 179 |
+
def forward(self, x, *, k_in, context):
|
| 180 |
+
d = k_in.size(-1)
|
| 181 |
+
eps_rms = (self.k_eps * self.k_eps) / d
|
| 182 |
+
k_rms = F.rms_norm(k_in, [d], eps=eps_rms)
|
| 183 |
+
scale = 1.0 / math.sqrt(d)
|
| 184 |
+
beta = 2.0 * torch.sigmoid(self.beta(context).float())
|
| 185 |
+
proj = torch.sum(k_rms * x, dim=-1, keepdim=True, dtype=torch.float32) * scale
|
| 186 |
+
v = torch.sigmoid(self.v_proj(x).float()) * self.v_sigmoid_scale
|
| 187 |
+
delta = ((beta * (v - proj)) * scale).to(dtype=x.dtype)
|
| 188 |
+
return x + delta * k_rms
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class PlainBlock(nn.Module):
|
| 192 |
+
def __init__(self, c):
|
| 193 |
+
super().__init__()
|
| 194 |
+
self.attn_norm = RMSNorm(c.hidden_size, c.rms_eps)
|
| 195 |
+
self.attn = Attention(c)
|
| 196 |
+
self.ffn_norm = RMSNorm(c.hidden_size, c.rms_eps)
|
| 197 |
+
self.ffn = SwiGLU(c)
|
| 198 |
+
|
| 199 |
+
def forward(self, x, cos, sin, attn_mask):
|
| 200 |
+
x = x + self.attn(self.attn_norm(x), cos, sin, attn_mask=attn_mask)
|
| 201 |
+
return x + self.ffn(self.ffn_norm(x))
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
class ContinuousLoopConditioner(nn.Module):
|
| 205 |
+
def __init__(self, d):
|
| 206 |
+
super().__init__()
|
| 207 |
+
self.net = nn.Sequential(nn.Linear(8, d), nn.SiLU(), nn.Linear(d, 2 * d))
|
| 208 |
+
|
| 209 |
+
@staticmethod
|
| 210 |
+
def features(t, dt, device, dtype):
|
| 211 |
+
return torch.tensor([
|
| 212 |
+
t, dt,
|
| 213 |
+
math.sin(math.pi * t), math.cos(math.pi * t),
|
| 214 |
+
math.sin(2 * math.pi * t), math.cos(2 * math.pi * t),
|
| 215 |
+
math.log(max(dt, 1e-6)), math.log(max(1.0 - t + dt, 1e-6)),
|
| 216 |
+
], device=device, dtype=dtype)
|
| 217 |
+
|
| 218 |
+
def parameters_for(self, t, dt, device, dtype):
|
| 219 |
+
f = self.features(t, dt, device, dtype)
|
| 220 |
+
scale, shift = self.net(f).chunk(2, dim=-1)
|
| 221 |
+
return f, scale, shift
|
| 222 |
+
|
| 223 |
+
@staticmethod
|
| 224 |
+
def modulate(x, scale, shift):
|
| 225 |
+
return x * (1.0 + 0.1 * scale) + 0.1 * shift
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class PhaseLoRA(nn.Module):
|
| 229 |
+
def __init__(self, c):
|
| 230 |
+
super().__init__()
|
| 231 |
+
self.down = nn.Linear(c.hidden_size, c.lora_rank, bias=False)
|
| 232 |
+
self.up = nn.Linear(c.lora_rank, c.qkv_dim, bias=False)
|
| 233 |
+
self.gate = nn.Linear(8, c.lora_rank, bias=True)
|
| 234 |
+
|
| 235 |
+
def forward(self, x, phase_features):
|
| 236 |
+
gate = (2.0 * torch.sigmoid(self.gate(phase_features).float())).to(dtype=x.dtype)
|
| 237 |
+
return self.up(self.down(x) * gate)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
class LoopedBlock(nn.Module):
|
| 241 |
+
def __init__(self, c):
|
| 242 |
+
super().__init__()
|
| 243 |
+
self.attn_norm = RMSNorm(c.hidden_size, c.rms_eps)
|
| 244 |
+
self.attn = Attention(c)
|
| 245 |
+
self.phase_lora = PhaseLoRA(c)
|
| 246 |
+
self.ddl_attn = DeepDeltaResidual(c)
|
| 247 |
+
self.ffn_norm = RMSNorm(c.hidden_size, c.rms_eps)
|
| 248 |
+
self.ffn = SwiGLU(c)
|
| 249 |
+
self.ddl_ffn = DeepDeltaResidual(c)
|
| 250 |
+
|
| 251 |
+
def forward(self, x, cos, sin, phase_scale, phase_shift, phase_features, attn_mask):
|
| 252 |
+
attn_context = ContinuousLoopConditioner.modulate(x, phase_scale, phase_shift)
|
| 253 |
+
qkv_delta = self.phase_lora(attn_context, phase_features)
|
| 254 |
+
x_norm = self.attn_norm(attn_context)
|
| 255 |
+
x = self.ddl_attn(
|
| 256 |
+
x,
|
| 257 |
+
k_in=self.attn(x_norm, cos, sin, qkv_delta=qkv_delta, attn_mask=attn_mask),
|
| 258 |
+
context=x_norm,
|
| 259 |
+
)
|
| 260 |
+
ffn_context = ContinuousLoopConditioner.modulate(x, phase_scale, phase_shift)
|
| 261 |
+
x_norm = self.ffn_norm(ffn_context)
|
| 262 |
+
return self.ddl_ffn(x, k_in=self.ffn(x_norm), context=x_norm)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class LoopHyperConnection(nn.Module):
|
| 266 |
+
"""Two persistent loop lanes with per-budget read/mix/write scalars."""
|
| 267 |
+
def __init__(self, c):
|
| 268 |
+
super().__init__()
|
| 269 |
+
self.k = int(c.hyper_lanes)
|
| 270 |
+
self.max_loops = int(c.max_loops)
|
| 271 |
+
if self.k != 2:
|
| 272 |
+
raise ValueError("SparkBET is defined for two Hyper-Connection lanes")
|
| 273 |
+
shape = (self.max_loops, self.max_loops)
|
| 274 |
+
self.alpha = nn.Parameter(torch.zeros(*shape, self.k))
|
| 275 |
+
self.mix = nn.Parameter(torch.zeros(*shape, self.k, self.k))
|
| 276 |
+
self.beta = nn.Parameter(torch.zeros(*shape, self.k))
|
| 277 |
+
self.reset_parameters()
|
| 278 |
+
|
| 279 |
+
@torch.no_grad()
|
| 280 |
+
def reset_parameters(self):
|
| 281 |
+
self.alpha.zero_(); self.mix.zero_(); self.beta.zero_()
|
| 282 |
+
eye = torch.eye(self.k, device=self.mix.device, dtype=self.mix.dtype)
|
| 283 |
+
for budget in range(1, self.max_loops + 1):
|
| 284 |
+
b = budget - 1
|
| 285 |
+
for i in range(budget):
|
| 286 |
+
self.alpha[b, i, i % self.k] = 1.0
|
| 287 |
+
self.mix[b, i].copy_(eye)
|
| 288 |
+
self.beta[b, i].fill_(1.0)
|
| 289 |
+
|
| 290 |
+
def init_lanes(self, prelude_state):
|
| 291 |
+
return prelude_state.unsqueeze(0).expand(self.k, *prelude_state.shape)
|
| 292 |
+
|
| 293 |
+
def read(self, lanes, loops, iteration):
|
| 294 |
+
a = self.alpha[loops - 1, iteration].to(dtype=lanes.dtype)
|
| 295 |
+
return torch.einsum("k,kbtd->btd", a, lanes)
|
| 296 |
+
|
| 297 |
+
def write(self, lanes, branch_delta, loops, iteration):
|
| 298 |
+
m = self.mix[loops - 1, iteration].to(dtype=lanes.dtype)
|
| 299 |
+
b = self.beta[loops - 1, iteration].to(dtype=lanes.dtype)
|
| 300 |
+
mixed = torch.einsum("kj,jbtd->kbtd", m, lanes)
|
| 301 |
+
return mixed + b[:, None, None, None] * branch_delta.unsqueeze(0)
|
| 302 |
+
|
| 303 |
+
@staticmethod
|
| 304 |
+
def pool(lanes):
|
| 305 |
+
return lanes.mean(dim=0)
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
class SparkBET(nn.Module):
|
| 309 |
+
"""Prelude -> six shared recurrent blocks -> coda, with exact loop budgets 1..8."""
|
| 310 |
+
def __init__(self, c=CFG):
|
| 311 |
+
super().__init__()
|
| 312 |
+
self.c = c
|
| 313 |
+
self.embed = nn.Embedding(c.vocab_size, c.hidden_size)
|
| 314 |
+
self.prelude = nn.ModuleList([PlainBlock(c) for _ in range(c.prelude_layers)])
|
| 315 |
+
self.body = nn.ModuleList([LoopedBlock(c) for _ in range(c.body_blocks)])
|
| 316 |
+
self.time_cond = ContinuousLoopConditioner(c.hidden_size)
|
| 317 |
+
self.loop_hyper = LoopHyperConnection(c)
|
| 318 |
+
self.coda = nn.ModuleList([PlainBlock(c) for _ in range(c.coda_layers)])
|
| 319 |
+
self.final_norm = RMSNorm(c.hidden_size, c.rms_eps)
|
| 320 |
+
self.apply(self._generic_init)
|
| 321 |
+
self._mechanism_init()
|
| 322 |
+
|
| 323 |
+
@staticmethod
|
| 324 |
+
def _generic_init(m):
|
| 325 |
+
if isinstance(m, nn.Linear):
|
| 326 |
+
nn.init.normal_(m.weight, 0.0, 0.02)
|
| 327 |
+
if m.bias is not None: nn.init.zeros_(m.bias)
|
| 328 |
+
elif isinstance(m, nn.Embedding):
|
| 329 |
+
nn.init.normal_(m.weight, 0.0, 0.02)
|
| 330 |
+
|
| 331 |
+
@torch.no_grad()
|
| 332 |
+
def _mechanism_init(self):
|
| 333 |
+
nn.init.zeros_(self.time_cond.net[-1].weight)
|
| 334 |
+
nn.init.zeros_(self.time_cond.net[-1].bias)
|
| 335 |
+
self.loop_hyper.reset_parameters()
|
| 336 |
+
for block in self.body:
|
| 337 |
+
nn.init.zeros_(block.phase_lora.up.weight)
|
| 338 |
+
nn.init.zeros_(block.phase_lora.gate.weight)
|
| 339 |
+
nn.init.zeros_(block.phase_lora.gate.bias)
|
| 340 |
+
for block in [*self.prelude, *self.body, *self.coda]:
|
| 341 |
+
nn.init.zeros_(block.attn.out.weight)
|
| 342 |
+
nn.init.zeros_(block.ffn.down.weight)
|
| 343 |
+
for block in self.body:
|
| 344 |
+
block.ddl_attn.reset_beta_bias(); block.ddl_ffn.reset_beta_bias()
|
| 345 |
+
|
| 346 |
+
def _run_plain(self, block, x, cos, sin, attn_mask):
|
| 347 |
+
if _USE_GRAD_CHECKPOINTING and self.training:
|
| 348 |
+
return checkpoint(block, x, cos, sin, attn_mask, use_reentrant=False)
|
| 349 |
+
return block(x, cos, sin, attn_mask)
|
| 350 |
+
|
| 351 |
+
def _run_looped(self, block, x, cos, sin, scale, shift, features, attn_mask):
|
| 352 |
+
if _USE_GRAD_CHECKPOINTING and self.training:
|
| 353 |
+
return checkpoint(block, x, cos, sin, scale, shift, features, attn_mask, use_reentrant=False)
|
| 354 |
+
return block(x, cos, sin, scale, shift, features, attn_mask)
|
| 355 |
+
|
| 356 |
+
def _readout(self, x, cos, sin, attn_mask):
|
| 357 |
+
h = x
|
| 358 |
+
for block in self.coda:
|
| 359 |
+
h = self._run_plain(block, h, cos, sin, attn_mask)
|
| 360 |
+
h = self.final_norm(h)
|
| 361 |
+
return F.linear(h, self.embed.weight)
|
| 362 |
+
|
| 363 |
+
@staticmethod
|
| 364 |
+
def _schedule(step_sizes):
|
| 365 |
+
if isinstance(step_sizes, int):
|
| 366 |
+
n = int(step_sizes)
|
| 367 |
+
step_sizes = uniform_steps(n)
|
| 368 |
+
if not step_sizes:
|
| 369 |
+
raise ValueError("empty refinement schedule")
|
| 370 |
+
values = [float(v) for v in step_sizes]
|
| 371 |
+
if any(v <= 0 for v in values):
|
| 372 |
+
raise ValueError("refinement strides must be positive")
|
| 373 |
+
if abs(sum(values) - 1.0) > 1e-5:
|
| 374 |
+
raise ValueError("refinement strides must sum to 1")
|
| 375 |
+
return values
|
| 376 |
+
|
| 377 |
+
def _prepare(self, input_ids, step_sizes, attention_mask=None):
|
| 378 |
+
steps = self._schedule(step_sizes)
|
| 379 |
+
loops = len(steps)
|
| 380 |
+
if loops > self.c.max_loops:
|
| 381 |
+
raise ValueError(f"loops {loops} > configured max_loops {self.c.max_loops}")
|
| 382 |
+
if input_ids.shape[1] > self.c.max_seq_len:
|
| 383 |
+
raise ValueError("context exceeds max_seq_len")
|
| 384 |
+
x = self.embed(input_ids)
|
| 385 |
+
attn_mask, pos = attention_mask_and_positions(input_ids, attention_mask)
|
| 386 |
+
cos, sin = rope_cos_sin(pos, self.c.head_dim, self.c.rope_theta, x.dtype)
|
| 387 |
+
for block in self.prelude:
|
| 388 |
+
x = self._run_plain(block, x, cos, sin, attn_mask)
|
| 389 |
+
lanes = self.loop_hyper.init_lanes(x)
|
| 390 |
+
shared_noise = torch.randn_like(x) if self.training and _STATE_NOISE_SIGMA > 0 else None
|
| 391 |
+
return steps, lanes, shared_noise, cos, sin, attn_mask
|
| 392 |
+
|
| 393 |
+
def _advance(self, lanes, steps, iteration, elapsed, shared_noise, cos, sin, attn_mask):
|
| 394 |
+
dt = steps[iteration]
|
| 395 |
+
t_mid = elapsed + 0.5 * dt
|
| 396 |
+
features, scale, shift = self.time_cond.parameters_for(t_mid, dt, lanes.device, lanes.dtype)
|
| 397 |
+
branch_input = self.loop_hyper.read(lanes, len(steps), iteration)
|
| 398 |
+
if shared_noise is not None:
|
| 399 |
+
t_end = elapsed + dt
|
| 400 |
+
sigma = _STATE_NOISE_SIGMA * max(0.0, 1.0 - t_end)
|
| 401 |
+
if sigma:
|
| 402 |
+
branch_input = branch_input + sigma * shared_noise
|
| 403 |
+
h = branch_input
|
| 404 |
+
for block in self.body:
|
| 405 |
+
h = self._run_looped(block, h, cos, sin, scale, shift, features, attn_mask)
|
| 406 |
+
return self.loop_hyper.write(lanes, h - branch_input, len(steps), iteration)
|
| 407 |
+
|
| 408 |
+
def _run_recurrence(self, input_ids, step_sizes, attention_mask=None, collect_states=False):
|
| 409 |
+
steps, lanes, noise, cos, sin, attn_mask = self._prepare(input_ids, step_sizes, attention_mask)
|
| 410 |
+
states = [] if collect_states else None
|
| 411 |
+
elapsed = 0.0
|
| 412 |
+
for i, dt in enumerate(steps):
|
| 413 |
+
lanes = self._advance(lanes, steps, i, elapsed, noise, cos, sin, attn_mask)
|
| 414 |
+
elapsed += dt
|
| 415 |
+
if collect_states: states.append(self.loop_hyper.pool(lanes))
|
| 416 |
+
return self.loop_hyper.pool(lanes), states, cos, sin, attn_mask
|
| 417 |
+
|
| 418 |
+
def forward(self, input_ids, step_sizes=None, attention_mask=None):
|
| 419 |
+
if step_sizes is None: step_sizes = uniform_steps(self.c.max_loops)
|
| 420 |
+
x, _, cos, sin, attn_mask = self._run_recurrence(input_ids, step_sizes, attention_mask, False)
|
| 421 |
+
return self._readout(x, cos, sin, attn_mask)
|
| 422 |
+
|
| 423 |
+
def forward_loop_exits(self, input_ids, step_sizes=None, attention_mask=None):
|
| 424 |
+
if step_sizes is None: step_sizes = uniform_steps(self.c.max_loops)
|
| 425 |
+
_, states, cos, sin, attn_mask = self._run_recurrence(input_ids, step_sizes, attention_mask, True)
|
| 426 |
+
return [self._readout(h, cos, sin, attn_mask) for h in states]
|
| 427 |
+
|
| 428 |
+
def count_params(self):
|
| 429 |
+
return sum(p.numel() for p in self.parameters())
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
# Preserve the old trainer/import name while changing the implementation.
|
| 433 |
+
BETFog = SparkBET
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def uniform_steps(n):
|
| 437 |
+
n = int(n)
|
| 438 |
+
if not 1 <= n <= MAX_LOOPS:
|
| 439 |
+
raise ValueError(f"loop budget must be in [1,{MAX_LOOPS}]")
|
| 440 |
+
return [1.0 / n] * n
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def architecture_shape_signature(model):
|
| 444 |
+
lines = [f"{k}:{tuple(v.shape)}:{v.dtype}" for k, v in model.state_dict().items()]
|
| 445 |
+
return hashlib.sha256("\n".join(lines).encode()).hexdigest()
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
@torch.no_grad()
|
| 449 |
+
def verify_architecture(model, rank=0):
|
| 450 |
+
expected = dict(
|
| 451 |
+
vocab_size=259, hidden_size=324, intermediate_size=864,
|
| 452 |
+
prelude_layers=1, body_blocks=6, coda_layers=1,
|
| 453 |
+
num_heads=6, num_kv_heads=2, head_dim=54,
|
| 454 |
+
lora_rank=16, hyper_lanes=2, max_seq_len=1024, max_loops=8,
|
| 455 |
+
rope_theta=10_000.0, rms_eps=1e-6,
|
| 456 |
+
ddl_beta_init=1.0, ddl_k_eps=1e-2, ddl_v_sigmoid_scale=4.0,
|
| 457 |
+
)
|
| 458 |
+
actual = asdict(model.c)
|
| 459 |
+
for k, v in expected.items():
|
| 460 |
+
if actual[k] != v:
|
| 461 |
+
raise AssertionError(f"Architecture drift: {k}={actual[k]} expected {v}")
|
| 462 |
+
if model.count_params() != EXPECTED_PARAM_COUNT:
|
| 463 |
+
raise AssertionError(f"Parameter drift: {model.count_params():,} != {EXPECTED_PARAM_COUNT:,}")
|
| 464 |
+
sig = architecture_shape_signature(model)
|
| 465 |
+
if sig != EXPECTED_ARCH_SHAPE_SHA256:
|
| 466 |
+
raise AssertionError(f"Architecture SHA drift: {sig} != {EXPECTED_ARCH_SHAPE_SHA256}")
|
| 467 |
+
if not torch.allclose(model.time_cond.net[-1].weight, torch.zeros_like(model.time_cond.net[-1].weight)):
|
| 468 |
+
raise AssertionError("time conditioner must start as identity")
|
| 469 |
+
for i, block in enumerate(model.body):
|
| 470 |
+
if not torch.allclose(block.phase_lora.up.weight, torch.zeros_like(block.phase_lora.up.weight)):
|
| 471 |
+
raise AssertionError(f"phase LoRA {i} up projection must start zero")
|
| 472 |
+
for name, ddl in (("attn", block.ddl_attn), ("ffn", block.ddl_ffn)):
|
| 473 |
+
beta = (2.0 * torch.sigmoid(ddl.beta.bias.float())).item()
|
| 474 |
+
if abs(beta - 1.0) > 1e-6:
|
| 475 |
+
raise AssertionError(f"body {i} {name} DDL beta init={beta}")
|
| 476 |
+
hc = model.loop_hyper
|
| 477 |
+
eye = torch.eye(hc.k, device=hc.mix.device, dtype=hc.mix.dtype)
|
| 478 |
+
for budget in range(1, model.c.max_loops + 1):
|
| 479 |
+
for i in range(budget):
|
| 480 |
+
expected_alpha = torch.zeros_like(hc.alpha[budget - 1, i]); expected_alpha[i % hc.k] = 1
|
| 481 |
+
if not torch.allclose(hc.alpha[budget - 1, i], expected_alpha): raise AssertionError("Hyper alpha init drift")
|
| 482 |
+
if not torch.allclose(hc.mix[budget - 1, i], eye): raise AssertionError("Hyper mix init drift")
|
| 483 |
+
if not torch.allclose(hc.beta[budget - 1, i], torch.ones_like(hc.beta[budget - 1, i])): raise AssertionError("Hyper beta init drift")
|
| 484 |
+
if rank == 0:
|
| 485 |
+
print("[verify] SparkBET architecture PASSED")
|
| 486 |
+
print(f"[verify] params: {model.count_params():,}")
|
| 487 |
+
print(f"[verify] physical blocks: 1 prelude + 6 shared body + 1 coda; L8 applications=50")
|
| 488 |
+
print(f"[verify] architecture SHA256: {sig}")
|
unified/ablations/unified-v1-blt-none-s42/source/braille_encoder.py
ADDED
|
@@ -0,0 +1,1532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Supported input sources
|
| 4 |
+
-----------------------
|
| 5 |
+
1. Single image file
|
| 6 |
+
2. Folder of images
|
| 7 |
+
3. CSV / TSV with an image path column
|
| 8 |
+
4. JSON / JSONL with an image/path field
|
| 9 |
+
5. Parquet with an image path column
|
| 10 |
+
6. Hugging Face dataset repo via datasets.load_dataset(...)
|
| 11 |
+
7. Hugging Face dataset already saved locally via datasets.load_from_disk(...)
|
| 12 |
+
|
| 13 |
+
Supported output formats
|
| 14 |
+
------------------------
|
| 15 |
+
- txt : single-image Braille text
|
| 16 |
+
- jsonl : dataset rows
|
| 17 |
+
- csv : dataset rows
|
| 18 |
+
- parquet : dataset rows
|
| 19 |
+
- hf-disk : datasets.Dataset.save_to_disk(...)
|
| 20 |
+
- hf-push : datasets.Dataset.push_to_hub(...)
|
| 21 |
+
|
| 22 |
+
Dependencies
|
| 23 |
+
------------
|
| 24 |
+
Core:
|
| 25 |
+
pip install pillow numpy opencv-python
|
| 26 |
+
|
| 27 |
+
Dataset features:
|
| 28 |
+
pip install datasets pandas pyarrow huggingface_hub
|
| 29 |
+
|
| 30 |
+
Single image usage
|
| 31 |
+
------------------
|
| 32 |
+
python smart_braille_encoder_v12.py input.png
|
| 33 |
+
python smart_braille_encoder_v12.py input.png --save-debug
|
| 34 |
+
|
| 35 |
+
Dataset usage
|
| 36 |
+
-------------
|
| 37 |
+
python smart_braille_encoder_v12.py ./images --dataset-out out.jsonl
|
| 38 |
+
python smart_braille_encoder_v12.py metadata.csv --image-column image_path --dataset-out out.parquet
|
| 39 |
+
python smart_braille_encoder_v12.py --hf-dataset user/dataset --split train --dataset-out out.jsonl
|
| 40 |
+
python smart_braille_encoder_v12.py --hf-dataset user/dataset --split train --hf-out braille_ds
|
| 41 |
+
python smart_braille_encoder_v12.py --hf-dataset user/dataset --split train --push-to-hub user/my-braille-dataset
|
| 42 |
+
|
| 43 |
+
Notes
|
| 44 |
+
-----
|
| 45 |
+
- The exported dataset includes:
|
| 46 |
+
braille_text
|
| 47 |
+
braille_lines
|
| 48 |
+
braille_codepoints
|
| 49 |
+
braille_flat_codepoints
|
| 50 |
+
decision metadata
|
| 51 |
+
- This makes it easy to train your own “ASCII tokenized” / Braille-tokenized models.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
from __future__ import annotations
|
| 55 |
+
|
| 56 |
+
import argparse
|
| 57 |
+
import io
|
| 58 |
+
import json
|
| 59 |
+
from pathlib import Path
|
| 60 |
+
from typing import Any
|
| 61 |
+
|
| 62 |
+
import cv2
|
| 63 |
+
import numpy as np
|
| 64 |
+
from PIL import Image, ImageDraw, ImageFilter
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ============================================================
|
| 68 |
+
# Utility
|
| 69 |
+
# ============================================================
|
| 70 |
+
|
| 71 |
+
IMAGE_EXTENSIONS = {
|
| 72 |
+
".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tif", ".tiff"
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def clamp(v: int, lo: int, hi: int) -> int:
|
| 77 |
+
return max(lo, min(v, hi))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def robust_norm_masked(
|
| 81 |
+
x: np.ndarray,
|
| 82 |
+
mask: np.ndarray | None = None,
|
| 83 |
+
low: float = 1.0,
|
| 84 |
+
high: float = 99.0,
|
| 85 |
+
) -> np.ndarray:
|
| 86 |
+
x = np.asarray(x, dtype=np.float32)
|
| 87 |
+
|
| 88 |
+
if mask is None:
|
| 89 |
+
vals = x.ravel()
|
| 90 |
+
else:
|
| 91 |
+
vals = x[mask.astype(bool)]
|
| 92 |
+
|
| 93 |
+
if vals.size == 0:
|
| 94 |
+
return np.zeros_like(x, dtype=np.float32)
|
| 95 |
+
|
| 96 |
+
lo = np.percentile(vals, low)
|
| 97 |
+
hi = np.percentile(vals, high)
|
| 98 |
+
|
| 99 |
+
out = np.clip((x - lo) / (hi - lo + 1e-6), 0.0, 1.0)
|
| 100 |
+
|
| 101 |
+
if mask is not None:
|
| 102 |
+
out *= mask.astype(np.float32)
|
| 103 |
+
|
| 104 |
+
return out.astype(np.float32)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def is_image_path(path: str | Path) -> bool:
|
| 108 |
+
return Path(path).suffix.lower() in IMAGE_EXTENSIONS
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def jsonable(value: Any) -> Any:
|
| 112 |
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
| 113 |
+
return value
|
| 114 |
+
if isinstance(value, Path):
|
| 115 |
+
return str(value)
|
| 116 |
+
if isinstance(value, (list, dict)):
|
| 117 |
+
return value
|
| 118 |
+
if isinstance(value, tuple):
|
| 119 |
+
return list(value)
|
| 120 |
+
if isinstance(value, np.generic):
|
| 121 |
+
return value.item()
|
| 122 |
+
return str(value)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# ============================================================
|
| 126 |
+
# Load / alpha handling
|
| 127 |
+
# ============================================================
|
| 128 |
+
|
| 129 |
+
def load_image(path: str | Path) -> Image.Image:
|
| 130 |
+
return Image.open(path).convert("RGBA")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def alpha_mask(
|
| 134 |
+
image: Image.Image,
|
| 135 |
+
alpha_threshold: int = 8,
|
| 136 |
+
) -> np.ndarray:
|
| 137 |
+
rgba = np.asarray(image.convert("RGBA"), dtype=np.uint8)
|
| 138 |
+
return rgba[..., 3] >= alpha_threshold
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def visible_bbox(
|
| 142 |
+
image: Image.Image,
|
| 143 |
+
alpha_threshold: int = 8,
|
| 144 |
+
padding_ratio: float = 0.02,
|
| 145 |
+
) -> tuple[int, int, int, int]:
|
| 146 |
+
mask = alpha_mask(image, alpha_threshold)
|
| 147 |
+
h, w = mask.shape
|
| 148 |
+
ys, xs = np.where(mask)
|
| 149 |
+
|
| 150 |
+
if xs.size == 0:
|
| 151 |
+
return (0, 0, w, h)
|
| 152 |
+
|
| 153 |
+
x0 = int(xs.min())
|
| 154 |
+
y0 = int(ys.min())
|
| 155 |
+
x1 = int(xs.max()) + 1
|
| 156 |
+
y1 = int(ys.max()) + 1
|
| 157 |
+
|
| 158 |
+
bw = x1 - x0
|
| 159 |
+
bh = y1 - y0
|
| 160 |
+
pad = int(round(max(bw, bh) * padding_ratio))
|
| 161 |
+
|
| 162 |
+
return (
|
| 163 |
+
max(0, x0 - pad),
|
| 164 |
+
max(0, y0 - pad),
|
| 165 |
+
min(w, x1 + pad),
|
| 166 |
+
min(h, y1 + pad),
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ============================================================
|
| 171 |
+
# Attention analysis
|
| 172 |
+
# ============================================================
|
| 173 |
+
|
| 174 |
+
def compute_maps(
|
| 175 |
+
image: Image.Image,
|
| 176 |
+
alpha_threshold: int = 8,
|
| 177 |
+
) -> dict:
|
| 178 |
+
rgba = np.asarray(image.convert("RGBA"), dtype=np.float32)
|
| 179 |
+
|
| 180 |
+
rgb = rgba[..., :3]
|
| 181 |
+
alpha_u8 = rgba[..., 3]
|
| 182 |
+
alpha = alpha_u8 / 255.0
|
| 183 |
+
|
| 184 |
+
visible = alpha_u8 >= alpha_threshold
|
| 185 |
+
visible_f = visible.astype(np.float32)
|
| 186 |
+
|
| 187 |
+
gray = (
|
| 188 |
+
0.299 * rgb[..., 0]
|
| 189 |
+
+ 0.587 * rgb[..., 1]
|
| 190 |
+
+ 0.114 * rgb[..., 2]
|
| 191 |
+
).astype(np.float32)
|
| 192 |
+
|
| 193 |
+
h, w = gray.shape
|
| 194 |
+
|
| 195 |
+
if not np.any(visible):
|
| 196 |
+
z = np.zeros((h, w), dtype=np.float32)
|
| 197 |
+
return {
|
| 198 |
+
"attention": z,
|
| 199 |
+
"edge_energy": z,
|
| 200 |
+
"colorfulness": z,
|
| 201 |
+
"visible": visible,
|
| 202 |
+
"alpha": alpha,
|
| 203 |
+
"gray": gray,
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
|
| 207 |
+
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
|
| 208 |
+
edge_energy = np.hypot(gx, gy)
|
| 209 |
+
|
| 210 |
+
edges = robust_norm_masked(edge_energy, visible, 5, 99.5)
|
| 211 |
+
|
| 212 |
+
local_contrast = np.zeros_like(gray)
|
| 213 |
+
for sigma in (2, 8, 24):
|
| 214 |
+
blur = cv2.GaussianBlur(gray, (0, 0), sigma)
|
| 215 |
+
local_contrast += np.abs(gray - blur)
|
| 216 |
+
local_contrast /= 3.0
|
| 217 |
+
local_contrast = robust_norm_masked(local_contrast, visible, 5, 99)
|
| 218 |
+
|
| 219 |
+
median_luma = np.median(gray[visible])
|
| 220 |
+
luma_surprise = robust_norm_masked(np.abs(gray - median_luma), visible, 5, 99)
|
| 221 |
+
|
| 222 |
+
median_rgb = np.median(rgb[visible], axis=0)
|
| 223 |
+
color_distance = np.linalg.norm(rgb - median_rgb, axis=2)
|
| 224 |
+
color_surprise = robust_norm_masked(color_distance, visible, 5, 99)
|
| 225 |
+
|
| 226 |
+
bins = 32
|
| 227 |
+
bin_id = np.clip((gray / 256.0 * bins).astype(np.int32), 0, bins - 1)
|
| 228 |
+
hist = np.bincount(bin_id[visible].ravel(), minlength=bins).astype(np.float32)
|
| 229 |
+
prob = hist / (hist.sum() + 1e-9)
|
| 230 |
+
rarity = -np.log(prob[bin_id] + 1e-9)
|
| 231 |
+
rarity = robust_norm_masked(rarity, visible, 1, 99)
|
| 232 |
+
|
| 233 |
+
raw = (
|
| 234 |
+
0.18 * edges
|
| 235 |
+
+ 0.20 * local_contrast
|
| 236 |
+
+ 0.26 * luma_surprise
|
| 237 |
+
+ 0.20 * color_surprise
|
| 238 |
+
+ 0.16 * rarity
|
| 239 |
+
)
|
| 240 |
+
raw *= visible_f
|
| 241 |
+
|
| 242 |
+
blurred_raw = cv2.GaussianBlur(raw, (0, 0), 10)
|
| 243 |
+
blurred_vis = cv2.GaussianBlur(visible_f, (0, 0), 10)
|
| 244 |
+
coherent = blurred_raw / (blurred_vis + 1e-6)
|
| 245 |
+
coherent = robust_norm_masked(coherent, visible, 1, 99.5)
|
| 246 |
+
|
| 247 |
+
yy, xx = np.mgrid[0:h, 0:w]
|
| 248 |
+
cx = w / 2.0
|
| 249 |
+
cy = h / 2.0
|
| 250 |
+
sx = w * 0.48
|
| 251 |
+
sy = h * 0.40
|
| 252 |
+
|
| 253 |
+
center = np.exp(
|
| 254 |
+
-(((xx - cx) ** 2) / (2 * sx ** 2) + ((yy - cy) ** 2) / (2 * sy ** 2))
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
attention = coherent * (0.84 + 0.16 * center)
|
| 258 |
+
attention *= visible_f
|
| 259 |
+
attention = robust_norm_masked(attention, visible, 0, 99.5)
|
| 260 |
+
|
| 261 |
+
rgb_u8 = np.clip(rgb, 0, 255).astype(np.uint8)
|
| 262 |
+
hsv = cv2.cvtColor(rgb_u8, cv2.COLOR_RGB2HSV).astype(np.float32)
|
| 263 |
+
|
| 264 |
+
saturation = hsv[..., 1] / 255.0
|
| 265 |
+
value = hsv[..., 2] / 255.0
|
| 266 |
+
colorfulness = saturation * (0.35 + 0.65 * value)
|
| 267 |
+
colorfulness *= visible_f
|
| 268 |
+
colorfulness = robust_norm_masked(colorfulness, visible, 2, 99)
|
| 269 |
+
|
| 270 |
+
return {
|
| 271 |
+
"attention": attention.astype(np.float32),
|
| 272 |
+
"edge_energy": (edge_energy * visible_f).astype(np.float32),
|
| 273 |
+
"colorfulness": colorfulness.astype(np.float32),
|
| 274 |
+
"visible": visible,
|
| 275 |
+
"alpha": alpha.astype(np.float32),
|
| 276 |
+
"gray": gray,
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
# ============================================================
|
| 281 |
+
# Saliency geometry
|
| 282 |
+
# ============================================================
|
| 283 |
+
|
| 284 |
+
def salient_mask(
|
| 285 |
+
attention: np.ndarray,
|
| 286 |
+
visible: np.ndarray,
|
| 287 |
+
percentile: float = 87.0,
|
| 288 |
+
) -> np.ndarray:
|
| 289 |
+
vals = attention[visible]
|
| 290 |
+
if vals.size == 0:
|
| 291 |
+
return np.zeros_like(attention, dtype=np.uint8)
|
| 292 |
+
|
| 293 |
+
threshold = np.percentile(vals, percentile)
|
| 294 |
+
mask = ((attention >= threshold) & visible).astype(np.uint8)
|
| 295 |
+
kernel = np.ones((5, 5), np.uint8)
|
| 296 |
+
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
| 297 |
+
mask *= visible.astype(np.uint8)
|
| 298 |
+
return mask
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def bbox_from_mask(
|
| 302 |
+
mask: np.ndarray,
|
| 303 |
+
fallback: tuple[int, int, int, int],
|
| 304 |
+
) -> tuple[int, int, int, int]:
|
| 305 |
+
ys, xs = np.where(mask > 0)
|
| 306 |
+
if xs.size == 0:
|
| 307 |
+
return fallback
|
| 308 |
+
|
| 309 |
+
return (
|
| 310 |
+
int(xs.min()),
|
| 311 |
+
int(ys.min()),
|
| 312 |
+
int(xs.max()) + 1,
|
| 313 |
+
int(ys.max()) + 1,
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def border_spread_in_visible_frame(
|
| 318 |
+
mask: np.ndarray,
|
| 319 |
+
visible_box: tuple[int, int, int, int],
|
| 320 |
+
border_ratio: float = 0.08,
|
| 321 |
+
) -> dict:
|
| 322 |
+
x0, y0, x1, y1 = visible_box
|
| 323 |
+
local = mask[y0:y1, x0:x1]
|
| 324 |
+
h, w = local.shape
|
| 325 |
+
|
| 326 |
+
if h <= 0 or w <= 0:
|
| 327 |
+
return {k: 0.0 for k in ("left", "right", "top", "bottom", "tl", "tr", "bl", "br")}
|
| 328 |
+
|
| 329 |
+
total = float(local.sum()) + 1e-9
|
| 330 |
+
|
| 331 |
+
bx = max(1, int(w * border_ratio))
|
| 332 |
+
by = max(1, int(h * border_ratio))
|
| 333 |
+
ch = max(1, int(h * 0.18))
|
| 334 |
+
cw = max(1, int(w * 0.18))
|
| 335 |
+
|
| 336 |
+
return {
|
| 337 |
+
"left": float(local[:, :bx].sum() / total),
|
| 338 |
+
"right": float(local[:, w - bx:].sum() / total),
|
| 339 |
+
"top": float(local[:by, :].sum() / total),
|
| 340 |
+
"bottom": float(local[h - by:, :].sum() / total),
|
| 341 |
+
"tl": float(local[:ch, :cw].sum() / total),
|
| 342 |
+
"tr": float(local[:ch, w - cw:].sum() / total),
|
| 343 |
+
"bl": float(local[h - ch:, :cw].sum() / total),
|
| 344 |
+
"br": float(local[h - ch:, w - cw:].sum() / total),
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def features_span_visible_frame(
|
| 349 |
+
mask: np.ndarray,
|
| 350 |
+
salient_box: tuple[int, int, int, int],
|
| 351 |
+
visible_box: tuple[int, int, int, int],
|
| 352 |
+
side_presence_threshold: float = 0.012,
|
| 353 |
+
bbox_span_threshold: float = 0.88,
|
| 354 |
+
) -> tuple[bool, dict]:
|
| 355 |
+
vx0, vy0, vx1, vy1 = visible_box
|
| 356 |
+
sx0, sy0, sx1, sy1 = salient_box
|
| 357 |
+
|
| 358 |
+
vw = max(vx1 - vx0, 1)
|
| 359 |
+
vh = max(vy1 - vy0, 1)
|
| 360 |
+
|
| 361 |
+
span_x = (sx1 - sx0) / vw
|
| 362 |
+
span_y = (sy1 - sy0) / vh
|
| 363 |
+
|
| 364 |
+
spread = border_spread_in_visible_frame(mask, visible_box)
|
| 365 |
+
|
| 366 |
+
all_sides = (
|
| 367 |
+
spread["left"] >= side_presence_threshold
|
| 368 |
+
and spread["right"] >= side_presence_threshold
|
| 369 |
+
and spread["top"] >= side_presence_threshold
|
| 370 |
+
and spread["bottom"] >= side_presence_threshold
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
corner_hits = sum(
|
| 374 |
+
spread[k] >= side_presence_threshold * 0.65
|
| 375 |
+
for k in ("tl", "tr", "bl", "br")
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
spans = span_x >= bbox_span_threshold and span_y >= bbox_span_threshold and all_sides
|
| 379 |
+
touches_four_corners = corner_hits == 4
|
| 380 |
+
|
| 381 |
+
return bool(spans or touches_four_corners), {
|
| 382 |
+
"span_x": float(span_x),
|
| 383 |
+
"span_y": float(span_y),
|
| 384 |
+
"corner_hits": int(corner_hits),
|
| 385 |
+
**spread,
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
# ============================================================
|
| 390 |
+
# Colorful center detector
|
| 391 |
+
# ============================================================
|
| 392 |
+
|
| 393 |
+
def detect_central_color_feature(
|
| 394 |
+
maps: dict,
|
| 395 |
+
visible_box: tuple[int, int, int, int],
|
| 396 |
+
) -> dict | None:
|
| 397 |
+
colorfulness = maps["colorfulness"]
|
| 398 |
+
attention = maps["attention"]
|
| 399 |
+
visible = maps["visible"]
|
| 400 |
+
|
| 401 |
+
vals = colorfulness[visible]
|
| 402 |
+
if vals.size == 0:
|
| 403 |
+
return None
|
| 404 |
+
|
| 405 |
+
threshold = max(float(np.percentile(vals, 72)), 0.22)
|
| 406 |
+
|
| 407 |
+
att_vals = attention[visible]
|
| 408 |
+
att_threshold = float(np.percentile(att_vals, 55)) if att_vals.size else 0.0
|
| 409 |
+
|
| 410 |
+
seed = ((colorfulness >= threshold) & visible).astype(np.uint8)
|
| 411 |
+
support = ((colorfulness >= threshold * 0.62) & (attention >= att_threshold) & visible).astype(np.uint8)
|
| 412 |
+
|
| 413 |
+
seed = cv2.morphologyEx(seed, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))
|
| 414 |
+
seed = cv2.dilate(seed, np.ones((3, 3), np.uint8), iterations=1)
|
| 415 |
+
|
| 416 |
+
candidate = ((seed > 0) & (support > 0)).astype(np.uint8)
|
| 417 |
+
|
| 418 |
+
n, labels, stats, centroids = cv2.connectedComponentsWithStats(candidate, connectivity=8)
|
| 419 |
+
if n <= 1:
|
| 420 |
+
return None
|
| 421 |
+
|
| 422 |
+
vx0, vy0, vx1, vy1 = visible_box
|
| 423 |
+
vis_w = max(vx1 - vx0, 1)
|
| 424 |
+
vis_h = max(vy1 - vy0, 1)
|
| 425 |
+
frame_cx = (vx0 + vx1) / 2.0
|
| 426 |
+
frame_cy = (vy0 + vy1) / 2.0
|
| 427 |
+
|
| 428 |
+
visible_area = float(visible.sum()) + 1e-9
|
| 429 |
+
total_color_mass = float((colorfulness * visible.astype(np.float32)).sum()) + 1e-9
|
| 430 |
+
|
| 431 |
+
best = None
|
| 432 |
+
|
| 433 |
+
for label in range(1, n):
|
| 434 |
+
area = int(stats[label, cv2.CC_STAT_AREA])
|
| 435 |
+
if area < max(8, int(visible_area * 0.002)):
|
| 436 |
+
continue
|
| 437 |
+
|
| 438 |
+
component = labels == label
|
| 439 |
+
cx, cy = centroids[label]
|
| 440 |
+
|
| 441 |
+
dx = (cx - frame_cx) / (vis_w / 2.0 + 1e-9)
|
| 442 |
+
dy = (cy - frame_cy) / (vis_h / 2.0 + 1e-9)
|
| 443 |
+
center_distance = float(np.sqrt(dx * dx + dy * dy))
|
| 444 |
+
|
| 445 |
+
if center_distance > 0.72:
|
| 446 |
+
continue
|
| 447 |
+
|
| 448 |
+
mean_color = float(colorfulness[component].mean())
|
| 449 |
+
mean_attention = float(attention[component].mean())
|
| 450 |
+
color_mass = float(colorfulness[component].sum())
|
| 451 |
+
dominance = color_mass / total_color_mass
|
| 452 |
+
area_ratio = area / visible_area
|
| 453 |
+
|
| 454 |
+
center_score = np.exp(-(center_distance ** 2) / (2 * 0.42 ** 2))
|
| 455 |
+
score = (
|
| 456 |
+
0.34 * mean_color
|
| 457 |
+
+ 0.24 * mean_attention
|
| 458 |
+
+ 0.22 * center_score
|
| 459 |
+
+ 0.14 * min(dominance * 2.0, 1.0)
|
| 460 |
+
+ 0.06 * min(area_ratio * 8.0, 1.0)
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
if best is None or score > best["score"]:
|
| 464 |
+
best = {
|
| 465 |
+
"score": float(score),
|
| 466 |
+
"mask": component.astype(np.uint8),
|
| 467 |
+
"centroid": (float(cx), float(cy)),
|
| 468 |
+
"center_distance": center_distance,
|
| 469 |
+
"mean_color": mean_color,
|
| 470 |
+
"mean_attention": mean_attention,
|
| 471 |
+
"dominance": float(dominance),
|
| 472 |
+
"area_ratio": float(area_ratio),
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
if best is None:
|
| 476 |
+
return None
|
| 477 |
+
if best["score"] < 0.44:
|
| 478 |
+
return None
|
| 479 |
+
if best["mean_color"] < 0.28:
|
| 480 |
+
return None
|
| 481 |
+
if best["mean_attention"] < 0.20:
|
| 482 |
+
return None
|
| 483 |
+
|
| 484 |
+
return best
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def color_outline_crop(
|
| 488 |
+
feature: dict,
|
| 489 |
+
image_size: tuple[int, int],
|
| 490 |
+
visible_box: tuple[int, int, int, int],
|
| 491 |
+
) -> tuple[tuple[int, int, int, int], dict]:
|
| 492 |
+
mask = feature["mask"].astype(np.uint8)
|
| 493 |
+
ys, xs = np.where(mask > 0)
|
| 494 |
+
|
| 495 |
+
if xs.size == 0:
|
| 496 |
+
return visible_box, {
|
| 497 |
+
"outline_layers": 0,
|
| 498 |
+
"layer_px": 0,
|
| 499 |
+
"object_scale": 1.0,
|
| 500 |
+
"expanded_mask": mask,
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
x0 = int(xs.min())
|
| 504 |
+
y0 = int(ys.min())
|
| 505 |
+
x1 = int(xs.max()) + 1
|
| 506 |
+
y1 = int(ys.max()) + 1
|
| 507 |
+
|
| 508 |
+
obj_side = max(x1 - x0, y1 - y0)
|
| 509 |
+
|
| 510 |
+
vx0, vy0, vx1, vy1 = visible_box
|
| 511 |
+
visible_side = max(vx1 - vx0, vy1 - vy0)
|
| 512 |
+
|
| 513 |
+
object_scale = obj_side / max(visible_side, 1)
|
| 514 |
+
layers = 3 if object_scale < 0.30 else 2
|
| 515 |
+
|
| 516 |
+
layer_px = max(2, int(round(obj_side * 0.045)))
|
| 517 |
+
kernel_size = layer_px * 2 + 1
|
| 518 |
+
|
| 519 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
|
| 520 |
+
|
| 521 |
+
expanded = mask.copy()
|
| 522 |
+
for _ in range(layers):
|
| 523 |
+
expanded = cv2.dilate(expanded, kernel, iterations=1)
|
| 524 |
+
|
| 525 |
+
eys, exs = np.where(expanded > 0)
|
| 526 |
+
|
| 527 |
+
ex0 = int(exs.min())
|
| 528 |
+
ey0 = int(eys.min())
|
| 529 |
+
ex1 = int(exs.max()) + 1
|
| 530 |
+
ey1 = int(eys.max()) + 1
|
| 531 |
+
|
| 532 |
+
side = max(ex1 - ex0, ey1 - ey0)
|
| 533 |
+
|
| 534 |
+
img_w, img_h = image_size
|
| 535 |
+
visible_w = vx1 - vx0
|
| 536 |
+
visible_h = vy1 - vy0
|
| 537 |
+
max_useful_side = max(visible_w, visible_h)
|
| 538 |
+
|
| 539 |
+
side = max(2, min(side, max_useful_side, min(img_w, img_h)))
|
| 540 |
+
|
| 541 |
+
cx = (ex0 + ex1) / 2.0
|
| 542 |
+
cy = (ey0 + ey1) / 2.0
|
| 543 |
+
|
| 544 |
+
sx0 = clamp(int(round(cx - side / 2)), 0, img_w - side)
|
| 545 |
+
sy0 = clamp(int(round(cy - side / 2)), 0, img_h - side)
|
| 546 |
+
|
| 547 |
+
return (
|
| 548 |
+
sx0,
|
| 549 |
+
sy0,
|
| 550 |
+
sx0 + side,
|
| 551 |
+
sy0 + side,
|
| 552 |
+
), {
|
| 553 |
+
"outline_layers": int(layers),
|
| 554 |
+
"layer_px": int(layer_px),
|
| 555 |
+
"object_scale": float(object_scale),
|
| 556 |
+
"expanded_mask": expanded,
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
# ============================================================
|
| 561 |
+
# Generic v6 crop geometry / evaluation
|
| 562 |
+
# ============================================================
|
| 563 |
+
|
| 564 |
+
def expanded_square_bbox(
|
| 565 |
+
salient_box: tuple[int, int, int, int],
|
| 566 |
+
visible_box: tuple[int, int, int, int],
|
| 567 |
+
image_size: tuple[int, int],
|
| 568 |
+
margin_ratio: float = 0.12,
|
| 569 |
+
) -> tuple[int, int, int, int]:
|
| 570 |
+
img_w, img_h = image_size
|
| 571 |
+
sx0, sy0, sx1, sy1 = salient_box
|
| 572 |
+
vx0, vy0, vx1, vy1 = visible_box
|
| 573 |
+
|
| 574 |
+
bw = sx1 - sx0
|
| 575 |
+
bh = sy1 - sy0
|
| 576 |
+
side = max(bw, bh)
|
| 577 |
+
|
| 578 |
+
side = int(round(side * (1.0 + 2.0 * margin_ratio)))
|
| 579 |
+
|
| 580 |
+
visible_w = vx1 - vx0
|
| 581 |
+
visible_h = vy1 - vy0
|
| 582 |
+
max_useful_side = max(visible_w, visible_h)
|
| 583 |
+
|
| 584 |
+
side = min(side, max_useful_side, min(img_w, img_h))
|
| 585 |
+
side = max(side, 2)
|
| 586 |
+
|
| 587 |
+
cx = (sx0 + sx1) / 2.0
|
| 588 |
+
cy = (sy0 + sy1) / 2.0
|
| 589 |
+
|
| 590 |
+
x0 = int(round(cx - side / 2))
|
| 591 |
+
y0 = int(round(cy - side / 2))
|
| 592 |
+
|
| 593 |
+
x0 = clamp(x0, 0, img_w - side)
|
| 594 |
+
y0 = clamp(y0, 0, img_h - side)
|
| 595 |
+
|
| 596 |
+
return (x0, y0, x0 + side, y0 + side)
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def evaluate_crop(
|
| 600 |
+
crop_box: tuple[int, int, int, int],
|
| 601 |
+
visible_box: tuple[int, int, int, int],
|
| 602 |
+
attention: np.ndarray,
|
| 603 |
+
edge_energy: np.ndarray,
|
| 604 |
+
visible: np.ndarray,
|
| 605 |
+
) -> dict:
|
| 606 |
+
x0, y0, x1, y1 = crop_box
|
| 607 |
+
vx0, vy0, vx1, vy1 = visible_box
|
| 608 |
+
|
| 609 |
+
visible_f = visible.astype(np.float32)
|
| 610 |
+
|
| 611 |
+
saliency_weighted = attention * visible_f
|
| 612 |
+
total_saliency = float(saliency_weighted.sum()) + 1e-9
|
| 613 |
+
crop_saliency = float(saliency_weighted[y0:y1, x0:x1].sum())
|
| 614 |
+
|
| 615 |
+
vals = attention[visible]
|
| 616 |
+
strong_threshold = np.percentile(vals, 90) if vals.size else 1.0
|
| 617 |
+
|
| 618 |
+
strong = attention * (attention >= strong_threshold) * visible_f
|
| 619 |
+
total_strong = float(strong.sum()) + 1e-9
|
| 620 |
+
crop_strong = float(strong[y0:y1, x0:x1].sum())
|
| 621 |
+
|
| 622 |
+
weighted_edges = edge_energy * (0.35 + 0.65 * attention) * visible_f
|
| 623 |
+
total_detail = float(weighted_edges.sum()) + 1e-9
|
| 624 |
+
crop_detail = float(weighted_edges[y0:y1, x0:x1].sum())
|
| 625 |
+
|
| 626 |
+
total_visible = float(visible_f.sum()) + 1e-9
|
| 627 |
+
crop_visible = float(visible_f[y0:y1, x0:x1].sum())
|
| 628 |
+
visible_coverage = crop_visible / total_visible
|
| 629 |
+
|
| 630 |
+
visible_w = vx1 - vx0
|
| 631 |
+
visible_h = vy1 - vy0
|
| 632 |
+
base_side = max(visible_w, visible_h)
|
| 633 |
+
crop_side = max(x1 - x0, y1 - y0)
|
| 634 |
+
resolution_gain = base_side / max(crop_side, 1)
|
| 635 |
+
|
| 636 |
+
return {
|
| 637 |
+
"saliency_coverage": float(crop_saliency / total_saliency),
|
| 638 |
+
"strong_saliency_coverage": float(crop_strong / total_strong),
|
| 639 |
+
"detail_coverage": float(crop_detail / total_detail),
|
| 640 |
+
"visible_pixel_coverage": float(visible_coverage),
|
| 641 |
+
"resolution_gain": float(resolution_gain),
|
| 642 |
+
"crop_ratio_of_visible_frame": float(crop_side / max(base_side, 1)),
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
def should_crop_v6(metrics: dict) -> tuple[bool, float, float]:
|
| 647 |
+
saliency_loss = 1.0 - metrics["saliency_coverage"]
|
| 648 |
+
strong_loss = 1.0 - metrics["strong_saliency_coverage"]
|
| 649 |
+
detail_loss = 1.0 - metrics["detail_coverage"]
|
| 650 |
+
|
| 651 |
+
gain = np.log2(max(metrics["resolution_gain"], 1.0))
|
| 652 |
+
benefit = 1.30 * gain
|
| 653 |
+
cost = 1.30 * strong_loss + 0.85 * saliency_loss + 0.95 * detail_loss
|
| 654 |
+
|
| 655 |
+
if metrics["strong_saliency_coverage"] < 0.93:
|
| 656 |
+
return False, float(benefit), float(cost)
|
| 657 |
+
if metrics["detail_coverage"] < 0.72:
|
| 658 |
+
return False, float(benefit), float(cost)
|
| 659 |
+
if metrics["resolution_gain"] < 1.12:
|
| 660 |
+
return False, float(benefit), float(cost)
|
| 661 |
+
|
| 662 |
+
return bool(benefit > cost), float(benefit), float(cost)
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
def should_crop_color_feature(
|
| 666 |
+
metrics: dict,
|
| 667 |
+
feature: dict,
|
| 668 |
+
) -> tuple[bool, float, float]:
|
| 669 |
+
saliency_loss = 1.0 - metrics["saliency_coverage"]
|
| 670 |
+
strong_loss = 1.0 - metrics["strong_saliency_coverage"]
|
| 671 |
+
detail_loss = 1.0 - metrics["detail_coverage"]
|
| 672 |
+
|
| 673 |
+
gain = np.log2(max(metrics["resolution_gain"], 1.0))
|
| 674 |
+
|
| 675 |
+
center_bonus = np.exp(-(feature["center_distance"] ** 2) / 0.18)
|
| 676 |
+
feature_bonus = (
|
| 677 |
+
0.34 * feature["mean_color"]
|
| 678 |
+
+ 0.28 * feature["mean_attention"]
|
| 679 |
+
+ 0.22 * min(feature["dominance"] * 2.0, 1.0)
|
| 680 |
+
+ 0.16 * center_bonus
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
benefit = 1.30 * gain + 0.45 * feature_bonus
|
| 684 |
+
cost = 1.22 * strong_loss + 0.72 * saliency_loss + 0.82 * detail_loss
|
| 685 |
+
|
| 686 |
+
if metrics["strong_saliency_coverage"] < 0.91:
|
| 687 |
+
return False, float(benefit), float(cost)
|
| 688 |
+
if metrics["detail_coverage"] < 0.66:
|
| 689 |
+
return False, float(benefit), float(cost)
|
| 690 |
+
if metrics["resolution_gain"] < 1.07:
|
| 691 |
+
return False, float(benefit), float(cost)
|
| 692 |
+
|
| 693 |
+
return bool(benefit > cost), float(benefit), float(cost)
|
| 694 |
+
|
| 695 |
+
|
| 696 |
+
# ============================================================
|
| 697 |
+
# View selection
|
| 698 |
+
# ============================================================
|
| 699 |
+
|
| 700 |
+
def select_view(
|
| 701 |
+
image: Image.Image,
|
| 702 |
+
alpha_threshold: int = 8,
|
| 703 |
+
saliency_percentile: float = 87.0,
|
| 704 |
+
margin_ratio: float = 0.12,
|
| 705 |
+
) -> dict:
|
| 706 |
+
w, h = image.size
|
| 707 |
+
|
| 708 |
+
vis_box = visible_bbox(image, alpha_threshold=alpha_threshold)
|
| 709 |
+
maps = compute_maps(image, alpha_threshold=alpha_threshold)
|
| 710 |
+
|
| 711 |
+
attention = maps["attention"]
|
| 712 |
+
edge_energy = maps["edge_energy"]
|
| 713 |
+
visible = maps["visible"]
|
| 714 |
+
|
| 715 |
+
if not np.any(visible):
|
| 716 |
+
return {
|
| 717 |
+
"mode": "empty",
|
| 718 |
+
"box": vis_box,
|
| 719 |
+
"visible_box": vis_box,
|
| 720 |
+
"attention": attention,
|
| 721 |
+
"mask": np.zeros((h, w), dtype=np.uint8),
|
| 722 |
+
"bbox": vis_box,
|
| 723 |
+
"maps": maps,
|
| 724 |
+
"spread": {},
|
| 725 |
+
"metrics": None,
|
| 726 |
+
"color_feature": None,
|
| 727 |
+
"outline_info": None,
|
| 728 |
+
"reason": "image has no visible pixels",
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
mask = salient_mask(attention, visible, percentile=saliency_percentile)
|
| 732 |
+
sal_box = bbox_from_mask(mask, fallback=vis_box)
|
| 733 |
+
|
| 734 |
+
spans, spread = features_span_visible_frame(mask, sal_box, vis_box)
|
| 735 |
+
|
| 736 |
+
if spans:
|
| 737 |
+
return {
|
| 738 |
+
"mode": "visible_full",
|
| 739 |
+
"box": vis_box,
|
| 740 |
+
"visible_box": vis_box,
|
| 741 |
+
"attention": attention,
|
| 742 |
+
"mask": mask,
|
| 743 |
+
"bbox": sal_box,
|
| 744 |
+
"maps": maps,
|
| 745 |
+
"spread": spread,
|
| 746 |
+
"metrics": None,
|
| 747 |
+
"color_feature": None,
|
| 748 |
+
"outline_info": None,
|
| 749 |
+
"reason": "v6 full-frame rule: salient features span the visible frame / all four sides or corners",
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
color_feature = detect_central_color_feature(maps, vis_box)
|
| 753 |
+
if color_feature is not None:
|
| 754 |
+
color_box, outline_info = color_outline_crop(color_feature, image.size, vis_box)
|
| 755 |
+
|
| 756 |
+
color_metrics = evaluate_crop(color_box, vis_box, attention, edge_energy, visible)
|
| 757 |
+
color_ok, benefit, cost = should_crop_color_feature(color_metrics, color_feature)
|
| 758 |
+
|
| 759 |
+
color_metrics["benefit"] = benefit
|
| 760 |
+
color_metrics["cost"] = cost
|
| 761 |
+
|
| 762 |
+
if color_ok:
|
| 763 |
+
return {
|
| 764 |
+
"mode": "color_outline_crop",
|
| 765 |
+
"box": color_box,
|
| 766 |
+
"visible_box": vis_box,
|
| 767 |
+
"attention": attention,
|
| 768 |
+
"mask": mask,
|
| 769 |
+
"bbox": sal_box,
|
| 770 |
+
"maps": maps,
|
| 771 |
+
"spread": spread,
|
| 772 |
+
"metrics": color_metrics,
|
| 773 |
+
"color_feature": color_feature,
|
| 774 |
+
"outline_info": outline_info,
|
| 775 |
+
"reason": f'central colorful feature detected after v6 full-frame check; kept {outline_info["outline_layers"]} outer outline layers',
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
candidate = expanded_square_bbox(sal_box, vis_box, image.size, margin_ratio=margin_ratio)
|
| 779 |
+
metrics = evaluate_crop(candidate, vis_box, attention, edge_energy, visible)
|
| 780 |
+
crop_ok, benefit, cost = should_crop_v6(metrics)
|
| 781 |
+
|
| 782 |
+
metrics["benefit"] = benefit
|
| 783 |
+
metrics["cost"] = cost
|
| 784 |
+
|
| 785 |
+
if crop_ok:
|
| 786 |
+
return {
|
| 787 |
+
"mode": "crop",
|
| 788 |
+
"box": candidate,
|
| 789 |
+
"visible_box": vis_box,
|
| 790 |
+
"attention": attention,
|
| 791 |
+
"mask": mask,
|
| 792 |
+
"bbox": sal_box,
|
| 793 |
+
"maps": maps,
|
| 794 |
+
"spread": spread,
|
| 795 |
+
"metrics": metrics,
|
| 796 |
+
"color_feature": color_feature,
|
| 797 |
+
"outline_info": None,
|
| 798 |
+
"reason": "v6 saliency/detail crop accepted because resolution gain exceeds weighted information loss",
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
return {
|
| 802 |
+
"mode": "visible_full",
|
| 803 |
+
"box": vis_box,
|
| 804 |
+
"visible_box": vis_box,
|
| 805 |
+
"attention": attention,
|
| 806 |
+
"mask": mask,
|
| 807 |
+
"bbox": sal_box,
|
| 808 |
+
"maps": maps,
|
| 809 |
+
"spread": spread,
|
| 810 |
+
"metrics": metrics,
|
| 811 |
+
"color_feature": color_feature,
|
| 812 |
+
"outline_info": None,
|
| 813 |
+
"reason": "transparent margins removed for free; v6 crop test rejected further cropping",
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
# ============================================================
|
| 818 |
+
# Fit selected box to 64x64
|
| 819 |
+
# ============================================================
|
| 820 |
+
|
| 821 |
+
def fit_box_to_square(
|
| 822 |
+
image: Image.Image,
|
| 823 |
+
box: tuple[int, int, int, int],
|
| 824 |
+
size: int = 64,
|
| 825 |
+
) -> Image.Image:
|
| 826 |
+
region = image.crop(box).convert("RGBA")
|
| 827 |
+
w, h = region.size
|
| 828 |
+
|
| 829 |
+
scale = min(size / max(w, 1), size / max(h, 1))
|
| 830 |
+
|
| 831 |
+
nw = max(1, int(round(w * scale)))
|
| 832 |
+
nh = max(1, int(round(h * scale)))
|
| 833 |
+
|
| 834 |
+
reduced = region.resize((nw, nh), Image.Resampling.LANCZOS)
|
| 835 |
+
|
| 836 |
+
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
| 837 |
+
|
| 838 |
+
ox = (size - nw) // 2
|
| 839 |
+
oy = (size - nh) // 2
|
| 840 |
+
|
| 841 |
+
canvas.alpha_composite(reduced, (ox, oy))
|
| 842 |
+
return canvas
|
| 843 |
+
|
| 844 |
+
|
| 845 |
+
# ============================================================
|
| 846 |
+
# Braille conversion
|
| 847 |
+
# ============================================================
|
| 848 |
+
|
| 849 |
+
BRAILLE_BITS = {
|
| 850 |
+
(0, 0): 0,
|
| 851 |
+
(0, 1): 1,
|
| 852 |
+
(0, 2): 2,
|
| 853 |
+
(1, 0): 3,
|
| 854 |
+
(1, 1): 4,
|
| 855 |
+
(1, 2): 5,
|
| 856 |
+
(0, 3): 6,
|
| 857 |
+
(1, 3): 7,
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
|
| 861 |
+
def local_contrast(
|
| 862 |
+
rgba_image: Image.Image,
|
| 863 |
+
amount: float = 1.5,
|
| 864 |
+
blur_radius: float = 2.4,
|
| 865 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 866 |
+
rgba = np.asarray(rgba_image.convert("RGBA"), dtype=np.float32)
|
| 867 |
+
rgb = rgba[..., :3]
|
| 868 |
+
alpha = rgba[..., 3]
|
| 869 |
+
|
| 870 |
+
gray = (
|
| 871 |
+
0.299 * rgb[..., 0]
|
| 872 |
+
+ 0.587 * rgb[..., 1]
|
| 873 |
+
+ 0.114 * rgb[..., 2]
|
| 874 |
+
).astype(np.float32)
|
| 875 |
+
|
| 876 |
+
gray_img = Image.fromarray(np.clip(gray, 0, 255).astype(np.uint8), mode="L")
|
| 877 |
+
local = np.asarray(gray_img.filter(ImageFilter.GaussianBlur(blur_radius)), dtype=np.float32)
|
| 878 |
+
|
| 879 |
+
enhanced = np.clip(gray + amount * (gray - local), 0, 255)
|
| 880 |
+
return enhanced, alpha
|
| 881 |
+
|
| 882 |
+
|
| 883 |
+
def floyd_steinberg(
|
| 884 |
+
arr: np.ndarray,
|
| 885 |
+
alpha: np.ndarray,
|
| 886 |
+
alpha_threshold: int = 8,
|
| 887 |
+
) -> np.ndarray:
|
| 888 |
+
work = arr.astype(np.float32).copy()
|
| 889 |
+
|
| 890 |
+
h, w = work.shape
|
| 891 |
+
out = np.full((h, w), 255, dtype=np.uint8)
|
| 892 |
+
visible = alpha >= alpha_threshold
|
| 893 |
+
|
| 894 |
+
for y in range(h):
|
| 895 |
+
for x in range(w):
|
| 896 |
+
if not visible[y, x]:
|
| 897 |
+
continue
|
| 898 |
+
|
| 899 |
+
old = work[y, x]
|
| 900 |
+
new = 255.0 if old >= 128 else 0.0
|
| 901 |
+
out[y, x] = int(new)
|
| 902 |
+
|
| 903 |
+
err = old - new
|
| 904 |
+
|
| 905 |
+
if x + 1 < w and visible[y, x + 1]:
|
| 906 |
+
work[y, x + 1] += err * 7 / 16
|
| 907 |
+
if y + 1 < h and x > 0 and visible[y + 1, x - 1]:
|
| 908 |
+
work[y + 1, x - 1] += err * 3 / 16
|
| 909 |
+
if y + 1 < h and visible[y + 1, x]:
|
| 910 |
+
work[y + 1, x] += err * 5 / 16
|
| 911 |
+
if y + 1 < h and x + 1 < w and visible[y + 1, x + 1]:
|
| 912 |
+
work[y + 1, x + 1] += err * 1 / 16
|
| 913 |
+
|
| 914 |
+
return out
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
def binary_to_braille(
|
| 918 |
+
binary: np.ndarray,
|
| 919 |
+
alpha: np.ndarray,
|
| 920 |
+
alpha_threshold: int = 8,
|
| 921 |
+
) -> str:
|
| 922 |
+
"""
|
| 923 |
+
64x64 binary source grid -> 32x16 Unicode Braille.
|
| 924 |
+
"""
|
| 925 |
+
h, w = binary.shape
|
| 926 |
+
|
| 927 |
+
if h % 4 != 0 or w % 2 != 0:
|
| 928 |
+
raise ValueError("Braille grid must be divisible by 4 vertically and 2 horizontally.")
|
| 929 |
+
|
| 930 |
+
lines = []
|
| 931 |
+
|
| 932 |
+
for cy in range(h // 4):
|
| 933 |
+
line = []
|
| 934 |
+
for cx in range(w // 2):
|
| 935 |
+
code = 0
|
| 936 |
+
any_visible = False
|
| 937 |
+
|
| 938 |
+
for sy in range(4):
|
| 939 |
+
for sx in range(2):
|
| 940 |
+
yy = cy * 4 + sy
|
| 941 |
+
xx = cx * 2 + sx
|
| 942 |
+
|
| 943 |
+
if alpha[yy, xx] < alpha_threshold:
|
| 944 |
+
continue
|
| 945 |
+
|
| 946 |
+
any_visible = True
|
| 947 |
+
|
| 948 |
+
if binary[yy, xx] < 128:
|
| 949 |
+
code |= 1 << BRAILLE_BITS[(sx, sy)]
|
| 950 |
+
|
| 951 |
+
if not any_visible:
|
| 952 |
+
line.append(" ")
|
| 953 |
+
else:
|
| 954 |
+
line.append(chr(0x2800 + code))
|
| 955 |
+
|
| 956 |
+
lines.append("".join(line))
|
| 957 |
+
|
| 958 |
+
return "\n".join(lines)
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
def braille_metadata(braille_text: str) -> dict:
|
| 962 |
+
lines = braille_text.splitlines()
|
| 963 |
+
codepoints_2d = [[ord(ch) for ch in line] for line in lines]
|
| 964 |
+
flat_codepoints = [cp for row in codepoints_2d for cp in row]
|
| 965 |
+
|
| 966 |
+
return {
|
| 967 |
+
"braille_lines": lines,
|
| 968 |
+
"braille_width_chars": max((len(line) for line in lines), default=0),
|
| 969 |
+
"braille_height_chars": len(lines),
|
| 970 |
+
"braille_codepoints": codepoints_2d,
|
| 971 |
+
"braille_flat_codepoints": flat_codepoints,
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
|
| 975 |
+
# ============================================================
|
| 976 |
+
# Full pipeline
|
| 977 |
+
# ============================================================
|
| 978 |
+
|
| 979 |
+
def encode_image(
|
| 980 |
+
image_path: str | Path | None = None,
|
| 981 |
+
image: Image.Image | None = None,
|
| 982 |
+
grid_size: int = 64,
|
| 983 |
+
alpha_threshold: int = 8,
|
| 984 |
+
) -> dict:
|
| 985 |
+
if image is None:
|
| 986 |
+
if image_path is None:
|
| 987 |
+
raise ValueError("Either image_path or image must be provided.")
|
| 988 |
+
image = load_image(image_path)
|
| 989 |
+
else:
|
| 990 |
+
image = image.convert("RGBA")
|
| 991 |
+
|
| 992 |
+
decision = select_view(image, alpha_threshold=alpha_threshold)
|
| 993 |
+
|
| 994 |
+
small = fit_box_to_square(image, decision["box"], size=grid_size)
|
| 995 |
+
|
| 996 |
+
enhanced, alpha = local_contrast(small)
|
| 997 |
+
binary = floyd_steinberg(enhanced, alpha, alpha_threshold=alpha_threshold)
|
| 998 |
+
braille = binary_to_braille(binary, alpha, alpha_threshold=alpha_threshold)
|
| 999 |
+
|
| 1000 |
+
return {
|
| 1001 |
+
"image": image,
|
| 1002 |
+
"decision": decision,
|
| 1003 |
+
"small": small,
|
| 1004 |
+
"enhanced": enhanced,
|
| 1005 |
+
"alpha_small": alpha,
|
| 1006 |
+
"binary": binary,
|
| 1007 |
+
"braille": braille,
|
| 1008 |
+
"grid_size": grid_size,
|
| 1009 |
+
}
|
| 1010 |
+
|
| 1011 |
+
|
| 1012 |
+
# ============================================================
|
| 1013 |
+
# Debug output
|
| 1014 |
+
# ============================================================
|
| 1015 |
+
|
| 1016 |
+
def save_debug(result: dict, out_dir: Path) -> None:
|
| 1017 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 1018 |
+
|
| 1019 |
+
image = result["image"]
|
| 1020 |
+
d = result["decision"]
|
| 1021 |
+
maps = d["maps"]
|
| 1022 |
+
|
| 1023 |
+
overlay = image.convert("RGBA").copy()
|
| 1024 |
+
draw = ImageDraw.Draw(overlay)
|
| 1025 |
+
|
| 1026 |
+
draw.rectangle(d["visible_box"], outline=(0, 128, 255, 255), width=5)
|
| 1027 |
+
draw.rectangle(d["bbox"], outline=(0, 255, 0, 255), width=3)
|
| 1028 |
+
draw.rectangle(d["box"], outline=(255, 0, 0, 255), width=4)
|
| 1029 |
+
|
| 1030 |
+
overlay.save(out_dir / "decision_overlay.png")
|
| 1031 |
+
|
| 1032 |
+
def save_map(filename: str, arr: np.ndarray):
|
| 1033 |
+
Image.fromarray(np.clip(arr * 255, 0, 255).astype(np.uint8), mode="L").save(out_dir / filename)
|
| 1034 |
+
|
| 1035 |
+
save_map("attention_map.png", maps["attention"])
|
| 1036 |
+
|
| 1037 |
+
weighted_detail = (
|
| 1038 |
+
maps["edge_energy"]
|
| 1039 |
+
* (0.35 + 0.65 * maps["attention"])
|
| 1040 |
+
* maps["visible"].astype(np.float32)
|
| 1041 |
+
)
|
| 1042 |
+
|
| 1043 |
+
if weighted_detail.max() > 0:
|
| 1044 |
+
weighted_detail = weighted_detail / (weighted_detail.max() + 1e-9)
|
| 1045 |
+
|
| 1046 |
+
save_map("weighted_detail_map.png", weighted_detail)
|
| 1047 |
+
save_map("colorfulness_map.png", maps["colorfulness"])
|
| 1048 |
+
|
| 1049 |
+
Image.fromarray(d["mask"] * 255, mode="L").save(out_dir / "salient_mask.png")
|
| 1050 |
+
|
| 1051 |
+
if d["color_feature"] is not None:
|
| 1052 |
+
Image.fromarray(d["color_feature"]["mask"] * 255, mode="L").save(out_dir / "central_color_feature.png")
|
| 1053 |
+
|
| 1054 |
+
if d["outline_info"] is not None and "expanded_mask" in d["outline_info"]:
|
| 1055 |
+
Image.fromarray(d["outline_info"]["expanded_mask"] * 255, mode="L").save(out_dir / "color_outline_expanded.png")
|
| 1056 |
+
|
| 1057 |
+
result["small"].save(out_dir / "selected_64x64.png")
|
| 1058 |
+
Image.fromarray(result["binary"], mode="L").save(out_dir / "binary_64x64.png")
|
| 1059 |
+
Image.fromarray(np.clip(result["alpha_small"], 0, 255).astype(np.uint8), mode="L").save(out_dir / "alpha_64x64.png")
|
| 1060 |
+
(out_dir / "braille.txt").write_text(result["braille"], encoding="utf-8")
|
| 1061 |
+
|
| 1062 |
+
|
| 1063 |
+
# ============================================================
|
| 1064 |
+
# Dataset ingestion helpers
|
| 1065 |
+
# ============================================================
|
| 1066 |
+
|
| 1067 |
+
def image_from_value(value: Any, base_dir: Path | None = None) -> tuple[Image.Image, str | None]:
|
| 1068 |
+
"""
|
| 1069 |
+
Supports:
|
| 1070 |
+
- local file path string
|
| 1071 |
+
- PIL Image
|
| 1072 |
+
- numpy array
|
| 1073 |
+
- dicts like {"path": ...} or {"bytes": ...}
|
| 1074 |
+
"""
|
| 1075 |
+
if isinstance(value, Image.Image):
|
| 1076 |
+
return value.convert("RGBA"), None
|
| 1077 |
+
|
| 1078 |
+
if isinstance(value, np.ndarray):
|
| 1079 |
+
return Image.fromarray(value).convert("RGBA"), None
|
| 1080 |
+
|
| 1081 |
+
if isinstance(value, dict):
|
| 1082 |
+
if "bytes" in value and value["bytes"] is not None:
|
| 1083 |
+
raw = value["bytes"]
|
| 1084 |
+
return Image.open(io.BytesIO(raw)).convert("RGBA"), value.get("path")
|
| 1085 |
+
for key in ("path", "file_name", "filename", "filepath", "image_path"):
|
| 1086 |
+
if key in value and value[key]:
|
| 1087 |
+
p = Path(value[key])
|
| 1088 |
+
if base_dir is not None and not p.is_absolute():
|
| 1089 |
+
p = base_dir / p
|
| 1090 |
+
return load_image(p), str(p)
|
| 1091 |
+
|
| 1092 |
+
if isinstance(value, (str, Path)):
|
| 1093 |
+
p = Path(value)
|
| 1094 |
+
if base_dir is not None and not p.is_absolute():
|
| 1095 |
+
p = base_dir / p
|
| 1096 |
+
return load_image(p), str(p)
|
| 1097 |
+
|
| 1098 |
+
raise ValueError(f"Unsupported image value type: {type(value)}")
|
| 1099 |
+
|
| 1100 |
+
|
| 1101 |
+
def iter_folder_images(folder: Path):
|
| 1102 |
+
for path in sorted(folder.rglob("*")):
|
| 1103 |
+
if path.is_file() and is_image_path(path):
|
| 1104 |
+
yield {
|
| 1105 |
+
"id": path.stem,
|
| 1106 |
+
"image_value": str(path),
|
| 1107 |
+
"source": str(path),
|
| 1108 |
+
"extra": {},
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
+
def iter_tabular_records(path: Path, image_column: str):
|
| 1113 |
+
import pandas as pd
|
| 1114 |
+
|
| 1115 |
+
suffix = path.suffix.lower()
|
| 1116 |
+
|
| 1117 |
+
if suffix == ".csv":
|
| 1118 |
+
df = pd.read_csv(path)
|
| 1119 |
+
elif suffix == ".tsv":
|
| 1120 |
+
df = pd.read_csv(path, sep="\t")
|
| 1121 |
+
elif suffix == ".parquet":
|
| 1122 |
+
df = pd.read_parquet(path)
|
| 1123 |
+
elif suffix == ".json":
|
| 1124 |
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
| 1125 |
+
if isinstance(raw, dict):
|
| 1126 |
+
raw = raw.get("data", raw.get("items", raw))
|
| 1127 |
+
if isinstance(raw, dict):
|
| 1128 |
+
raw = [raw]
|
| 1129 |
+
df = pd.DataFrame(raw)
|
| 1130 |
+
elif suffix == ".jsonl":
|
| 1131 |
+
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 1132 |
+
df = pd.DataFrame(rows)
|
| 1133 |
+
else:
|
| 1134 |
+
raise ValueError(f"Unsupported tabular file: {path}")
|
| 1135 |
+
|
| 1136 |
+
if image_column not in df.columns:
|
| 1137 |
+
fallback_cols = [c for c in df.columns if c.lower() in {"image", "image_path", "path", "file_name", "filename"}]
|
| 1138 |
+
if not fallback_cols:
|
| 1139 |
+
raise ValueError(f'Image column "{image_column}" not found.')
|
| 1140 |
+
image_column = fallback_cols[0]
|
| 1141 |
+
|
| 1142 |
+
base_dir = path.parent
|
| 1143 |
+
|
| 1144 |
+
for idx, row in df.iterrows():
|
| 1145 |
+
rec = row.to_dict()
|
| 1146 |
+
yield {
|
| 1147 |
+
"id": str(rec.get("id", idx)),
|
| 1148 |
+
"image_value": rec[image_column],
|
| 1149 |
+
"source": f"{path}:{idx}",
|
| 1150 |
+
"base_dir": base_dir,
|
| 1151 |
+
"extra": rec,
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
|
| 1155 |
+
def iter_hf_dataset_repo(dataset_name: str, split: str, config_name: str | None):
|
| 1156 |
+
try:
|
| 1157 |
+
from datasets import load_dataset
|
| 1158 |
+
except Exception as e:
|
| 1159 |
+
raise RuntimeError("datasets is required for --hf-dataset. Install with: pip install datasets") from e
|
| 1160 |
+
|
| 1161 |
+
kwargs = {}
|
| 1162 |
+
if config_name:
|
| 1163 |
+
kwargs["name"] = config_name
|
| 1164 |
+
|
| 1165 |
+
ds = load_dataset(dataset_name, **kwargs, split=split)
|
| 1166 |
+
|
| 1167 |
+
image_column = None
|
| 1168 |
+
for c in ds.column_names:
|
| 1169 |
+
if c.lower() in {"image", "img"}:
|
| 1170 |
+
image_column = c
|
| 1171 |
+
break
|
| 1172 |
+
if image_column is None:
|
| 1173 |
+
for c in ds.column_names:
|
| 1174 |
+
if "image" in c.lower():
|
| 1175 |
+
image_column = c
|
| 1176 |
+
break
|
| 1177 |
+
if image_column is None:
|
| 1178 |
+
raise ValueError("Could not find an image column in the HF dataset.")
|
| 1179 |
+
|
| 1180 |
+
for idx, row in enumerate(ds):
|
| 1181 |
+
yield {
|
| 1182 |
+
"id": str(row.get("id", idx)),
|
| 1183 |
+
"image_value": row[image_column],
|
| 1184 |
+
"source": f"{dataset_name}:{split}:{idx}",
|
| 1185 |
+
"extra": dict(row),
|
| 1186 |
+
}
|
| 1187 |
+
|
| 1188 |
+
|
| 1189 |
+
def iter_hf_local_disk(path: Path):
|
| 1190 |
+
try:
|
| 1191 |
+
from datasets import load_from_disk
|
| 1192 |
+
except Exception as e:
|
| 1193 |
+
raise RuntimeError("datasets is required for --hf-load-from-disk. Install with: pip install datasets") from e
|
| 1194 |
+
|
| 1195 |
+
ds = load_from_disk(str(path))
|
| 1196 |
+
|
| 1197 |
+
if hasattr(ds, "keys"):
|
| 1198 |
+
if "train" in ds:
|
| 1199 |
+
ds = ds["train"]
|
| 1200 |
+
else:
|
| 1201 |
+
first_key = next(iter(ds.keys()))
|
| 1202 |
+
ds = ds[first_key]
|
| 1203 |
+
|
| 1204 |
+
image_column = None
|
| 1205 |
+
for c in ds.column_names:
|
| 1206 |
+
if c.lower() in {"image", "img"}:
|
| 1207 |
+
image_column = c
|
| 1208 |
+
break
|
| 1209 |
+
if image_column is None:
|
| 1210 |
+
for c in ds.column_names:
|
| 1211 |
+
if "image" in c.lower():
|
| 1212 |
+
image_column = c
|
| 1213 |
+
break
|
| 1214 |
+
if image_column is None:
|
| 1215 |
+
raise ValueError("Could not find an image column in the local HF dataset.")
|
| 1216 |
+
|
| 1217 |
+
for idx, row in enumerate(ds):
|
| 1218 |
+
yield {
|
| 1219 |
+
"id": str(row.get("id", idx)),
|
| 1220 |
+
"image_value": row[image_column],
|
| 1221 |
+
"source": f"{path}:{idx}",
|
| 1222 |
+
"extra": dict(row),
|
| 1223 |
+
}
|
| 1224 |
+
|
| 1225 |
+
|
| 1226 |
+
def pick_input_iterator(
|
| 1227 |
+
input_path: str | None,
|
| 1228 |
+
hf_dataset: str | None,
|
| 1229 |
+
hf_load_from_disk: str | None,
|
| 1230 |
+
image_column: str,
|
| 1231 |
+
):
|
| 1232 |
+
if hf_dataset:
|
| 1233 |
+
return iter_hf_dataset_repo(hf_dataset, split="train", config_name=None)
|
| 1234 |
+
|
| 1235 |
+
if hf_load_from_disk:
|
| 1236 |
+
return iter_hf_local_disk(Path(hf_load_from_disk))
|
| 1237 |
+
|
| 1238 |
+
if input_path is None:
|
| 1239 |
+
raise ValueError("You must provide an input path or a Hugging Face dataset source.")
|
| 1240 |
+
|
| 1241 |
+
path = Path(input_path)
|
| 1242 |
+
|
| 1243 |
+
if path.is_dir():
|
| 1244 |
+
return iter_folder_images(path)
|
| 1245 |
+
|
| 1246 |
+
if path.is_file() and is_image_path(path):
|
| 1247 |
+
return [{
|
| 1248 |
+
"id": path.stem,
|
| 1249 |
+
"image_value": str(path),
|
| 1250 |
+
"source": str(path),
|
| 1251 |
+
"extra": {},
|
| 1252 |
+
}]
|
| 1253 |
+
|
| 1254 |
+
if path.is_file() and path.suffix.lower() in {".csv", ".tsv", ".json", ".jsonl", ".parquet"}:
|
| 1255 |
+
return iter_tabular_records(path, image_column=image_column)
|
| 1256 |
+
|
| 1257 |
+
raise ValueError(f"Unsupported input source: {input_path}")
|
| 1258 |
+
|
| 1259 |
+
|
| 1260 |
+
# ============================================================
|
| 1261 |
+
# Dataset row building
|
| 1262 |
+
# ============================================================
|
| 1263 |
+
|
| 1264 |
+
def make_dataset_row(
|
| 1265 |
+
item: dict,
|
| 1266 |
+
grid_size: int,
|
| 1267 |
+
alpha_threshold: int,
|
| 1268 |
+
) -> dict:
|
| 1269 |
+
image, resolved_path = image_from_value(item["image_value"], base_dir=item.get("base_dir"))
|
| 1270 |
+
result = encode_image(image=image, grid_size=grid_size, alpha_threshold=alpha_threshold)
|
| 1271 |
+
|
| 1272 |
+
d = result["decision"]
|
| 1273 |
+
braille = result["braille"]
|
| 1274 |
+
meta = braille_metadata(braille)
|
| 1275 |
+
|
| 1276 |
+
width, height = result["image"].size
|
| 1277 |
+
|
| 1278 |
+
row = {
|
| 1279 |
+
"id": item.get("id"),
|
| 1280 |
+
"source": item.get("source"),
|
| 1281 |
+
"image": resolved_path if resolved_path is not None else None,
|
| 1282 |
+
"original_width": width,
|
| 1283 |
+
"original_height": height,
|
| 1284 |
+
"grid_size": grid_size,
|
| 1285 |
+
"braille_text": braille,
|
| 1286 |
+
**meta,
|
| 1287 |
+
"mode": d["mode"],
|
| 1288 |
+
"reason": d["reason"],
|
| 1289 |
+
"visible_box": list(map(int, d["visible_box"])),
|
| 1290 |
+
"selected_box": list(map(int, d["box"])),
|
| 1291 |
+
"salient_box": list(map(int, d["bbox"])),
|
| 1292 |
+
"metrics": {k: jsonable(v) for k, v in (d["metrics"] or {}).items()},
|
| 1293 |
+
"outline_info": {k: jsonable(v) for k, v in (d["outline_info"] or {}).items() if k != "expanded_mask"},
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
extra = item.get("extra", {})
|
| 1297 |
+
for k, v in extra.items():
|
| 1298 |
+
if k not in row:
|
| 1299 |
+
row[k] = jsonable(v)
|
| 1300 |
+
|
| 1301 |
+
return row
|
| 1302 |
+
|
| 1303 |
+
|
| 1304 |
+
def build_dataset_rows(
|
| 1305 |
+
iterator,
|
| 1306 |
+
grid_size: int,
|
| 1307 |
+
alpha_threshold: int,
|
| 1308 |
+
limit: int | None = None,
|
| 1309 |
+
):
|
| 1310 |
+
rows = []
|
| 1311 |
+
for idx, item in enumerate(iterator):
|
| 1312 |
+
if limit is not None and idx >= limit:
|
| 1313 |
+
break
|
| 1314 |
+
rows.append(make_dataset_row(item, grid_size=grid_size, alpha_threshold=alpha_threshold))
|
| 1315 |
+
return rows
|
| 1316 |
+
|
| 1317 |
+
|
| 1318 |
+
# ============================================================
|
| 1319 |
+
# Dataset export
|
| 1320 |
+
# ============================================================
|
| 1321 |
+
|
| 1322 |
+
def export_jsonl(rows: list[dict], out_path: Path):
|
| 1323 |
+
with out_path.open("w", encoding="utf-8") as f:
|
| 1324 |
+
for row in rows:
|
| 1325 |
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 1326 |
+
|
| 1327 |
+
|
| 1328 |
+
def export_csv(rows: list[dict], out_path: Path):
|
| 1329 |
+
import pandas as pd
|
| 1330 |
+
|
| 1331 |
+
serializable_rows = []
|
| 1332 |
+
for row in rows:
|
| 1333 |
+
new_row = {}
|
| 1334 |
+
for k, v in row.items():
|
| 1335 |
+
if isinstance(v, (list, dict)):
|
| 1336 |
+
new_row[k] = json.dumps(v, ensure_ascii=False)
|
| 1337 |
+
else:
|
| 1338 |
+
new_row[k] = v
|
| 1339 |
+
serializable_rows.append(new_row)
|
| 1340 |
+
|
| 1341 |
+
pd.DataFrame(serializable_rows).to_csv(out_path, index=False)
|
| 1342 |
+
|
| 1343 |
+
|
| 1344 |
+
def export_parquet(rows: list[dict], out_path: Path):
|
| 1345 |
+
import pandas as pd
|
| 1346 |
+
|
| 1347 |
+
serializable_rows = []
|
| 1348 |
+
for row in rows:
|
| 1349 |
+
new_row = {}
|
| 1350 |
+
for k, v in row.items():
|
| 1351 |
+
if isinstance(v, (list, dict)):
|
| 1352 |
+
new_row[k] = json.dumps(v, ensure_ascii=False)
|
| 1353 |
+
else:
|
| 1354 |
+
new_row[k] = v
|
| 1355 |
+
serializable_rows.append(new_row)
|
| 1356 |
+
|
| 1357 |
+
pd.DataFrame(serializable_rows).to_parquet(out_path, index=False)
|
| 1358 |
+
|
| 1359 |
+
|
| 1360 |
+
def rows_to_hf_dataset(rows: list[dict]):
|
| 1361 |
+
try:
|
| 1362 |
+
from datasets import Dataset, Image as HFImage
|
| 1363 |
+
except Exception as e:
|
| 1364 |
+
raise RuntimeError("datasets is required for HF export. Install with: pip install datasets") from e
|
| 1365 |
+
|
| 1366 |
+
ds = Dataset.from_list(rows)
|
| 1367 |
+
|
| 1368 |
+
# If the dataset has an image-path column with local files, cast it.
|
| 1369 |
+
if "image" in ds.column_names:
|
| 1370 |
+
image_values = ds["image"]
|
| 1371 |
+
if any(v for v in image_values if isinstance(v, str)):
|
| 1372 |
+
try:
|
| 1373 |
+
ds = ds.cast_column("image", HFImage())
|
| 1374 |
+
except Exception:
|
| 1375 |
+
pass
|
| 1376 |
+
|
| 1377 |
+
return ds
|
| 1378 |
+
|
| 1379 |
+
|
| 1380 |
+
def export_hf_disk(rows: list[dict], out_dir: Path):
|
| 1381 |
+
ds = rows_to_hf_dataset(rows)
|
| 1382 |
+
ds.save_to_disk(str(out_dir))
|
| 1383 |
+
return ds
|
| 1384 |
+
|
| 1385 |
+
|
| 1386 |
+
def push_hf_dataset(rows: list[dict], repo_id: str, private: bool = False):
|
| 1387 |
+
ds = rows_to_hf_dataset(rows)
|
| 1388 |
+
ds.push_to_hub(repo_id, private=private)
|
| 1389 |
+
return ds
|
| 1390 |
+
|
| 1391 |
+
|
| 1392 |
+
# ============================================================
|
| 1393 |
+
# CLI
|
| 1394 |
+
# ============================================================
|
| 1395 |
+
|
| 1396 |
+
def main():
|
| 1397 |
+
parser = argparse.ArgumentParser(
|
| 1398 |
+
description="v12 attention-based RGBA -> 64x64 -> Unicode Braille, with HF-friendly dataset export"
|
| 1399 |
+
)
|
| 1400 |
+
|
| 1401 |
+
parser.add_argument("input", nargs="?", help="Image file, folder, or table file")
|
| 1402 |
+
parser.add_argument("-o", "--output", default="braille_output.txt", help="Single-image text output path")
|
| 1403 |
+
parser.add_argument("--grid-size", type=int, default=64, help="Default: 64")
|
| 1404 |
+
parser.add_argument("--alpha-threshold", type=int, default=8)
|
| 1405 |
+
parser.add_argument("--save-debug", action="store_true")
|
| 1406 |
+
parser.add_argument("--debug-dir", default="braille_debug")
|
| 1407 |
+
|
| 1408 |
+
# Dataset / HF options
|
| 1409 |
+
parser.add_argument("--dataset-out", help="Write dataset rows to .jsonl / .csv / .parquet")
|
| 1410 |
+
parser.add_argument("--hf-out", help="Save as datasets.Dataset.save_to_disk(...) folder")
|
| 1411 |
+
parser.add_argument("--push-to-hub", help="Push the generated dataset to Hugging Face Hub")
|
| 1412 |
+
parser.add_argument("--private", action="store_true", help="Use with --push-to-hub")
|
| 1413 |
+
parser.add_argument("--image-column", default="image", help='For CSV/JSON/Parquet inputs. Default: "image"')
|
| 1414 |
+
parser.add_argument("--limit", type=int, default=None, help="Optional row limit for dataset mode")
|
| 1415 |
+
|
| 1416 |
+
# HF input
|
| 1417 |
+
parser.add_argument("--hf-dataset", help="Load a Hugging Face dataset repo, e.g. user/dataset")
|
| 1418 |
+
parser.add_argument("--hf-load-from-disk", help="Load a dataset previously saved with save_to_disk(...)")
|
| 1419 |
+
parser.add_argument("--split", default="train", help="HF split. Default: train")
|
| 1420 |
+
parser.add_argument("--config-name", default=None, help="Optional HF config/subset name")
|
| 1421 |
+
|
| 1422 |
+
args = parser.parse_args()
|
| 1423 |
+
|
| 1424 |
+
# -----------------------------------------
|
| 1425 |
+
# Single-image mode
|
| 1426 |
+
# -----------------------------------------
|
| 1427 |
+
if (
|
| 1428 |
+
args.input
|
| 1429 |
+
and Path(args.input).is_file()
|
| 1430 |
+
and is_image_path(args.input)
|
| 1431 |
+
and not args.dataset_out
|
| 1432 |
+
and not args.hf_out
|
| 1433 |
+
and not args.push_to_hub
|
| 1434 |
+
and not args.hf_dataset
|
| 1435 |
+
and not args.hf_load_from_disk
|
| 1436 |
+
):
|
| 1437 |
+
result = encode_image(
|
| 1438 |
+
image_path=args.input,
|
| 1439 |
+
grid_size=args.grid_size,
|
| 1440 |
+
alpha_threshold=args.alpha_threshold,
|
| 1441 |
+
)
|
| 1442 |
+
|
| 1443 |
+
Path(args.output).write_text(result["braille"], encoding="utf-8")
|
| 1444 |
+
|
| 1445 |
+
d = result["decision"]
|
| 1446 |
+
|
| 1447 |
+
print(result["braille"])
|
| 1448 |
+
print()
|
| 1449 |
+
print("Mode:", d["mode"])
|
| 1450 |
+
print("Reason:", d["reason"])
|
| 1451 |
+
print("Visible box:", d["visible_box"])
|
| 1452 |
+
print("Selected box:", d["box"])
|
| 1453 |
+
|
| 1454 |
+
if d["outline_info"] is not None:
|
| 1455 |
+
print("Color outline layers:", d["outline_info"]["outline_layers"])
|
| 1456 |
+
print("Pixels per outline layer:", d["outline_info"]["layer_px"])
|
| 1457 |
+
|
| 1458 |
+
if d["metrics"] is not None:
|
| 1459 |
+
m = d["metrics"]
|
| 1460 |
+
print("Strong saliency retained:", f'{m["strong_saliency_coverage"] * 100:.1f}%')
|
| 1461 |
+
print("Saliency retained:", f'{m["saliency_coverage"] * 100:.1f}%')
|
| 1462 |
+
print("Weighted detail retained:", f'{m["detail_coverage"] * 100:.1f}%')
|
| 1463 |
+
print("Visible pixels retained:", f'{m["visible_pixel_coverage"] * 100:.1f}%')
|
| 1464 |
+
print("Resolution gain:", f'{m["resolution_gain"]:.2f}x')
|
| 1465 |
+
|
| 1466 |
+
print("Saved:", args.output)
|
| 1467 |
+
|
| 1468 |
+
if args.save_debug:
|
| 1469 |
+
save_debug(result, Path(args.debug_dir))
|
| 1470 |
+
print("Debug files:", args.debug_dir)
|
| 1471 |
+
|
| 1472 |
+
return
|
| 1473 |
+
|
| 1474 |
+
# -----------------------------------------
|
| 1475 |
+
# Dataset mode
|
| 1476 |
+
# -----------------------------------------
|
| 1477 |
+
if args.hf_dataset:
|
| 1478 |
+
iterator = iter_hf_dataset_repo(args.hf_dataset, split=args.split, config_name=args.config_name)
|
| 1479 |
+
elif args.hf_load_from_disk:
|
| 1480 |
+
iterator = iter_hf_local_disk(Path(args.hf_load_from_disk))
|
| 1481 |
+
else:
|
| 1482 |
+
iterator = pick_input_iterator(
|
| 1483 |
+
input_path=args.input,
|
| 1484 |
+
hf_dataset=None,
|
| 1485 |
+
hf_load_from_disk=None,
|
| 1486 |
+
image_column=args.image_column,
|
| 1487 |
+
)
|
| 1488 |
+
|
| 1489 |
+
rows = build_dataset_rows(
|
| 1490 |
+
iterator,
|
| 1491 |
+
grid_size=args.grid_size,
|
| 1492 |
+
alpha_threshold=args.alpha_threshold,
|
| 1493 |
+
limit=args.limit,
|
| 1494 |
+
)
|
| 1495 |
+
|
| 1496 |
+
if args.dataset_out:
|
| 1497 |
+
out_path = Path(args.dataset_out)
|
| 1498 |
+
suffix = out_path.suffix.lower()
|
| 1499 |
+
if suffix == ".jsonl":
|
| 1500 |
+
export_jsonl(rows, out_path)
|
| 1501 |
+
elif suffix == ".csv":
|
| 1502 |
+
export_csv(rows, out_path)
|
| 1503 |
+
elif suffix == ".parquet":
|
| 1504 |
+
export_parquet(rows, out_path)
|
| 1505 |
+
else:
|
| 1506 |
+
raise ValueError("dataset-out must end with .jsonl, .csv, or .parquet")
|
| 1507 |
+
print("Saved dataset rows:", out_path)
|
| 1508 |
+
|
| 1509 |
+
ds = None
|
| 1510 |
+
|
| 1511 |
+
if args.hf_out:
|
| 1512 |
+
ds = export_hf_disk(rows, Path(args.hf_out))
|
| 1513 |
+
print("Saved HF dataset to disk:", args.hf_out)
|
| 1514 |
+
|
| 1515 |
+
if args.push_to_hub:
|
| 1516 |
+
ds = push_hf_dataset(rows, args.push_to_hub, private=args.private)
|
| 1517 |
+
print("Pushed HF dataset:", args.push_to_hub)
|
| 1518 |
+
|
| 1519 |
+
if not args.dataset_out and not args.hf_out and not args.push_to_hub:
|
| 1520 |
+
# Default dataset fallback: write JSONL if not explicitly specified.
|
| 1521 |
+
out_path = Path("braille_dataset.jsonl")
|
| 1522 |
+
export_jsonl(rows, out_path)
|
| 1523 |
+
print("Saved dataset rows:", out_path)
|
| 1524 |
+
|
| 1525 |
+
print("Rows:", len(rows))
|
| 1526 |
+
if rows:
|
| 1527 |
+
print("Braille size:", f'{rows[0]["braille_width_chars"]}x{rows[0]["braille_height_chars"]} chars')
|
| 1528 |
+
print("Source grid:", f'{rows[0]["grid_size"]}x{rows[0]["grid_size"]}')
|
| 1529 |
+
|
| 1530 |
+
|
| 1531 |
+
if __name__ == "__main__":
|
| 1532 |
+
main()
|
unified/ablations/unified-v1-blt-none-s42/source/canvas_flow.py
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CanvasFlow Lab: independently implemented discrete-flow canvas experiments.
|
| 2 |
+
|
| 3 |
+
No recurrent-depth weight sharing. Image locations are arbitrary masks. Text is
|
| 4 |
+
an ordered byte lane; geometry supplies capacity coordinates, the final renderer
|
| 5 |
+
supplies exact word-wrapped coordinates. See the notebook for the distinction.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
import argparse, contextlib, copy, hashlib, html, json, math, os, random, re
|
| 9 |
+
import tempfile, time, warnings, pickle, inspect
|
| 10 |
+
from dataclasses import asdict, dataclass, replace
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
import numpy as np
|
| 13 |
+
from PIL import Image, ImageDraw
|
| 14 |
+
import torch
|
| 15 |
+
from torch import nn
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
|
| 18 |
+
FREE, TEXT, IMAGE, RESERVED, UNKNOWN = range(5)
|
| 19 |
+
EOS, PAD, MASK = 256, 257, 258
|
| 20 |
+
BYTE_VOCAB = 259
|
| 21 |
+
SCHEMA = 1
|
| 22 |
+
FAMILIES = ('shapes', 'tree', 'house', 'fish', 'rocket', 'chart', 'waves', 'terrain', 'network', 'texture')
|
| 23 |
+
POLICIES = ('row', 'columns', 'reverse_columns')
|
| 24 |
+
DOTS = ((0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (3, 0), (3, 1))
|
| 25 |
+
|
| 26 |
+
def digest(obj):
|
| 27 |
+
return hashlib.sha256(json.dumps(obj, sort_keys=True).encode()).hexdigest()
|
| 28 |
+
|
| 29 |
+
def atomic_json(path, obj):
|
| 30 |
+
path = Path(path); path.parent.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
tmp = path.with_suffix(path.suffix + '.tmp')
|
| 32 |
+
tmp.write_text(json.dumps(obj, indent=2, allow_nan=False)); os.replace(tmp, path)
|
| 33 |
+
|
| 34 |
+
def seed_all(seed):
|
| 35 |
+
random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
|
| 36 |
+
if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
|
| 37 |
+
|
| 38 |
+
def pack_braille(bitmap):
|
| 39 |
+
a = np.asarray(bitmap, dtype=bool)
|
| 40 |
+
if a.ndim != 2 or a.shape[0] % 4 or a.shape[1] % 2:
|
| 41 |
+
raise ValueError('Braille bitmap must have height divisible by 4 and width by 2')
|
| 42 |
+
out = np.zeros((a.shape[0] // 4, a.shape[1] // 2), np.uint8)
|
| 43 |
+
for bit, (y, x) in enumerate(DOTS): out |= a[y::4, x::2].astype(np.uint8) << bit
|
| 44 |
+
return out
|
| 45 |
+
|
| 46 |
+
def unpack_braille(cells):
|
| 47 |
+
a = np.asarray(cells, np.uint8); out = np.zeros((a.shape[0] * 4, a.shape[1] * 2), bool)
|
| 48 |
+
for bit, (y, x) in enumerate(DOTS): out[y::4, x::2] = ((a >> bit) & 1).astype(bool)
|
| 49 |
+
return out
|
| 50 |
+
|
| 51 |
+
def dilate(mask, radius=1):
|
| 52 |
+
mask = np.asarray(mask, bool)
|
| 53 |
+
if radius < 0: raise ValueError('negative margin')
|
| 54 |
+
p = np.pad(mask, radius)
|
| 55 |
+
out = np.zeros_like(mask)
|
| 56 |
+
for dy in range(2 * radius + 1):
|
| 57 |
+
for dx in range(2 * radius + 1): out |= p[dy:dy+mask.shape[0], dx:dx+mask.shape[1]]
|
| 58 |
+
return out
|
| 59 |
+
|
| 60 |
+
def flow_runs(allowed, policy='columns', region_order=None):
|
| 61 |
+
"""Horizontal runs linked only through unambiguous overlap across rows.
|
| 62 |
+
|
| 63 |
+
Split/merge events start new regions: left/right columns beside an obstacle
|
| 64 |
+
do not collapse into one region merely because they connect below it.
|
| 65 |
+
region_order can explicitly supply any permutation of the resulting regions.
|
| 66 |
+
"""
|
| 67 |
+
allowed = np.asarray(allowed, bool); runs = []; previous = []; next_region = 0
|
| 68 |
+
for y, row in enumerate(allowed):
|
| 69 |
+
edges = np.flatnonzero(np.diff(np.r_[False, row, False].astype(np.int8)))
|
| 70 |
+
current = [(y, int(a), int(b)) for a, b in zip(edges[::2], edges[1::2])]
|
| 71 |
+
overlap = [[j for j, p in enumerate(previous) if max(a, p[1]) < min(b, p[2])]
|
| 72 |
+
for _, a, b in current]
|
| 73 |
+
uses = [sum(j in js for js in overlap) for j in range(len(previous))]
|
| 74 |
+
tagged = []
|
| 75 |
+
for r, js in zip(current, overlap):
|
| 76 |
+
if len(js) == 1 and uses[js[0]] == 1: region = previous[js[0]][3]
|
| 77 |
+
else: region = next_region; next_region += 1
|
| 78 |
+
tagged.append((*r, region))
|
| 79 |
+
runs.extend(tagged); previous = tagged
|
| 80 |
+
if policy == 'row' and region_order is None: return runs
|
| 81 |
+
first = {}
|
| 82 |
+
for y, a, b, region in runs: first.setdefault(region, (y, a))
|
| 83 |
+
if region_order is not None:
|
| 84 |
+
if sorted(region_order) != sorted(first): raise ValueError('region_order must be a permutation')
|
| 85 |
+
rank = {r: i for i, r in enumerate(region_order)}
|
| 86 |
+
else:
|
| 87 |
+
if policy not in POLICIES: raise ValueError(policy)
|
| 88 |
+
sign = -1 if policy == 'reverse_columns' else 1
|
| 89 |
+
# Regions beginning on the same row are columns; merging regions follow.
|
| 90 |
+
order = sorted(first, key=lambda r: (first[r][0], sign * first[r][1]))
|
| 91 |
+
rank = {r: i for i, r in enumerate(order)}
|
| 92 |
+
return sorted(runs, key=lambda r: (rank[r[3]], r[0], r[1]))
|
| 93 |
+
|
| 94 |
+
def capacity_map(layout, length, policy='columns', region_order=None):
|
| 95 |
+
"""Content-independent, provisional coordinates. NEVER derived from targets."""
|
| 96 |
+
coords = np.zeros((length, 2), np.int16); regions = np.zeros(length, np.int16)
|
| 97 |
+
valid = np.zeros(length, bool); k = 0
|
| 98 |
+
for y, a, b, region in flow_runs(np.asarray(layout) == TEXT, policy, region_order):
|
| 99 |
+
n = min(b - a, length - k)
|
| 100 |
+
if n <= 0: break
|
| 101 |
+
coords[k:k+n, 0] = y; coords[k:k+n, 1] = np.arange(a, a+n)
|
| 102 |
+
regions[k:k+n] = region + 1; valid[k:k+n] = True; k += n
|
| 103 |
+
return coords, regions, valid
|
| 104 |
+
|
| 105 |
+
def wrap_words(text, layout, policy='columns', region_order=None):
|
| 106 |
+
"""ASCII wrapper. Preserves normalized byte order and never crosses obstacles.
|
| 107 |
+
|
| 108 |
+
A word is moved intact to the next run that can hold it. Only words longer
|
| 109 |
+
than EVERY run are split. Whitespace is normalized explicitly. Overflow is
|
| 110 |
+
returned, never silently lost. Returned mapping is the exact rendered map.
|
| 111 |
+
"""
|
| 112 |
+
text.encode('ascii', errors='strict')
|
| 113 |
+
normalized = ' '.join(text.split())
|
| 114 |
+
runs = flow_runs(np.asarray(layout) == TEXT, policy, region_order)
|
| 115 |
+
coords, indices, rendered = [], [], []
|
| 116 |
+
if not runs: return dict(text=normalized, chars='', coords=[], indices=[], overflow=normalized)
|
| 117 |
+
longest = max(b-a for _, a, b, _ in runs)
|
| 118 |
+
ri = 0; cursor = runs[0][1]; consumed = 0
|
| 119 |
+
for wi, word in enumerate(normalized.split(' ')):
|
| 120 |
+
if not word: continue
|
| 121 |
+
piece = (' ' if wi else '') + word
|
| 122 |
+
# Keep the actual separating space in the byte-to-cell map.
|
| 123 |
+
if len(piece) <= longest:
|
| 124 |
+
while ri < len(runs) and runs[ri][2] - cursor < len(piece):
|
| 125 |
+
ri += 1
|
| 126 |
+
if ri < len(runs): cursor = runs[ri][1]
|
| 127 |
+
if ri == len(runs): break
|
| 128 |
+
for ch in piece:
|
| 129 |
+
while ri < len(runs) and cursor >= runs[ri][2]:
|
| 130 |
+
ri += 1
|
| 131 |
+
if ri < len(runs): cursor = runs[ri][1]
|
| 132 |
+
if ri == len(runs): break
|
| 133 |
+
coords.append((runs[ri][0], cursor)); indices.append(consumed)
|
| 134 |
+
rendered.append(ch); cursor += 1; consumed += 1
|
| 135 |
+
if ri == len(runs): break
|
| 136 |
+
return dict(text=normalized, chars=''.join(rendered), coords=coords,
|
| 137 |
+
indices=indices, overflow=normalized[consumed:])
|
| 138 |
+
|
| 139 |
+
def draw_picture(rng, family, side=64):
|
| 140 |
+
"""Original procedural binary graphics, not downloaded or generated captions."""
|
| 141 |
+
im = Image.new('L', (side, side), 0); d = ImageDraw.Draw(im)
|
| 142 |
+
p = lambda x: int(x * side / 64)
|
| 143 |
+
def line(points, width=2): d.line([(p(x), p(y)) for x,y in points], fill=255, width=max(1,p(width)))
|
| 144 |
+
if family == 'tree':
|
| 145 |
+
d.polygon([(p(32),p(3)),(p(8),p(42)),(p(56),p(42))], fill=255)
|
| 146 |
+
d.rectangle((p(28),p(39),p(36),p(61)),fill=255)
|
| 147 |
+
elif family == 'house':
|
| 148 |
+
d.polygon([(p(4),p(28)),(p(32),p(4)),(p(60),p(28))],fill=255)
|
| 149 |
+
d.rectangle((p(12),p(28),p(52),p(60)),outline=255,width=2)
|
| 150 |
+
d.rectangle((p(27),p(39),p(38),p(60)),outline=255,width=2)
|
| 151 |
+
d.rectangle((p(17),p(34),p(23),p(42)),fill=255)
|
| 152 |
+
elif family == 'fish':
|
| 153 |
+
d.ellipse((p(5),p(15),p(48),p(48)), fill=255)
|
| 154 |
+
d.polygon([(p(43),p(31)),(p(61),p(15)),(p(61),p(49))],fill=255)
|
| 155 |
+
d.ellipse((p(13),p(24),p(18),p(29)),fill=0)
|
| 156 |
+
elif family == 'rocket':
|
| 157 |
+
d.polygon([(p(32),p(2)),(p(43),p(24)),(p(43),p(48)),(p(21),p(48)),(p(21),p(24))],fill=255)
|
| 158 |
+
d.ellipse((p(27),p(23),p(37),p(33)),fill=0)
|
| 159 |
+
line([(24,50),(20,62)]); line([(32,50),(32,61)]); line([(40,50),(44,62)])
|
| 160 |
+
elif family == 'chart':
|
| 161 |
+
line([(5,4),(5,58),(61,58)],1)
|
| 162 |
+
for j in range(5):
|
| 163 |
+
x = 10+j*10; h = int(rng.integers(5,48))
|
| 164 |
+
d.rectangle((p(x),p(57-h),p(x+6),p(57)),fill=255)
|
| 165 |
+
elif family == 'waves':
|
| 166 |
+
for j in range(4):
|
| 167 |
+
yy = 10+j*14 + np.sin(np.linspace(0, rng.uniform(5,15),side))*rng.uniform(2,5)
|
| 168 |
+
d.line(list(zip(range(side),[p(v) for v in yy])),fill=255,width=1)
|
| 169 |
+
elif family == 'terrain':
|
| 170 |
+
heights = rng.integers(12,42,9)
|
| 171 |
+
line([(i*8,int(y)) for i,y in enumerate(heights)])
|
| 172 |
+
line([(0,57),(64,57)],1); d.ellipse((p(44),p(2),p(55),p(13)),outline=255,width=2)
|
| 173 |
+
elif family == 'network':
|
| 174 |
+
pts = rng.integers(8,56,(7,2))
|
| 175 |
+
for a,b in zip(pts, np.roll(pts,1,axis=0)): line([a,b],1)
|
| 176 |
+
for x,y in pts: d.ellipse((p(x-3),p(y-3),p(x+3),p(y+3)),fill=255)
|
| 177 |
+
elif family == 'texture':
|
| 178 |
+
x,y=np.meshgrid(np.arange(side),np.arange(side))
|
| 179 |
+
arr = ((x//int(rng.integers(3,10)) + y//int(rng.integers(3,10)))%2)*255
|
| 180 |
+
return arr.astype(np.uint8)>0
|
| 181 |
+
else:
|
| 182 |
+
for _ in range(int(rng.integers(2,6))):
|
| 183 |
+
x,y = rng.integers(2,40,2); w,h = rng.integers(8,23,2)
|
| 184 |
+
box=(p(x),p(y),p(x+w),p(y+h))
|
| 185 |
+
if rng.random()<.5: d.ellipse(box,outline=255,width=2)
|
| 186 |
+
else: d.rectangle(box,outline=255,width=2)
|
| 187 |
+
if rng.random()<.5: im=im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
| 188 |
+
# Shift the drawing within its source crop, not just the canvas placement.
|
| 189 |
+
a=np.asarray(im)>127
|
| 190 |
+
return np.roll(a, tuple(rng.integers(-3,4,2)),axis=(0,1))
|
| 191 |
+
|
| 192 |
+
def source_text(rng, families, scene_id):
|
| 193 |
+
subjects = ', '.join(families) if families else 'an empty page'
|
| 194 |
+
verbs = ('examined','described','compared','sketched','measured','revisited')
|
| 195 |
+
names = ('Mira','Leo','Ada','Noah','Iris','Omar','Lina','Theo')
|
| 196 |
+
name = str(rng.choice(names)); verb = str(rng.choice(verbs)); n=int(rng.integers(2,40))
|
| 197 |
+
types = [
|
| 198 |
+
f'{name} {verb} {subjects}. The picture belongs to study {scene_id}. The notes stay in reading order even when the page has several separate regions.',
|
| 199 |
+
f'Observation {scene_id}: {subjects}. We counted {n} marks in the draft and added {n+3} in the revision. The new total is {2*n+3}. This is a worked arithmetic example, not a count of visible objects.',
|
| 200 |
+
f'Field notes by {name}. First inspect {subjects}. Next compare the outline and the empty spaces. Finally write a short explanation. Keep every word away from the drawing.',
|
| 201 |
+
f'{name} opened a notebook at sunrise. A study of {subjects} filled part of the page. The remaining space held a short story about a careful observer and a surprising discovery.',
|
| 202 |
+
f'Record {scene_id}. Subject: {subjects}. Author: {name}. Revision: {n}. Status: reviewed. The diagram and the text are two views of the same small experiment.',
|
| 203 |
+
]
|
| 204 |
+
text = str(rng.choice(types))
|
| 205 |
+
if rng.random()<.35: text += ' ' + str(rng.choice(types))
|
| 206 |
+
return text
|
| 207 |
+
|
| 208 |
+
def make_example(index, split='train', size=128, text_len=512, prompt_len=192, seed=731, encoding_override=None):
|
| 209 |
+
split_code={'train':0,'val':1,'test':2,'ood':3}[split]
|
| 210 |
+
rng=np.random.default_rng(np.random.SeedSequence([seed,split_code,index]))
|
| 211 |
+
kind=('text','image','mixed')[index%3]
|
| 212 |
+
encoding=int(rng.random()<.25) # 0=Braille bytes, 1=ASCII image glyphs
|
| 213 |
+
if encoding_override is not None:encoding=int(encoding_override)
|
| 214 |
+
policy=POLICIES[int(rng.integers(0,3))]
|
| 215 |
+
image=np.zeros((size,size),np.uint8); occupied=np.zeros((size,size),bool)
|
| 216 |
+
families=[]; objects=[]
|
| 217 |
+
if kind!='text':
|
| 218 |
+
count=int(rng.integers(1,4 if split!='ood' else 6))
|
| 219 |
+
for j in range(count):
|
| 220 |
+
family=str(rng.choice(FAMILIES)); families.append(family)
|
| 221 |
+
source_side=int(rng.choice([32,64,64,64,96,128]))
|
| 222 |
+
bitmap=draw_picture(rng,family,source_side)
|
| 223 |
+
if rng.random()<.3: bitmap=np.rot90(bitmap,int(rng.integers(1,4)))
|
| 224 |
+
cells=pack_braille(bitmap)
|
| 225 |
+
if encoding:
|
| 226 |
+
block=bitmap.reshape(source_side//4,4,source_side//2,2).mean(axis=(1,3))
|
| 227 |
+
ramp=np.frombuffer(b' .:-=+*#%@',np.uint8)
|
| 228 |
+
cells=ramp[np.round(block*(len(ramp)-1)).astype(int)]
|
| 229 |
+
ink=cells!=32
|
| 230 |
+
else: ink=cells!=0
|
| 231 |
+
# 64x64 source pixels -> 16x32 Braille cells. Small test canvases resize.
|
| 232 |
+
h=min(cells.shape[0],max(4,size//2)); w=min(cells.shape[1],max(4,size//2))
|
| 233 |
+
if (h,w)!=cells.shape:
|
| 234 |
+
cells=np.asarray(Image.fromarray(cells).resize((w,h),Image.Resampling.NEAREST))
|
| 235 |
+
ink=cells!=(32 if encoding else 0)
|
| 236 |
+
y=int(rng.integers(0,size-h+1)); x=int(rng.integers(0,size-w+1))
|
| 237 |
+
if split=='ood': # Unseen edge contacts and diagonal chained placement.
|
| 238 |
+
y=int((size-h)*j/max(1,count-1)); x=int((size-w)*(count-1-j)/max(1,count-1))
|
| 239 |
+
box=bool(rng.random()<.25)
|
| 240 |
+
mask=np.ones_like(ink) if box else ink
|
| 241 |
+
sub=image[y:y+h,x:x+w]; sub[mask]=cells[mask]
|
| 242 |
+
occupied[y:y+h,x:x+w]|=mask
|
| 243 |
+
objects.append(dict(family=family,y=y,x=x,h=h,w=w,box=box))
|
| 244 |
+
margin=int(rng.integers(1,4)); blocked=dilate(occupied,margin)
|
| 245 |
+
layout=np.full((size,size),FREE,np.uint8)
|
| 246 |
+
if kind!='image':
|
| 247 |
+
if objects and rng.random()<.8:
|
| 248 |
+
# Put the reading region around an image wherever it happens to be.
|
| 249 |
+
# Otherwise short 512-byte text at row zero would rarely test wrapping.
|
| 250 |
+
focus=objects[int(rng.integers(len(objects)))]; pad=int(rng.integers(4,17))
|
| 251 |
+
y0=max(0,focus['y']-2); y1=min(size,focus['y']+focus['h']+pad)
|
| 252 |
+
x0=max(0,focus['x']-pad); x1=min(size,focus['x']+focus['w']+pad)
|
| 253 |
+
elif rng.random()<.5:
|
| 254 |
+
y0=int(rng.integers(0,max(1,size//2))); y1=size
|
| 255 |
+
x0=int(rng.integers(0,max(1,size//3))); x1=size-int(rng.integers(0,max(1,size//4)))
|
| 256 |
+
else: y0,x0,y1,x1=0,0,size,size
|
| 257 |
+
layout[y0:y1,x0:x1]=TEXT
|
| 258 |
+
# Additional gutters can create disconnected text regions without image boxes.
|
| 259 |
+
if kind!='image' and rng.random()<.4:
|
| 260 |
+
mid=int(rng.integers(size//3,2*size//3)); layout[:,mid:mid+1]=RESERVED
|
| 261 |
+
layout[blocked]=RESERVED; layout[occupied]=IMAGE
|
| 262 |
+
text=source_text(rng,families,f'{split}-{index}') if kind!='image' else ''
|
| 263 |
+
text=text[:text_len-1]
|
| 264 |
+
if len(text)==text_len-1 and ' ' in text: text=text.rsplit(' ',1)[0]
|
| 265 |
+
# Short canvases have a real capacity constraint. Determine a fitting prefix.
|
| 266 |
+
rendered=wrap_words(text,layout,policy)
|
| 267 |
+
if rendered['overflow']: text=rendered['chars'].rstrip()
|
| 268 |
+
tokens=np.full(text_len,PAD,np.int16); b=text.encode('ascii')
|
| 269 |
+
tokens[:len(b)]=list(b); tokens[len(b)]=EOS
|
| 270 |
+
geometry='; '.join(f"{o['family']} at {o['x']},{o['y']}" for o in objects)
|
| 271 |
+
prompt=f"{kind}; {'ASCII' if encoding else 'Braille'}; order {policy}; margin {margin}; {geometry}. Write notes."[:prompt_len]
|
| 272 |
+
p=np.full(prompt_len,PAD,np.int16); p[:len(prompt)]=list(prompt.encode('ascii'))
|
| 273 |
+
coords,regions,capacity=capacity_map(layout,text_len,policy)
|
| 274 |
+
return dict(image=image,layout=layout,text=tokens,prompt=p,coords=coords,
|
| 275 |
+
regions=regions,capacity=capacity,encoding=np.int64(encoding),order=np.int64(POLICIES.index(policy)),
|
| 276 |
+
meta=dict(index=index,split=split,kind=kind,families=families,objects=objects,
|
| 277 |
+
text=text,prompt=prompt,policy=policy,margin=margin))
|
| 278 |
+
|
| 279 |
+
ARRAY_KEYS=('image','layout','text','prompt','coords','regions','capacity','encoding','order')
|
| 280 |
+
|
| 281 |
+
def build_data(root, count=4096, size=128, text_len=512, prompt_len=192, seed=731):
|
| 282 |
+
root=Path(root); root.mkdir(parents=True,exist_ok=True)
|
| 283 |
+
generator_source='\n'.join(inspect.getsource(fn) for fn in
|
| 284 |
+
(pack_braille,dilate,flow_runs,capacity_map,wrap_words,draw_picture,source_text,make_example))
|
| 285 |
+
config=dict(schema=SCHEMA,count=count,size=size,text_len=text_len,prompt_len=prompt_len,seed=seed,
|
| 286 |
+
generator_sha=digest(dict(source=generator_source,families=FAMILIES,policies=POLICIES,dots=DOTS)))
|
| 287 |
+
manifest=root/'manifest.json'
|
| 288 |
+
if manifest.exists():
|
| 289 |
+
old=json.loads(manifest.read_text())
|
| 290 |
+
if old['config']!=config: raise ValueError('Dataset config/code mismatch; choose a fresh data directory')
|
| 291 |
+
for name,sha in old['sha256'].items():
|
| 292 |
+
if hashlib.sha256((root/name).read_bytes()).hexdigest()!=sha: raise ValueError(f'Data checksum mismatch: {name}')
|
| 293 |
+
return old
|
| 294 |
+
checksums={}; counts={'train':count,'val':max(32,count//16),'test':max(32,count//16),'ood':max(32,count//16)}
|
| 295 |
+
for split,n in counts.items():
|
| 296 |
+
arrays=None; metadata=[]
|
| 297 |
+
for i in range(n):
|
| 298 |
+
ex=make_example(i,split,size,text_len,prompt_len,seed)
|
| 299 |
+
if arrays is None: arrays={k:np.empty((n,*np.asarray(ex[k]).shape),np.asarray(ex[k]).dtype) for k in ARRAY_KEYS}
|
| 300 |
+
for k in ARRAY_KEYS: arrays[k][i]=ex[k]
|
| 301 |
+
metadata.append(ex['meta'])
|
| 302 |
+
for k,a in arrays.items():
|
| 303 |
+
name=f'{split}_{k}.npy'; tmp=root/(name+'.tmp')
|
| 304 |
+
with open(tmp,'wb') as f: np.save(f,a)
|
| 305 |
+
os.replace(tmp,root/name); checksums[name]=hashlib.sha256((root/name).read_bytes()).hexdigest()
|
| 306 |
+
name=f'{split}_meta.json'; atomic_json(root/name,metadata)
|
| 307 |
+
checksums[name]=hashlib.sha256((root/name).read_bytes()).hexdigest()
|
| 308 |
+
print(f'data {split}: {n} examples',flush=True)
|
| 309 |
+
result=dict(config=config,counts=counts,sha256=checksums,fingerprint=digest(checksums))
|
| 310 |
+
atomic_json(manifest,result); return result
|
| 311 |
+
|
| 312 |
+
class CanvasData:
|
| 313 |
+
def __init__(self, root, split='train', device='cpu', preload=False):
|
| 314 |
+
root=Path(root); self.manifest=json.loads((root/'manifest.json').read_text())
|
| 315 |
+
self.data={k:np.load(root/f'{split}_{k}.npy',mmap_mode='r') for k in ARRAY_KEYS}
|
| 316 |
+
self.n=len(self.data['text']); self.device=device
|
| 317 |
+
self.raster_cache=None
|
| 318 |
+
if preload:
|
| 319 |
+
self.data={k:torch.from_numpy(np.array(v)).to(device) for k,v in self.data.items()}
|
| 320 |
+
def batch(self, indices, variant='full'):
|
| 321 |
+
idx=np.asarray(indices)%self.n
|
| 322 |
+
out={k:(v[torch.as_tensor(idx,device=v.device)].long() if torch.is_tensor(v)
|
| 323 |
+
else torch.as_tensor(np.array(v[idx]),device=self.device).long()) for k,v in self.data.items()}
|
| 324 |
+
if variant=='raster':
|
| 325 |
+
if self.raster_cache is None:
|
| 326 |
+
# Build ONCE, outside steady-state timings after the first batch.
|
| 327 |
+
layouts=self.data['layout'].cpu().numpy() if torch.is_tensor(self.data['layout']) else self.data['layout']
|
| 328 |
+
maps=[capacity_map(x,out['text'].shape[1],'row') for x in layouts]
|
| 329 |
+
self.raster_cache=tuple(torch.as_tensor(np.stack([x[j] for x in maps]),device=self.device).long() for j in (0,1))
|
| 330 |
+
out['coords']=self.raster_cache[0][torch.as_tensor(idx,device=self.device)]
|
| 331 |
+
out['regions']=self.raster_cache[1][torch.as_tensor(idx,device=self.device)]
|
| 332 |
+
return out
|
| 333 |
+
|
| 334 |
+
def decode_text(tokens):
|
| 335 |
+
values=[]
|
| 336 |
+
for t in np.asarray(tokens).reshape(-1):
|
| 337 |
+
if t==EOS: break
|
| 338 |
+
if 0<=t<256: values.append(int(t))
|
| 339 |
+
return bytes(values).decode('ascii',errors='replace')
|
| 340 |
+
|
| 341 |
+
def page_html(image, layout, text, encoding=0, policy='columns', title='Canvas'):
|
| 342 |
+
rendered=wrap_words(text,layout,policy); size=len(layout)
|
| 343 |
+
page=np.full((size,size),' ',dtype='<U1')
|
| 344 |
+
for y,x in np.argwhere(layout==IMAGE):
|
| 345 |
+
v=int(image[y,x]); page[y,x]=chr(0x2800+v) if not encoding else chr(v if 32<=v<127 else 63)
|
| 346 |
+
for (y,x),ch in zip(rendered['coords'],rendered['chars']): page[y,x]=ch
|
| 347 |
+
body='\n'.join(''.join(row) for row in page)
|
| 348 |
+
return f'<section><h2>{html.escape(title)}</h2><pre>{html.escape(body)}</pre><p>Overflow: {html.escape(rendered["overflow"])}</p></section>'
|
| 349 |
+
|
| 350 |
+
def gallery(root, output, n=9, split='val'):
|
| 351 |
+
root=Path(root); meta=json.loads((root/f'{split}_meta.json').read_text())
|
| 352 |
+
images=np.load(root/f'{split}_image.npy',mmap_mode='r'); layouts=np.load(root/f'{split}_layout.npy',mmap_mode='r')
|
| 353 |
+
enc=np.load(root/f'{split}_encoding.npy',mmap_mode='r')
|
| 354 |
+
parts=[page_html(images[i],layouts[i],meta[i]['text'],int(enc[i]),meta[i]['policy'],meta[i]['prompt']) for i in range(min(n,len(meta)))]
|
| 355 |
+
Path(output).write_text('<!doctype html><meta charset="utf-8"><style>body{background:#111827;color:#e5e7eb;font:14px system-ui}section{margin:24px}pre{font:10px/12px monospace;white-space:pre;border:1px solid #475569;display:inline-block;padding:12px}h2{font-size:15px}</style>'+''.join(parts))
|
| 356 |
+
|
| 357 |
+
@dataclass
|
| 358 |
+
class Config:
|
| 359 |
+
size: int=128
|
| 360 |
+
text_len: int=512
|
| 361 |
+
prompt_len: int=192
|
| 362 |
+
dim: int=256
|
| 363 |
+
depth: int=6
|
| 364 |
+
heads: int=4
|
| 365 |
+
patch: int=4
|
| 366 |
+
local_dim: int=32
|
| 367 |
+
variant: str='full'
|
| 368 |
+
objective: str='dfm'
|
| 369 |
+
checkpoint: bool=False
|
| 370 |
+
def validate(self):
|
| 371 |
+
if self.size%self.patch or self.dim%self.heads or self.dim%2:
|
| 372 |
+
raise ValueError('Incompatible canvas/patch/head dimensions')
|
| 373 |
+
if self.variant not in ('full','no_flow','raster','patch8','mdlm'): raise ValueError(self.variant)
|
| 374 |
+
if self.objective not in ('dfm','mdlm'): raise ValueError(self.objective)
|
| 375 |
+
|
| 376 |
+
def positional(t, dim):
|
| 377 |
+
freq=torch.exp(-math.log(10000)*torch.arange(dim//2,device=t.device).float()/max(1,dim//2-1))
|
| 378 |
+
phase=t.float()[...,None]*freq
|
| 379 |
+
return torch.cat((phase.sin(),phase.cos()),-1)
|
| 380 |
+
|
| 381 |
+
class Block(nn.Module):
|
| 382 |
+
def __init__(self, dim, heads):
|
| 383 |
+
super().__init__(); self.heads=heads
|
| 384 |
+
self.n1=nn.LayerNorm(dim); self.n2=nn.LayerNorm(dim)
|
| 385 |
+
self.qkv=nn.Linear(dim,dim*3,bias=False); self.proj=nn.Linear(dim,dim,bias=False)
|
| 386 |
+
self.up=nn.Linear(dim,dim*8,bias=False); self.down=nn.Linear(dim*4,dim,bias=False)
|
| 387 |
+
def forward(self,h):
|
| 388 |
+
b,n,d=h.shape
|
| 389 |
+
q,k,v=self.qkv(self.n1(h)).reshape(b,n,3,self.heads,d//self.heads).permute(2,0,3,1,4).unbind(0)
|
| 390 |
+
# Fused SDPA chooses a supported kernel. No 16K x 16K full-canvas mask.
|
| 391 |
+
z=F.scaled_dot_product_attention(q,k,v,dropout_p=0.0,is_causal=False)
|
| 392 |
+
h=h+self.proj(z.transpose(1,2).reshape(b,n,d))
|
| 393 |
+
a,g=self.up(self.n2(h)).chunk(2,-1)
|
| 394 |
+
return h+self.down(F.silu(a)*g)
|
| 395 |
+
|
| 396 |
+
class CanvasModel(nn.Module):
|
| 397 |
+
"""Two modes, same implementation: small layout planner or content denoiser.
|
| 398 |
+
|
| 399 |
+
Content uses a single shared byte embedding and transformer for the spatial
|
| 400 |
+
patches, linguistic byte lane, and byte prompt. Every block has unique weights.
|
| 401 |
+
"""
|
| 402 |
+
def __init__(self,c,planner=False):
|
| 403 |
+
super().__init__(); c.validate(); self.c=c; self.planner=planner
|
| 404 |
+
self.byte=nn.Embedding(BYTE_VOCAB,c.dim)
|
| 405 |
+
self.local=nn.Linear(c.dim,c.local_dim,bias=False)
|
| 406 |
+
self.occupancy=nn.Embedding(5,c.local_dim)
|
| 407 |
+
self.to_patch=nn.Linear(c.local_dim*c.patch*c.patch,c.dim)
|
| 408 |
+
self.to_cells=nn.Linear(c.dim,c.local_dim*c.patch*c.patch)
|
| 409 |
+
self.x=nn.Embedding(c.size,c.dim); self.y=nn.Embedding(c.size,c.dim)
|
| 410 |
+
self.region=nn.Embedding(257,c.dim)
|
| 411 |
+
self.modality=nn.Embedding(4,c.dim)
|
| 412 |
+
self.format=nn.Embedding(2,c.dim)
|
| 413 |
+
self.time=nn.Sequential(nn.Linear(c.dim,c.dim),nn.SiLU(),nn.Linear(c.dim,c.dim))
|
| 414 |
+
self.blocks=nn.ModuleList([Block(c.dim,c.heads) for _ in range(c.depth)])
|
| 415 |
+
self.norm=nn.LayerNorm(c.dim)
|
| 416 |
+
self.local_norm=nn.LayerNorm(c.local_dim)
|
| 417 |
+
self.local_conv=nn.Conv2d(c.local_dim,c.local_dim,3,padding=1,groups=c.local_dim)
|
| 418 |
+
self.image_head=nn.Linear(c.local_dim,4 if planner else 256)
|
| 419 |
+
self.text_head=nn.Linear(c.dim,258,bias=False) if not planner else None
|
| 420 |
+
self.order_head=nn.Linear(c.dim,3) if planner else None
|
| 421 |
+
gy,gx=torch.meshgrid(torch.arange(c.size//c.patch)*c.patch+c.patch//2,
|
| 422 |
+
torch.arange(c.size//c.patch)*c.patch+c.patch//2,indexing='ij')
|
| 423 |
+
self.register_buffer('gx',gx.flatten(),persistent=False); self.register_buffer('gy',gy.flatten(),persistent=False)
|
| 424 |
+
self.apply(self._init)
|
| 425 |
+
for block in self.blocks:
|
| 426 |
+
nn.init.normal_(block.proj.weight,std=.02/math.sqrt(2*c.depth))
|
| 427 |
+
nn.init.normal_(block.down.weight,std=.02/math.sqrt(2*c.depth))
|
| 428 |
+
@staticmethod
|
| 429 |
+
def _init(m):
|
| 430 |
+
if isinstance(m,(nn.Linear,nn.Embedding)):
|
| 431 |
+
nn.init.normal_(m.weight,std=.02)
|
| 432 |
+
if isinstance(m,nn.Linear) and m.bias is not None: nn.init.zeros_(m.bias)
|
| 433 |
+
def forward(self,image,text,layout,prompt,coords,regions,encoding,t):
|
| 434 |
+
c=self.c; b=image.shape[0]; s=c.size; p=c.patch; g=s//p
|
| 435 |
+
local=self.local(self.byte(image))+self.occupancy(layout)
|
| 436 |
+
patches=local.reshape(b,g,p,g,p,c.local_dim).permute(0,1,3,2,4,5).reshape(b,g*g,-1)
|
| 437 |
+
spatial=self.to_patch(patches)+self.x(self.gx)+self.y(self.gy)+self.modality.weight[0]+self.format(encoding)[:,None]
|
| 438 |
+
words=self.byte(text)+self.modality.weight[1]
|
| 439 |
+
if not self.planner:
|
| 440 |
+
words=words+self.y(coords[...,0])+self.x(coords[...,1])
|
| 441 |
+
if c.variant!='no_flow':
|
| 442 |
+
flow=positional(torch.arange(c.text_len,device=image.device),c.dim)
|
| 443 |
+
words=words+flow[None]
|
| 444 |
+
words=words+self.region(regions.clamp(0,256))
|
| 445 |
+
else:
|
| 446 |
+
words=words+positional(torch.arange(c.text_len,device=image.device),c.dim)[None]
|
| 447 |
+
condition=self.byte(prompt)+self.modality.weight[2]
|
| 448 |
+
condition=condition+positional(torch.arange(c.prompt_len,device=image.device),c.dim)[None]
|
| 449 |
+
h=torch.cat((spatial,words,condition),1)+self.time(positional(t*1000,c.dim))[:,None]
|
| 450 |
+
for block in self.blocks:
|
| 451 |
+
if c.checkpoint and self.training:
|
| 452 |
+
from torch.utils.checkpoint import checkpoint
|
| 453 |
+
h=checkpoint(block,h,use_reentrant=False)
|
| 454 |
+
else: h=block(h)
|
| 455 |
+
h=self.norm(h)
|
| 456 |
+
z=self.to_cells(h[:,:g*g]).reshape(b,g,g,p,p,c.local_dim).permute(0,1,3,2,4,5).reshape(b,s,s,c.local_dim)
|
| 457 |
+
z=z+local
|
| 458 |
+
z=z+self.local_conv(z.permute(0,3,1,2)).permute(0,2,3,1)
|
| 459 |
+
img=self.image_head(self.local_norm(z))
|
| 460 |
+
if self.planner:
|
| 461 |
+
return img,self.order_head(h[:,-c.prompt_len:].mean(1))
|
| 462 |
+
return img,self.text_head(h[:,g*g:g*g+c.text_len])
|
| 463 |
+
|
| 464 |
+
def stratified_time(batch,device):
|
| 465 |
+
# Forward time: 0=all masked, 1=data. A random shift stratifies the batch.
|
| 466 |
+
return ((torch.rand((),device=device)+torch.arange(batch,device=device)/batch)%1)*.998+.001
|
| 467 |
+
|
| 468 |
+
def conditioning(batch):
|
| 469 |
+
"""Task dropout: joint / image->text / text->image / arbitrary infill."""
|
| 470 |
+
img=batch['image']; txt=batch['text']; device=img.device; b=len(img)
|
| 471 |
+
mode=torch.randint(0,4,(b,),device=device)
|
| 472 |
+
has_image=(batch['layout']==IMAGE).flatten(1).any(-1)
|
| 473 |
+
has_text=(batch['layout']==TEXT).flatten(1).any(-1)
|
| 474 |
+
# Do not ask a text-only page to generate an absent image (or vice versa).
|
| 475 |
+
mode=torch.where(((mode==2)&~has_image)|((mode==1)&~has_text),0,mode)
|
| 476 |
+
ki=(mode==1)[:,None,None].expand_as(img).clone()
|
| 477 |
+
kt=(mode==2)[:,None].expand_as(txt).clone()
|
| 478 |
+
ki|=(mode==3)[:,None,None] & (torch.rand(img.shape,device=device)<.35)
|
| 479 |
+
kt|=(mode==3)[:,None] & (torch.rand(txt.shape,device=device)<.35)
|
| 480 |
+
ki&=batch['layout']==IMAGE
|
| 481 |
+
kt|=(~has_text)[:,None]
|
| 482 |
+
return ki,kt
|
| 483 |
+
|
| 484 |
+
def masked_ce(logits,target,masked,eligible,t,objective,weights=None):
|
| 485 |
+
"""Explicit modality-normalized posterior CE, optionally 1/(1-t) weighted.
|
| 486 |
+
|
| 487 |
+
MDLM mode uses the absorbing linear-path time weight; optional class/PAD
|
| 488 |
+
balancing makes the overall canvas objective a surrogate, not a reported ELBO.
|
| 489 |
+
"""
|
| 490 |
+
ce=F.cross_entropy(logits.float().reshape(-1,logits.shape[-1]),target.reshape(-1),reduction='none').reshape_as(target)
|
| 491 |
+
w=eligible.float() if weights is None else eligible.float()*weights
|
| 492 |
+
den=w.flatten(1).sum(-1).clamp_min(1)
|
| 493 |
+
value=(ce*w*masked).flatten(1).sum(-1)/den
|
| 494 |
+
if objective=='mdlm': value=value/(1-t)
|
| 495 |
+
return value.mean()
|
| 496 |
+
|
| 497 |
+
def make_loss(model,batch,planner=False,objective='dfm',fixed_t=None):
|
| 498 |
+
b=len(batch['image']); dev=batch['image'].device
|
| 499 |
+
t=stratified_time(b,dev) if fixed_t is None else torch.full((b,),fixed_t,device=dev)
|
| 500 |
+
ki,kt=conditioning(batch); active=batch['layout']==IMAGE
|
| 501 |
+
im=torch.where(ki,batch['image'],MASK); txt=torch.where(kt,batch['text'],MASK)
|
| 502 |
+
if planner:
|
| 503 |
+
# Observed symbols constrain occupancy; unknown cells carry no GT class.
|
| 504 |
+
known=ki.clone()
|
| 505 |
+
known|=torch.rand(active.shape,device=dev)<.10
|
| 506 |
+
noise=(torch.rand(active.shape,device=dev)>t[:,None,None]) & ~known
|
| 507 |
+
layout=torch.where(noise,UNKNOWN,batch['layout'])
|
| 508 |
+
logits,order=model(im,txt,layout,batch['prompt'],batch['coords'],batch['regions'],batch['encoding'],t)
|
| 509 |
+
weights=torch.tensor([1.,1.,6.,2.],device=dev)[batch['layout']]
|
| 510 |
+
loss=masked_ce(logits,batch['layout'],noise,~known,t,objective,weights)
|
| 511 |
+
loss=loss+.15*F.cross_entropy(order.float(),batch['order'])
|
| 512 |
+
return loss
|
| 513 |
+
mi=(torch.rand(active.shape,device=dev)>t[:,None,None])&~ki&active
|
| 514 |
+
mt=(torch.rand(txt.shape,device=dev)>t[:,None])&~kt
|
| 515 |
+
im=torch.where(mi,MASK,batch['image']); im=torch.where(active,im,PAD)
|
| 516 |
+
txt=torch.where(mt,MASK,batch['text'])
|
| 517 |
+
il,tl=model(im,txt,batch['layout'],batch['prompt'],batch['coords'],batch['regions'],batch['encoding'],t)
|
| 518 |
+
text_weight=torch.where(batch['text']==PAD,.05,1.)
|
| 519 |
+
li=masked_ce(il,batch['image'],mi,active&~ki,t,objective)
|
| 520 |
+
lt=masked_ce(tl,batch['text'],mt,~kt,t,objective,text_weight)
|
| 521 |
+
return li+lt
|
| 522 |
+
|
| 523 |
+
def autocast(device,enabled=True):
|
| 524 |
+
return torch.autocast('cuda',dtype=torch.bfloat16,enabled=enabled) if str(device).startswith('cuda') else contextlib.nullcontext()
|
| 525 |
+
|
| 526 |
+
def sample_categorical(logits,temperature=1.,greedy=False):
|
| 527 |
+
if greedy or temperature==0: return logits.argmax(-1)
|
| 528 |
+
probs=(logits.float()/temperature).softmax(-1)
|
| 529 |
+
return torch.multinomial(probs.reshape(-1,probs.shape[-1]),1).reshape(probs.shape[:-1])
|
| 530 |
+
|
| 531 |
+
@torch.no_grad()
|
| 532 |
+
def sample_layout(model,batch,steps=8,known_layout=None,known_image=None,known_text=None,greedy=False):
|
| 533 |
+
if steps<1: raise ValueError('At least one sampling step is required')
|
| 534 |
+
c=model.c; dev=batch['image'].device; b=len(batch['image'])
|
| 535 |
+
# A known layout uses UNKNOWN at unconstrained cells, including free cells.
|
| 536 |
+
lay=torch.full_like(batch['layout'],UNKNOWN) if known_layout is None else known_layout.clone()
|
| 537 |
+
locked=lay!=UNKNOWN
|
| 538 |
+
im=torch.full_like(batch['image'],MASK) if known_image is None else known_image
|
| 539 |
+
txt=torch.full_like(batch['text'],MASK) if known_text is None else known_text
|
| 540 |
+
was=model.training; model.eval()
|
| 541 |
+
for i in range(steps):
|
| 542 |
+
t=torch.full((b,),i/steps,device=dev)
|
| 543 |
+
with autocast(dev): logits,order=model(im,txt,lay,batch['prompt'],batch['coords'],batch['regions'],batch['encoding'],t)
|
| 544 |
+
proposals=sample_categorical(logits,greedy=greedy)
|
| 545 |
+
# Integrated linear-path reveal hazard: (t_next-t)/(1-t).
|
| 546 |
+
reveal=(torch.rand(lay.shape,device=dev)<1/(steps-i))&(lay==UNKNOWN)&~locked
|
| 547 |
+
lay=torch.where(reveal,proposals,lay)
|
| 548 |
+
model.train(was)
|
| 549 |
+
return lay,order.argmax(-1)
|
| 550 |
+
|
| 551 |
+
def remap_batch(batch,layout,order,variant='full'):
|
| 552 |
+
result=dict(batch); result['layout']=layout
|
| 553 |
+
maps=[capacity_map(l.cpu().numpy(),batch['text'].shape[1],
|
| 554 |
+
'row' if variant=='raster' else POLICIES[int(o)]) for l,o in zip(layout,order)]
|
| 555 |
+
result['coords']=torch.as_tensor(np.stack([m[0] for m in maps]),device=layout.device).long()
|
| 556 |
+
result['regions']=torch.as_tensor(np.stack([m[1] for m in maps]),device=layout.device).long()
|
| 557 |
+
result['capacity']=torch.as_tensor(np.stack([m[2] for m in maps]),device=layout.device)
|
| 558 |
+
return result
|
| 559 |
+
|
| 560 |
+
@torch.no_grad()
|
| 561 |
+
def sample_content(model,batch,steps=8,known_image=None,known_text=None,greedy=False):
|
| 562 |
+
if steps<1: raise ValueError('At least one sampling step is required')
|
| 563 |
+
dev=batch['image'].device; b=len(batch['image']); active=batch['layout']==IMAGE
|
| 564 |
+
im=torch.full_like(batch['image'],MASK) if known_image is None else known_image.clone()
|
| 565 |
+
im=torch.where(active,im,PAD)
|
| 566 |
+
txt=torch.full_like(batch['text'],MASK) if known_text is None else known_text.clone()
|
| 567 |
+
no_text=~(batch['layout']==TEXT).flatten(1).any(-1)
|
| 568 |
+
empty=torch.full_like(txt,PAD); empty[:,0]=EOS
|
| 569 |
+
# A layout with no text region has an empty new text lane. Explicit supplied
|
| 570 |
+
# text is preserved so overflow can be reported rather than silently erased.
|
| 571 |
+
if known_text is None:
|
| 572 |
+
txt=torch.where(no_text[:,None],empty,txt)
|
| 573 |
+
else:
|
| 574 |
+
entirely_unknown=(known_text==MASK).all(-1)
|
| 575 |
+
txt=torch.where((no_text&entirely_unknown)[:,None],empty,txt)
|
| 576 |
+
was=model.training; model.eval()
|
| 577 |
+
for i in range(steps):
|
| 578 |
+
t=torch.full((b,),i/steps,device=dev)
|
| 579 |
+
with autocast(dev): il,tl=model(im,txt,batch['layout'],batch['prompt'],batch['coords'],batch['regions'],batch['encoding'],t)
|
| 580 |
+
# Image glyphs are printable ASCII; text remains unrestricted UTF-8 bytes.
|
| 581 |
+
allowed=torch.arange(256,device=dev)
|
| 582 |
+
il=il.float().masked_fill((batch['encoding']==1)[:,None,None,None]&((allowed<32)|(allowed>126)), -torch.inf)
|
| 583 |
+
tl=tl.float() # Preserve all 256 text bytes, EOS and PAD.
|
| 584 |
+
ip=sample_categorical(il,greedy=greedy); tp=sample_categorical(tl,greedy=greedy)
|
| 585 |
+
ri=(torch.rand(im.shape,device=dev)<1/(steps-i))&(im==MASK)&active
|
| 586 |
+
rt=(torch.rand(txt.shape,device=dev)<1/(steps-i))&(txt==MASK)
|
| 587 |
+
im=torch.where(ri,ip,im); txt=torch.where(rt,tp,txt)
|
| 588 |
+
model.train(was)
|
| 589 |
+
return torch.where(active,im,0),txt
|
| 590 |
+
|
| 591 |
+
def rng_state():
|
| 592 |
+
return dict(torch=torch.get_rng_state(),cuda=torch.cuda.get_rng_state_all() if torch.cuda.is_available() else [],
|
| 593 |
+
numpy=np.random.get_state(),python=random.getstate())
|
| 594 |
+
|
| 595 |
+
def restore_rng(state):
|
| 596 |
+
torch.set_rng_state(state['torch'].cpu()); np.random.set_state(state['numpy']); random.setstate(state['python'])
|
| 597 |
+
if state['cuda']: torch.cuda.set_rng_state_all([x.cpu() for x in state['cuda']])
|
| 598 |
+
|
| 599 |
+
def cpu_tree(x):
|
| 600 |
+
if torch.is_tensor(x): return x.detach().cpu().clone()
|
| 601 |
+
if isinstance(x,dict): return {k:cpu_tree(v) for k,v in x.items()}
|
| 602 |
+
if isinstance(x,list): return [cpu_tree(v) for v in x]
|
| 603 |
+
if isinstance(x,tuple): return tuple(cpu_tree(v) for v in x)
|
| 604 |
+
return x
|
| 605 |
+
|
| 606 |
+
def save_checkpoint(path,model,opt,step,signature,extra):
|
| 607 |
+
path=Path(path); path.parent.mkdir(parents=True,exist_ok=True)
|
| 608 |
+
payload=dict(model=cpu_tree(model.state_dict()),optimizer=cpu_tree(opt.state_dict()),step=step,
|
| 609 |
+
rng=rng_state(),signature=signature,extra=extra,config=asdict(model.c),planner=model.planner)
|
| 610 |
+
tmp=path.with_suffix('.tmp'); torch.save(payload,tmp)
|
| 611 |
+
# Previous generation survives an interrupted write or corrupt last file.
|
| 612 |
+
if path.exists(): os.replace(path,path.with_suffix('.previous.pt'))
|
| 613 |
+
os.replace(tmp,path)
|
| 614 |
+
|
| 615 |
+
def read_checkpoint(path,signature=None):
|
| 616 |
+
path=Path(path); errors=[]
|
| 617 |
+
for candidate in (path,path.with_suffix('.previous.pt')):
|
| 618 |
+
if not candidate.exists(): continue
|
| 619 |
+
try: value=torch.load(candidate,map_location='cpu',weights_only=False)
|
| 620 |
+
except (OSError,RuntimeError,EOFError,ValueError,IndexError,pickle.UnpicklingError) as e:
|
| 621 |
+
errors.append(str(e)); continue
|
| 622 |
+
if signature is not None and value['signature']!=signature:
|
| 623 |
+
raise ValueError('Checkpoint config/data/code/runtime differs. Use a new run directory.')
|
| 624 |
+
return value
|
| 625 |
+
raise RuntimeError(f'No readable checkpoint: {errors}')
|
| 626 |
+
|
| 627 |
+
def finite_tensors(tensors):
|
| 628 |
+
tensors=[x for x in tensors if x.numel()]
|
| 629 |
+
if not tensors: return True
|
| 630 |
+
# One host synchronization, not one per tensor.
|
| 631 |
+
return bool(torch.stack([torch.isfinite(x).all() for x in tensors]).all())
|
| 632 |
+
|
| 633 |
+
def finite_optimizer(opt):
|
| 634 |
+
return finite_tensors([v for s in opt.state.values() for v in s.values() if torch.is_tensor(v)])
|
| 635 |
+
|
| 636 |
+
def batch_indices(step,batch_size,accum,n,seed):
|
| 637 |
+
# A full deterministic permutation per epoch; resume depends only on accepted step.
|
| 638 |
+
ids=np.arange(step*batch_size*accum,(step+1)*batch_size*accum)
|
| 639 |
+
result=np.empty_like(ids)
|
| 640 |
+
for epoch in np.unique(ids//n):
|
| 641 |
+
sel=ids//n==epoch
|
| 642 |
+
perm=np.random.default_rng(np.random.SeedSequence([seed,int(epoch)])).permutation(n)
|
| 643 |
+
result[sel]=perm[ids[sel]%n]
|
| 644 |
+
return result.reshape(accum,batch_size)
|
| 645 |
+
|
| 646 |
+
def synchronize(device):
|
| 647 |
+
if str(device).startswith('cuda'): torch.cuda.synchronize()
|
| 648 |
+
|
| 649 |
+
def train(root,run,c,steps=300,batch_size=4,accum=1,lr=3e-4,seed=42,planner=False,
|
| 650 |
+
compile_model=False,preload=False,save_every=50,resume=True,device=None,
|
| 651 |
+
stop_after=None,inject_nonfinite_at=None,inject_poststep_at=None):
|
| 652 |
+
"""Single-GPU training. No train-time diffusion rollouts; one time per sample.
|
| 653 |
+
|
| 654 |
+
stop_after is for exact-resume tests, and does not alter the LR schedule.
|
| 655 |
+
Fallback changes are counted, logged, and invalidate strict untouched comparisons.
|
| 656 |
+
"""
|
| 657 |
+
device=device or ('cuda' if torch.cuda.is_available() else 'cpu')
|
| 658 |
+
run=Path(run); run.mkdir(parents=True,exist_ok=True); seed_all(seed)
|
| 659 |
+
if str(device).startswith('cuda'):
|
| 660 |
+
if not torch.cuda.is_bf16_supported(): raise RuntimeError('This profile requires CUDA BF16 support')
|
| 661 |
+
torch.set_float32_matmul_precision('high')
|
| 662 |
+
data=CanvasData(root,'train',device,preload)
|
| 663 |
+
c.validate(); model=CanvasModel(c,planner).to(device)
|
| 664 |
+
opt=torch.optim.AdamW(model.parameters(),lr=lr,betas=(.9,.95),weight_decay=.1,
|
| 665 |
+
fused=str(device).startswith('cuda'))
|
| 666 |
+
# Architecture-dependent parameter initialization must not change paired
|
| 667 |
+
# task/corruption draws across ablations with different projection sizes.
|
| 668 |
+
seed_all(seed+1_000_000)
|
| 669 |
+
signature=digest(dict(config=asdict(c),planner=planner,data=data.manifest['fingerprint'],steps=steps,
|
| 670 |
+
batch_size=batch_size,accum=accum,lr=lr,seed=seed,torch=torch.__version__,
|
| 671 |
+
device_type=str(device).split(':')[0],compile=compile_model,
|
| 672 |
+
code=hashlib.sha256(Path(__file__).read_bytes()).hexdigest()))
|
| 673 |
+
last=run/'last.pt'; start=0; retries=0; rollbacks=0; lr_mult=1.; ema=0.
|
| 674 |
+
if resume and last.exists():
|
| 675 |
+
state=read_checkpoint(last,signature); model.load_state_dict(state['model']); opt.load_state_dict(state['optimizer'])
|
| 676 |
+
restore_rng(state['rng']); start=state['step']; extra=state['extra']
|
| 677 |
+
retries=extra['retries']; rollbacks=extra['rollbacks']; lr_mult=extra['lr_mult']; ema=extra['grad_ema']
|
| 678 |
+
if start==steps and (run/'summary.json').exists():
|
| 679 |
+
saved=json.loads((run/'summary.json').read_text())
|
| 680 |
+
if saved.get('signature')==signature:
|
| 681 |
+
print(f'Already complete: {run}; retaining measured timing summary',flush=True)
|
| 682 |
+
return model,saved
|
| 683 |
+
elif last.exists(): raise FileExistsError('Refusing to overwrite existing run; use a fresh directory')
|
| 684 |
+
else:
|
| 685 |
+
save_checkpoint(last,model,opt,0,signature,dict(retries=0,rollbacks=0,lr_mult=1.,grad_ema=0.))
|
| 686 |
+
forward=torch.compile(model,dynamic=False) if compile_model else model
|
| 687 |
+
compile_active=compile_model
|
| 688 |
+
print(json.dumps(dict(run=str(run),planner=planner,params=sum(p.numel() for p in model.parameters()),
|
| 689 |
+
attention_tokens=(c.size//c.patch)**2+c.text_len+c.prompt_len,device=str(device),start=start)),flush=True)
|
| 690 |
+
step=start; wall=time.perf_counter(); timings=[]; injected=False; injected_post=False
|
| 691 |
+
log=run/'train.jsonl'
|
| 692 |
+
if str(device).startswith('cuda'): torch.cuda.reset_peak_memory_stats()
|
| 693 |
+
while step<steps and (stop_after is None or step<stop_after):
|
| 694 |
+
ids=batch_indices(step,batch_size,accum,data.n,seed)
|
| 695 |
+
batches=[data.batch(i,c.variant) for i in ids]
|
| 696 |
+
replay=rng_state(); base_lr=lr*min(1.,(step+1)/min(30,max(1,steps//10)))
|
| 697 |
+
base_lr*=.1+.9*.5*(1+math.cos(math.pi*step/max(1,steps)))
|
| 698 |
+
accepted=False; attempt_records=[]; synchronize(device); begin=time.perf_counter()
|
| 699 |
+
try:
|
| 700 |
+
for attempt,(amp,backoff,clip) in enumerate(((True,1.,1.),(True,.25,.5),(False,.1,.25))):
|
| 701 |
+
restore_rng(replay); opt.zero_grad(set_to_none=True); loss_value=0.
|
| 702 |
+
for group in opt.param_groups: group['lr']=base_lr*lr_mult*backoff
|
| 703 |
+
good=True
|
| 704 |
+
for batch in batches:
|
| 705 |
+
with autocast(device,amp): loss=make_loss(forward,batch,planner,c.objective)/accum
|
| 706 |
+
if not bool(torch.isfinite(loss)):
|
| 707 |
+
good=False; break
|
| 708 |
+
loss_value+=float(loss.detach()); loss.backward()
|
| 709 |
+
if inject_nonfinite_at==step and not injected:
|
| 710 |
+
next(p for p in model.parameters() if p.grad is not None).grad.flatten()[0]=torch.inf
|
| 711 |
+
injected=True
|
| 712 |
+
params=[p for p in model.parameters() if p.grad is not None]
|
| 713 |
+
good=good and finite_tensors([p.grad for p in params])
|
| 714 |
+
norm=float(torch.nn.utils.clip_grad_norm_(params,clip)) if good else float('inf')
|
| 715 |
+
spike=math.isfinite(norm) and norm>max(100.,20*ema)
|
| 716 |
+
if not good or not math.isfinite(norm) or (spike and attempt==0):
|
| 717 |
+
retries+=1; attempt_records.append(dict(attempt=attempt,reason='spike' if spike else 'nonfinite'))
|
| 718 |
+
continue
|
| 719 |
+
opt.step()
|
| 720 |
+
if inject_poststep_at==step and not injected_post:
|
| 721 |
+
with torch.no_grad(): next(model.parameters()).flatten()[0]=torch.nan
|
| 722 |
+
injected_post=True
|
| 723 |
+
if not finite_tensors(list(model.parameters())) or not finite_optimizer(opt):
|
| 724 |
+
raise FloatingPointError('Nonfinite parameter or optimizer state after update')
|
| 725 |
+
# Reduced LR affects this update only; all fallback events are counted.
|
| 726 |
+
accepted=True; ema=norm if ema==0 else .95*ema+.05*norm; break
|
| 727 |
+
if not accepted: raise FloatingPointError('All precision/clip/LR fallbacks exhausted')
|
| 728 |
+
except torch.OutOfMemoryError:
|
| 729 |
+
opt.zero_grad(set_to_none=True)
|
| 730 |
+
raise RuntimeError(f'CUDA OOM. Last known-good checkpoint: {last}. Reduce the common batch or enable checkpointing in a NEW suite; no examples were silently skipped.')
|
| 731 |
+
except FloatingPointError as exc:
|
| 732 |
+
rollbacks+=1
|
| 733 |
+
if rollbacks>3: raise RuntimeError(f'Recovery budget exhausted: {exc}') from exc
|
| 734 |
+
state=read_checkpoint(last,signature); model.load_state_dict(state['model']); opt.load_state_dict(state['optimizer'])
|
| 735 |
+
restore_rng(state['rng']); step=state['step']; ema=state['extra']['grad_ema']; lr_mult*=.25
|
| 736 |
+
with open(log,'a') as f: f.write(json.dumps(dict(event='rollback',step=step,reason=str(exc),lr_mult=lr_mult))+'\n')
|
| 737 |
+
continue
|
| 738 |
+
except Exception as exc:
|
| 739 |
+
if compile_active and (type(exc).__module__.startswith(('torch._dynamo','torch._inductor'))):
|
| 740 |
+
compile_active=False; forward=model; restore_rng(replay)
|
| 741 |
+
with open(log,'a') as f: f.write(json.dumps(dict(event='compile_fallback',reason=str(exc)[:300]))+'\n')
|
| 742 |
+
continue
|
| 743 |
+
raise
|
| 744 |
+
synchronize(device); elapsed=time.perf_counter()-begin
|
| 745 |
+
step+=1
|
| 746 |
+
if step>start+3: timings.append(elapsed)
|
| 747 |
+
record=dict(step=step,loss=loss_value,grad_norm=norm,seconds=elapsed,lr=opt.param_groups[0]['lr'],
|
| 748 |
+
retries=retries,rollbacks=rollbacks,attempts=attempt_records)
|
| 749 |
+
with open(log,'a') as f: f.write(json.dumps(record,allow_nan=False)+'\n')
|
| 750 |
+
if step==1 or step%25==0: print(json.dumps(record),flush=True)
|
| 751 |
+
if step%save_every==0 or step==steps or step==stop_after:
|
| 752 |
+
save_checkpoint(last,model,opt,step,signature,dict(retries=retries,rollbacks=rollbacks,lr_mult=lr_mult,grad_ema=ema))
|
| 753 |
+
summary=dict(config=asdict(c),planner=planner,seed=seed,step=step,target_steps=steps,params=sum(p.numel() for p in model.parameters()),
|
| 754 |
+
accepted_pages=step*batch_size*accum,batch_size=batch_size,accum=accum,
|
| 755 |
+
retries=retries,rollbacks=rollbacks,unchanged_recipe=retries==0 and rollbacks==0,
|
| 756 |
+
resume_from=start,session_seconds=time.perf_counter()-wall,
|
| 757 |
+
median_step_seconds=float(np.median(timings)) if timings else None,
|
| 758 |
+
pages_per_second=batch_size*accum/float(np.median(timings)) if timings else None,
|
| 759 |
+
peak_gpu_GB=torch.cuda.max_memory_allocated()/1e9 if str(device).startswith('cuda') else None,
|
| 760 |
+
compile_requested=compile_model,compile_active=compile_active,signature=signature,
|
| 761 |
+
data_fingerprint=data.manifest['fingerprint'],device=str(device),torch=torch.__version__)
|
| 762 |
+
atomic_json(run/'summary.json',summary)
|
| 763 |
+
return model,summary
|
| 764 |
+
|
| 765 |
+
def load_model(path,device=None):
|
| 766 |
+
device=device or ('cuda' if torch.cuda.is_available() else 'cpu')
|
| 767 |
+
ck=read_checkpoint(path); c=Config(**ck['config']); model=CanvasModel(c,ck['planner']).to(device)
|
| 768 |
+
model.load_state_dict(ck['model']); model.eval(); return model
|
| 769 |
+
|
| 770 |
+
@torch.no_grad()
|
| 771 |
+
def evaluate(model,data,planner_model=None,batches=4,batch_size=2,steps=8):
|
| 772 |
+
"""Loss probes and true rollout metrics. Oracle geometry is labeled explicitly."""
|
| 773 |
+
was=model.training; model.eval(); device=next(model.parameters()).device
|
| 774 |
+
losses=[]; text_hits=0; text_count=0; image_hits=0; image_count=0
|
| 775 |
+
dot_correct=0; dot_count=0; known_checks=0; known_total=0; overflow=0
|
| 776 |
+
intersections=0; unions=0; generated_pages=0; invalid_padding=0; elapsed=0.
|
| 777 |
+
prior=rng_state(); seed_all(2029)
|
| 778 |
+
try:
|
| 779 |
+
for i in range(batches):
|
| 780 |
+
b=data.batch(np.arange(i*batch_size,(i+1)*batch_size),model.c.variant)
|
| 781 |
+
for t in (.1,.5,.9):
|
| 782 |
+
# A common unweighted probe is comparable across training objectives.
|
| 783 |
+
with autocast(device): losses.append(float(make_loss(model,b,False,'dfm',t)))
|
| 784 |
+
if planner_model is not None:
|
| 785 |
+
synchronize(device); begin=time.perf_counter()
|
| 786 |
+
layout,order=sample_layout(planner_model,b,steps)
|
| 787 |
+
used=remap_batch(b,layout,order,model.c.variant)
|
| 788 |
+
else:
|
| 789 |
+
synchronize(device); begin=time.perf_counter(); used=b; layout=b['layout']; order=b['order']
|
| 790 |
+
img,txt=sample_content(model,used,steps)
|
| 791 |
+
synchronize(device); elapsed+=time.perf_counter()-begin
|
| 792 |
+
active=b['layout']==IMAGE; predicted=layout==IMAGE
|
| 793 |
+
intersections+=int((active&predicted).sum()); unions+=int((active|predicted).sum())
|
| 794 |
+
image_hits+=int(((img==b['image'])&active&predicted).sum()); image_count+=int(active.sum())
|
| 795 |
+
mask=b['text']!=PAD; text_hits+=int(((txt==b['text'])&mask).sum()); text_count+=int(mask.sum())
|
| 796 |
+
for j in range(len(img)):
|
| 797 |
+
truth=b['image'][j].cpu().numpy().astype(np.uint8); guess=img[j].cpu().numpy().astype(np.uint8)
|
| 798 |
+
if int(b['encoding'][j])==0:
|
| 799 |
+
bits=unpack_braille(truth)^unpack_braille(guess)
|
| 800 |
+
eval_mask=np.repeat(np.repeat((active[j]|predicted[j]).cpu().numpy(),4,0),2,1)
|
| 801 |
+
dot_correct+=int((~bits&eval_mask).sum()); dot_count+=int(eval_mask.sum())
|
| 802 |
+
pol='row' if model.c.variant=='raster' else POLICIES[int(order[j])]
|
| 803 |
+
wrapped=wrap_words(decode_text(txt[j].cpu().numpy()).replace('\ufffd','?'),layout[j].cpu().numpy(),pol)
|
| 804 |
+
overflow+=int(bool(wrapped['overflow'])); generated_pages+=1
|
| 805 |
+
ts=txt[j].cpu().numpy(); end=np.flatnonzero(ts==EOS)
|
| 806 |
+
invalid_padding+=int(bool(np.any(ts[:end[0] if len(end) else len(ts)]==PAD)))
|
| 807 |
+
# A separate arbitrary partial-content test verifies clamping exactly.
|
| 808 |
+
ki=torch.rand(b['image'].shape,device=device)<.2; kt=torch.rand(b['text'].shape,device=device)<.2
|
| 809 |
+
known_i=torch.where(ki,b['image'],MASK); known_t=torch.where(kt,b['text'],MASK)
|
| 810 |
+
ci,ct=sample_content(model,b,min(steps,2),known_i,known_t)
|
| 811 |
+
ki&=active
|
| 812 |
+
known_checks+=int(((ci==b['image'])&ki).sum()+((ct==b['text'])&kt).sum())
|
| 813 |
+
known_total+=int(ki.sum()+kt.sum())
|
| 814 |
+
finally: restore_rng(prior); model.train(was)
|
| 815 |
+
return dict(geometry='predicted' if planner_model is not None else 'oracle_diagnostic',sampling_steps=steps,
|
| 816 |
+
content_nfe=steps,planner_nfe=steps if planner_model is not None else 0,
|
| 817 |
+
common_masked_ce=float(np.mean(losses)),text_byte_accuracy=text_hits/max(1,text_count),
|
| 818 |
+
image_cell_accuracy=image_hits/max(1,image_count),image_occupancy_iou=intersections/max(1,unions),
|
| 819 |
+
braille_dot_accuracy=dot_correct/max(1,dot_count),known_preservation=known_checks/max(1,known_total),
|
| 820 |
+
overflow_page_rate=overflow/max(1,generated_pages),early_pad_page_rate=invalid_padding/max(1,generated_pages),
|
| 821 |
+
generation_seconds_per_page=elapsed/max(1,generated_pages),pages=generated_pages,
|
| 822 |
+
metric_note='Exact target accuracy is a paired diagnostic; multiple valid texts/layouts exist. This is not perplexity or FID.')
|
| 823 |
+
|
| 824 |
+
def benchmark(root,c,device='cuda',batches=(1,2,4,8),repeats=5):
|
| 825 |
+
"""Actual forward+backward+Adam timing, including steady-state memory.
|
| 826 |
+
|
| 827 |
+
A disposable model keeps tuning updates out of the experiment checkpoints.
|
| 828 |
+
Run per candidate variant; choose ONE common batch for controlled comparisons.
|
| 829 |
+
"""
|
| 830 |
+
results=[]
|
| 831 |
+
for batch_size in batches:
|
| 832 |
+
model=opt=data=batch=None
|
| 833 |
+
try:
|
| 834 |
+
seed_all(42); model=CanvasModel(c).to(device)
|
| 835 |
+
opt=torch.optim.AdamW(model.parameters(),lr=1e-4,fused=str(device).startswith('cuda'))
|
| 836 |
+
data=CanvasData(root,'train',device); batch=data.batch(np.arange(batch_size),c.variant)
|
| 837 |
+
if str(device).startswith('cuda'): torch.cuda.reset_peak_memory_stats()
|
| 838 |
+
samples=[]
|
| 839 |
+
for j in range(repeats+2):
|
| 840 |
+
synchronize(device); begin=time.perf_counter(); opt.zero_grad(set_to_none=True)
|
| 841 |
+
with autocast(device): loss=make_loss(model,batch,objective=c.objective)
|
| 842 |
+
loss.backward(); torch.nn.utils.clip_grad_norm_(model.parameters(),1.); opt.step(); synchronize(device)
|
| 843 |
+
if j>=2: samples.append(time.perf_counter()-begin)
|
| 844 |
+
med=float(np.median(samples))
|
| 845 |
+
results.append(dict(batch_size=batch_size,seconds=med,pages_per_second=batch_size/med,
|
| 846 |
+
peak_GB=torch.cuda.max_memory_allocated()/1e9 if str(device).startswith('cuda') else None))
|
| 847 |
+
except torch.OutOfMemoryError:
|
| 848 |
+
results.append(dict(batch_size=batch_size,oom=True)); break
|
| 849 |
+
finally:
|
| 850 |
+
del model,opt,data,batch
|
| 851 |
+
if str(device).startswith('cuda'): torch.cuda.empty_cache()
|
| 852 |
+
return results
|
| 853 |
+
|
| 854 |
+
def generate_page(model,planner,prompt,layout=None,image=None,image_known=None,
|
| 855 |
+
text=None,text_tokens=None,fixed_text=None,encoding=0,policy=None,
|
| 856 |
+
steps=8,planner_steps=8,seed=123,greedy=False,region_order=None):
|
| 857 |
+
"""Public generation API; no source example or ground truth is needed.
|
| 858 |
+
|
| 859 |
+
layout: full explicit occupancy, or UNKNOWN at unplanned cells.
|
| 860 |
+
image_known: arbitrary cell mask; zero-valued known Braille cells stay known.
|
| 861 |
+
text: optional complete ASCII text, or text_tokens with MASK for unknown bytes.
|
| 862 |
+
fixed_text: {(y,x): ASCII character}; immutable canvas text layer, separate
|
| 863 |
+
from the newly generated text lane. Planner is forbidden to overwrite it.
|
| 864 |
+
policy: optional reading order override; otherwise planner predicts one.
|
| 865 |
+
"""
|
| 866 |
+
c=model.c; dev=next(model.parameters()).device; s=c.size
|
| 867 |
+
if policy is not None and policy not in POLICIES: raise ValueError(policy)
|
| 868 |
+
if encoding not in (0,1): raise ValueError('encoding must be 0 Braille or 1 ASCII')
|
| 869 |
+
lay=np.full((s,s),UNKNOWN,np.int64) if layout is None else np.array(layout,dtype=np.int64,copy=True)
|
| 870 |
+
if lay.shape!=(s,s) or np.any((lay<0)|(lay>UNKNOWN)): raise ValueError('Invalid layout')
|
| 871 |
+
im=np.zeros((s,s),np.int64) if image is None else np.asarray(image,dtype=np.int64)
|
| 872 |
+
known=np.zeros((s,s),bool) if image_known is None else np.asarray(image_known,bool)
|
| 873 |
+
if im.shape!=(s,s) or known.shape!=(s,s) or np.any((im<0)|(im>255)): raise ValueError('Invalid image')
|
| 874 |
+
if np.any(known&(lay!=UNKNOWN)&(lay!=IMAGE)): raise ValueError('Known image conflicts with explicit occupancy')
|
| 875 |
+
lay[known]=IMAGE
|
| 876 |
+
fixed_text=fixed_text or {}
|
| 877 |
+
for (y,x),char in fixed_text.items():
|
| 878 |
+
if not (0<=y<s and 0<=x<s and len(char)==1 and 32<=ord(char)<127): raise ValueError('Invalid fixed text cell')
|
| 879 |
+
if known[y,x] or lay[y,x]==IMAGE: raise ValueError('Text/image overlap in user constraints')
|
| 880 |
+
lay[y,x]=RESERVED
|
| 881 |
+
if fixed_text:
|
| 882 |
+
context=''.join(fixed_text[p] for p in sorted(fixed_text))
|
| 883 |
+
prompt=f'{prompt} Existing text: {context}'
|
| 884 |
+
prompt.encode('ascii',errors='strict')
|
| 885 |
+
if len(prompt)>c.prompt_len: raise ValueError(f'Prompt exceeds {c.prompt_len} ASCII bytes; shorten it explicitly')
|
| 886 |
+
p=np.full(c.prompt_len,PAD,np.int64); p[:len(prompt)]=list(prompt.encode('ascii'))
|
| 887 |
+
kt=np.full(c.text_len,MASK,np.int64)
|
| 888 |
+
if text is not None and text_tokens is not None: raise ValueError('Pass text OR text_tokens')
|
| 889 |
+
if text is not None:
|
| 890 |
+
raw=text.encode('ascii',errors='strict')
|
| 891 |
+
if len(raw)>=c.text_len: raise ValueError('Text exceeds lane capacity including EOS')
|
| 892 |
+
kt[:]=PAD; kt[:len(raw)]=list(raw); kt[len(raw)]=EOS
|
| 893 |
+
if text_tokens is not None:
|
| 894 |
+
kt=np.asarray(text_tokens,np.int64)
|
| 895 |
+
if kt.shape!=(c.text_len,) or np.any((kt<0)|(kt>MASK)): raise ValueError('Invalid text_tokens')
|
| 896 |
+
tensor=lambda a:torch.as_tensor(a,device=dev).long()[None]
|
| 897 |
+
b=dict(image=tensor(im),layout=tensor(lay),text=tensor(kt),prompt=tensor(p),
|
| 898 |
+
coords=torch.zeros(1,c.text_len,2,dtype=torch.long,device=dev),
|
| 899 |
+
regions=torch.zeros(1,c.text_len,dtype=torch.long,device=dev),encoding=tensor(np.asarray(encoding)),
|
| 900 |
+
order=tensor(np.asarray(POLICIES.index(policy or 'columns'))))
|
| 901 |
+
old=rng_state(); seed_all(seed)
|
| 902 |
+
try:
|
| 903 |
+
known_i=tensor(np.where(known,im,MASK)); known_t=tensor(kt)
|
| 904 |
+
if np.any(lay==UNKNOWN):
|
| 905 |
+
if planner is None: raise ValueError('Unknown occupancy requires a trained planner')
|
| 906 |
+
planned,order=sample_layout(planner,b,planner_steps,b['layout'],known_i,known_t,greedy)
|
| 907 |
+
else: planned,order=b['layout'],b['order']
|
| 908 |
+
if policy is not None: order.fill_(POLICIES.index(policy))
|
| 909 |
+
mapped=remap_batch(b,planned,order,c.variant)
|
| 910 |
+
if region_order is not None:
|
| 911 |
+
cc,rr,_=capacity_map(planned[0].cpu().numpy(),c.text_len,POLICIES[int(order[0])],region_order)
|
| 912 |
+
mapped['coords']=tensor(cc); mapped['regions']=tensor(rr)
|
| 913 |
+
generated,tokens=sample_content(model,mapped,steps,known_i,known_t,greedy)
|
| 914 |
+
finally: restore_rng(old)
|
| 915 |
+
out_image=generated[0].cpu().numpy().astype(np.uint8); out_layout=planned[0].cpu().numpy().astype(np.uint8)
|
| 916 |
+
out_text=decode_text(tokens[0].cpu().numpy()).replace('\ufffd','?')
|
| 917 |
+
out_policy='row' if c.variant=='raster' else POLICIES[int(order[0])]
|
| 918 |
+
rendered=wrap_words(out_text,out_layout,out_policy,region_order)
|
| 919 |
+
# Final canvas retains fixed text cells exactly; byte indices apply to new lane.
|
| 920 |
+
canvas=np.full((s,s),' ',dtype='<U1')
|
| 921 |
+
for y,x in np.argwhere(out_layout==IMAGE): canvas[y,x]=chr(0x2800+int(out_image[y,x])) if encoding==0 else chr(int(out_image[y,x]))
|
| 922 |
+
for (y,x),char in zip(rendered['coords'],rendered['chars']): canvas[y,x]=char
|
| 923 |
+
for (y,x),char in fixed_text.items(): canvas[y,x]=char
|
| 924 |
+
return dict(canvas=canvas,image=out_image,layout=out_layout,text=out_text,
|
| 925 |
+
tokens=tokens[0].cpu().numpy(),flow_map=rendered,policy=out_policy,
|
| 926 |
+
known_image_preserved=bool(np.array_equal(out_image[known],im[known])),
|
| 927 |
+
content_nfe=steps,planner_nfe=planner_steps if np.any(lay==UNKNOWN) else 0)
|
| 928 |
+
|
| 929 |
+
def self_test():
|
| 930 |
+
"""Small CPU tests check contracts; they are not CUDA throughput evidence."""
|
| 931 |
+
torch.set_num_threads(min(4,os.cpu_count() or 1)); seed_all(3); results=[]
|
| 932 |
+
def ok(name): results.append(name); print('PASS',name,flush=True)
|
| 933 |
+
cells=np.arange(256,dtype=np.uint8).reshape(16,16)
|
| 934 |
+
assert np.array_equal(pack_braille(unpack_braille(cells)),cells)
|
| 935 |
+
for bit,(y,x) in enumerate(DOTS): assert unpack_braille(np.array([[1<<bit]],np.uint8))[y,x]
|
| 936 |
+
ok('all 256 Braille dot patterns round trip')
|
| 937 |
+
layout=np.full((24,24),TEXT,np.uint8); layout[:16,8:16]=IMAGE
|
| 938 |
+
runs=flow_runs(layout==TEXT,'columns')
|
| 939 |
+
assert runs[1][0]==1 and runs[1][1]==0 # Column-first, not raster hopping.
|
| 940 |
+
text='This is a test with several words beside a tall image.'
|
| 941 |
+
wrapped=wrap_words(text,layout,'columns')
|
| 942 |
+
assert wrapped['chars']==text and not wrapped['overflow']
|
| 943 |
+
assert len(set(wrapped['coords']))==len(wrapped['coords'])
|
| 944 |
+
assert all(layout[y,x]==TEXT for y,x in wrapped['coords'])
|
| 945 |
+
tiny=np.full((3,4),TEXT,np.uint8)
|
| 946 |
+
w=wrap_words('abcdefghijklmnopqrst',tiny); assert w['chars']+w['overflow']=='abcdefghijklmnopqrst'
|
| 947 |
+
assert wrap_words('hello',np.full((3,3),IMAGE))['overflow']=='hello'
|
| 948 |
+
ok('geometry regions, exact byte order, long-word splitting, explicit overflow')
|
| 949 |
+
for split in ('train','val','test','ood'):
|
| 950 |
+
for i in range(9):
|
| 951 |
+
e=make_example(i,split,size=32,text_len=64,prompt_len=96)
|
| 952 |
+
assert np.array_equal(e['coords'],capacity_map(e['layout'],64,e['meta']['policy'])[0])
|
| 953 |
+
w=wrap_words(e['meta']['text'],e['layout'],e['meta']['policy'])
|
| 954 |
+
assert not w['overflow'] and all(e['layout'][y,x]==TEXT for y,x in w['coords'])
|
| 955 |
+
assert make_example(0,'train')['meta']!=make_example(0,'val')['meta']
|
| 956 |
+
ok('deterministic split streams, arbitrary masks, no target-derived coordinates')
|
| 957 |
+
examples=[make_example(i,'train',16,32,64) for i in range(3)]
|
| 958 |
+
batch={k:torch.tensor(np.stack([e[k] for e in examples])).long() for k in ARRAY_KEYS}
|
| 959 |
+
config=Config(size=16,text_len=32,prompt_len=64,dim=32,heads=2,depth=2,patch=4,local_dim=8)
|
| 960 |
+
for planner in (False,True):
|
| 961 |
+
for variant in (('full','no_flow','raster','patch8','mdlm') if not planner else ('full',)):
|
| 962 |
+
c=replace(config,variant=variant,patch=8 if variant=='patch8' else 4,objective='mdlm' if variant=='mdlm' else 'dfm')
|
| 963 |
+
model=CanvasModel(c,planner); loss=make_loss(model,batch,planner,c.objective); loss.backward()
|
| 964 |
+
assert torch.isfinite(loss) and finite_tensors([p.grad for p in model.parameters() if p.grad is not None])
|
| 965 |
+
assert len({id(b.qkv.weight) for b in model.blocks})==c.depth
|
| 966 |
+
ok('all five ablations and planner forward/backward; unique block weights')
|
| 967 |
+
logits=torch.zeros(1,4,2); target=torch.zeros(1,4,dtype=torch.long)
|
| 968 |
+
mask=torch.tensor([[True,True,False,False]]); eligible=torch.ones_like(mask); t=torch.tensor([.5])
|
| 969 |
+
assert abs(float(masked_ce(logits,target,mask,eligible,t,'dfm'))-.5*math.log(2))<1e-6
|
| 970 |
+
assert abs(float(masked_ce(logits,target,mask,eligible,t,'mdlm'))-math.log(2))<1e-6
|
| 971 |
+
ok('DFM posterior CE and absorbing-path time weight analytic check')
|
| 972 |
+
model=CanvasModel(config); planner=CanvasModel(config,True)
|
| 973 |
+
for n in (1,4):
|
| 974 |
+
known_i=torch.full_like(batch['image'],MASK); active=batch['layout']==IMAGE
|
| 975 |
+
known_i[active]=batch['image'][active]
|
| 976 |
+
known_t=torch.full_like(batch['text'],MASK); known_t[:,:4]=batch['text'][:,:4]
|
| 977 |
+
img,txt=sample_content(model,batch,n,known_i,known_t)
|
| 978 |
+
assert not (img==MASK).any() and not (txt==MASK).any()
|
| 979 |
+
assert torch.equal(img[active],batch['image'][active]) and torch.equal(txt[:,:4],batch['text'][:,:4])
|
| 980 |
+
known_layout=torch.full_like(batch['layout'],UNKNOWN); known_layout[:,:,0]=RESERVED
|
| 981 |
+
out,order=sample_layout(planner,batch,n,known_layout)
|
| 982 |
+
assert not (out==UNKNOWN).any() and (out[:,:,0]==RESERVED).all()
|
| 983 |
+
ok('one/four-step sampling terminates; arbitrary known values and geometry clamped')
|
| 984 |
+
explicit=np.full((16,16),TEXT); explicit[5:8,7:11]=IMAGE
|
| 985 |
+
image=np.zeros((16,16),np.uint8); image[5:8,7:11]=255; known=explicit==IMAGE
|
| 986 |
+
result=generate_page(model,None,'mixed page',explicit,image,known,text='Hello world.',fixed_text={(15,15):'X'},steps=1)
|
| 987 |
+
assert result['known_image_preserved'] and result['canvas'][15,15]=='X' and result['text']=='Hello world.'
|
| 988 |
+
ok('public generation API preserves known image/text and fixed canvas cells')
|
| 989 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 990 |
+
root=Path(tmp)/'data'; build_data(root,count=9,size=16,text_len=32,prompt_len=64)
|
| 991 |
+
args=dict(root=root,c=config,steps=3,batch_size=2,accum=1,device='cpu',save_every=1,seed=91)
|
| 992 |
+
m1,_=train(run=Path(tmp)/'whole',**args)
|
| 993 |
+
train(run=Path(tmp)/'resume',stop_after=1,**args)
|
| 994 |
+
m2,_=train(run=Path(tmp)/'resume',**args)
|
| 995 |
+
assert all(torch.equal(v,m2.state_dict()[k]) for k,v in m1.state_dict().items())
|
| 996 |
+
a=read_checkpoint(Path(tmp)/'whole/last.pt'); b=read_checkpoint(Path(tmp)/'resume/last.pt')
|
| 997 |
+
for key in a['optimizer']['state']:
|
| 998 |
+
for field,x in a['optimizer']['state'][key].items():
|
| 999 |
+
assert torch.equal(x,b['optimizer']['state'][key][field]) if torch.is_tensor(x) else x==b['optimizer']['state'][key][field]
|
| 1000 |
+
_,summary=train(run=Path(tmp)/'retry',inject_nonfinite_at=0,**args)
|
| 1001 |
+
assert summary['step']==3 and summary['retries']==1
|
| 1002 |
+
_,rollback=train(run=Path(tmp)/'rollback',inject_poststep_at=1,**args)
|
| 1003 |
+
assert rollback['step']==3 and rollback['rollbacks']==1
|
| 1004 |
+
corrupt=Path(tmp)/'rollback/last.pt'; corrupt.write_bytes(b'broken checkpoint')
|
| 1005 |
+
recovered=read_checkpoint(corrupt); assert recovered['step']==2
|
| 1006 |
+
try: read_checkpoint(Path(tmp)/'whole/last.pt',signature='wrong')
|
| 1007 |
+
except ValueError: pass
|
| 1008 |
+
else: raise AssertionError('Incompatible resume should fail')
|
| 1009 |
+
ok('bit-exact interrupted resume including Adam; injected Inf replays without skips')
|
| 1010 |
+
ok('post-update NaN rollback, previous-checkpoint recovery, signature rejection')
|
| 1011 |
+
print(json.dumps(dict(passed=len(results),tests=results)),flush=True)
|
| 1012 |
+
return results
|
| 1013 |
+
|
| 1014 |
+
def main():
|
| 1015 |
+
p=argparse.ArgumentParser(description=__doc__); sub=p.add_subparsers(dest='command',required=True)
|
| 1016 |
+
sub.add_parser('selftest')
|
| 1017 |
+
prep=sub.add_parser('prepare'); prep.add_argument('--root',required=True); prep.add_argument('--count',type=int,default=4096)
|
| 1018 |
+
prep.add_argument('--size',type=int,default=128); prep.add_argument('--text-len',type=int,default=512)
|
| 1019 |
+
gen=sub.add_parser('gallery'); gen.add_argument('--root',required=True); gen.add_argument('--output',required=True)
|
| 1020 |
+
a=p.parse_args()
|
| 1021 |
+
if a.command=='selftest': self_test()
|
| 1022 |
+
elif a.command=='prepare': build_data(a.root,a.count,a.size,a.text_len)
|
| 1023 |
+
else: gallery(a.root,a.output)
|
| 1024 |
+
|
| 1025 |
+
if __name__=='__main__': main()
|
unified/ablations/unified-v1-blt-none-s42/source/causal_arch.py
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Audited byte architecture screen. See the notebook's fidelity table and sources.
|
| 2 |
+
|
| 3 |
+
This is a scaled, from-scratch study, not a reproduction of published checkpoints.
|
| 4 |
+
ByteFlow's whole-sequence Top-K mode is deliberately excluded from causal rankings.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import argparse
|
| 9 |
+
import contextlib
|
| 10 |
+
import copy
|
| 11 |
+
import hashlib
|
| 12 |
+
import json
|
| 13 |
+
import math
|
| 14 |
+
import os
|
| 15 |
+
import random
|
| 16 |
+
import sys
|
| 17 |
+
import time
|
| 18 |
+
from dataclasses import asdict, dataclass, replace
|
| 19 |
+
from datetime import timedelta
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import torch
|
| 24 |
+
from torch import nn
|
| 25 |
+
from torch.nn import functional as F
|
| 26 |
+
from torch.utils.checkpoint import checkpoint
|
| 27 |
+
|
| 28 |
+
PAD, BOS, EOS, VOCAB = 256, 257, 258, 259
|
| 29 |
+
SCHEMA = 3
|
| 30 |
+
FINEWEB_REVISION = "9bb295ddab0e05d785b879661af7260fed5140fc"
|
| 31 |
+
ARCHS = ("ouro_1pass", "evabyte", "blt", "byteflow_causal")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class ModelCfg:
|
| 36 |
+
dim: int = 640
|
| 37 |
+
ff: int = 1536
|
| 38 |
+
heads: int = 10
|
| 39 |
+
context: int = 4096
|
| 40 |
+
body_layers: int = 6
|
| 41 |
+
loops: int = 1
|
| 42 |
+
local_dim: int = 256
|
| 43 |
+
local_ff: int = 768
|
| 44 |
+
local_heads: int = 4
|
| 45 |
+
encoder_layers: int = 2
|
| 46 |
+
decoder_layers: int = 2
|
| 47 |
+
local_window: int = 512
|
| 48 |
+
query_block: int = 128
|
| 49 |
+
# Released EvaByte configuration, NOT the generic class defaults.
|
| 50 |
+
eva_window: int = 2048
|
| 51 |
+
eva_chunk: int = 16
|
| 52 |
+
eva_heads: int = 8
|
| 53 |
+
# Scaled BLT capacities. Hash sizes 3..8 and two latent slots are retained.
|
| 54 |
+
hash_buckets: int = 4096
|
| 55 |
+
hash_sizes: tuple = (3, 4, 5, 6, 7, 8)
|
| 56 |
+
cross_slots: int = 2
|
| 57 |
+
entropy_dim: int = 128
|
| 58 |
+
entropy_ff: int = 384
|
| 59 |
+
entropy_heads: int = 2
|
| 60 |
+
entropy_layers: int = 4
|
| 61 |
+
patch_size: float = 4.0
|
| 62 |
+
byteflow_bins: int = 16
|
| 63 |
+
byteflow_eps2: float = 1.0
|
| 64 |
+
byteflow_score: str = "l2" # Appendix B trace approximation, on RAW states.
|
| 65 |
+
router_window: int = 64 # Explicit causal adaptation, not paper Top-K.
|
| 66 |
+
grad_checkpoint: bool = True
|
| 67 |
+
|
| 68 |
+
def validate(self):
|
| 69 |
+
for d, h in ((self.dim, self.heads), (self.local_dim, self.local_heads),
|
| 70 |
+
(self.entropy_dim, self.entropy_heads)):
|
| 71 |
+
if d % h or (d // h) % 2:
|
| 72 |
+
raise ValueError("Each head must have an even integral dimension.")
|
| 73 |
+
if self.context < 2 or self.patch_size < 1 or self.loops < 1:
|
| 74 |
+
raise ValueError("Invalid context, patch size, or loop count.")
|
| 75 |
+
if self.eva_window % self.eva_chunk or self.eva_chunk < 1:
|
| 76 |
+
raise ValueError("EVA window must be divisible by chunk size.")
|
| 77 |
+
if min(self.local_window, self.query_block, self.router_window) < 1:
|
| 78 |
+
raise ValueError("Attention/router windows must be positive.")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def seed_all(seed):
|
| 82 |
+
random.seed(seed)
|
| 83 |
+
np.random.seed(seed)
|
| 84 |
+
torch.manual_seed(seed)
|
| 85 |
+
if torch.cuda.is_available():
|
| 86 |
+
torch.cuda.manual_seed_all(seed)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def documents(ids):
|
| 90 |
+
"""BOS starts a new document; the first row position may be a continuation."""
|
| 91 |
+
return (ids == BOS).long().cumsum(-1)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def ckpt(module, *args, enabled=False):
|
| 95 |
+
if enabled and module.training and torch.is_grad_enabled():
|
| 96 |
+
return checkpoint(module, *args, use_reentrant=False, preserve_rng_state=True)
|
| 97 |
+
return module(*args)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class RMSNorm(nn.Module):
|
| 101 |
+
def __init__(self, dim, eps=1e-6, unit_offset=False):
|
| 102 |
+
super().__init__()
|
| 103 |
+
self.weight = nn.Parameter(torch.zeros(dim) if unit_offset else torch.ones(dim))
|
| 104 |
+
self.eps, self.unit_offset = eps, unit_offset
|
| 105 |
+
|
| 106 |
+
def forward(self, x):
|
| 107 |
+
# FP32 statistics AND multiplication; the residual stream stays FP32.
|
| 108 |
+
z = x.float()
|
| 109 |
+
w = self.weight.float() + (1.0 if self.unit_offset else 0.0)
|
| 110 |
+
return (z * torch.rsqrt(z.square().mean(-1, keepdim=True) + self.eps) * w).to(x.dtype)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def rotary(q, k, theta):
|
| 114 |
+
t, d = q.shape[-2:]
|
| 115 |
+
with torch.autocast(q.device.type, enabled=False):
|
| 116 |
+
freq = theta ** (-torch.arange(0, d, 2, device=q.device, dtype=torch.float32) / d)
|
| 117 |
+
phase = torch.arange(t, device=q.device, dtype=torch.float32)[:, None] * freq
|
| 118 |
+
cs = torch.cat((phase, phase), -1).cos()[None, None]
|
| 119 |
+
sn = torch.cat((phase, phase), -1).sin()[None, None]
|
| 120 |
+
def rotate(x):
|
| 121 |
+
a, b = x.float().chunk(2, -1)
|
| 122 |
+
return (x.float() * cs + torch.cat((-b, a), -1) * sn).to(x.dtype)
|
| 123 |
+
return rotate(q), rotate(k)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def exclusive(z, v):
|
| 127 |
+
# Half precision dot products here used to overflow before GradScaler could help.
|
| 128 |
+
a, b = z.float(), v.float()
|
| 129 |
+
return (a - (a * b).sum(-1, keepdim=True) * b /
|
| 130 |
+
b.square().sum(-1, keepdim=True).clamp_min(1e-12)).to(z.dtype)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def sdpa(q, k, v, allowed):
|
| 134 |
+
# At least one allowed key per real query is guaranteed by callers. Empty
|
| 135 |
+
# padding queries receive a dummy key and are explicitly zeroed afterward.
|
| 136 |
+
return F.scaled_dot_product_attention(q, k, v, attn_mask=allowed,
|
| 137 |
+
dropout_p=0.0, is_causal=False)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class Attention(nn.Module):
|
| 141 |
+
def __init__(self, d, heads, window=None, theta=100000.0,
|
| 142 |
+
qk_norm=False, gated=False, xsa=False, query_block=128):
|
| 143 |
+
super().__init__()
|
| 144 |
+
self.d, self.h, self.hd = d, heads, d // heads
|
| 145 |
+
self.window, self.theta, self.query_block = window, theta, query_block
|
| 146 |
+
self.gated, self.xsa = gated, xsa
|
| 147 |
+
self.q = nn.Linear(d, d * (2 if gated else 1), bias=False)
|
| 148 |
+
self.k, self.v, self.o = [nn.Linear(d, d, bias=False) for _ in range(3)]
|
| 149 |
+
self.qn = RMSNorm(self.hd) if qk_norm else nn.Identity()
|
| 150 |
+
self.kn = RMSNorm(self.hd) if qk_norm else nn.Identity()
|
| 151 |
+
|
| 152 |
+
def forward(self, x, doc):
|
| 153 |
+
b, t, d = x.shape
|
| 154 |
+
qr = self.q(x)
|
| 155 |
+
qr, gate = qr.chunk(2, -1) if self.gated else (qr, None)
|
| 156 |
+
shape = lambda z: z.reshape(b, t, self.h, self.hd).transpose(1, 2)
|
| 157 |
+
q, k, v = self.qn(shape(qr)), self.kn(shape(self.k(x))), shape(self.v(x))
|
| 158 |
+
q, k = rotary(q, k, self.theta)
|
| 159 |
+
pieces = []
|
| 160 |
+
# Chunk queries; SWA also slices keys. There is no T x T SWA allocation.
|
| 161 |
+
for start in range(0, t, self.query_block):
|
| 162 |
+
end = min(t, start + self.query_block)
|
| 163 |
+
lo = max(0, start - self.window + 1) if self.window else 0
|
| 164 |
+
qi = torch.arange(start, end, device=x.device)[:, None]
|
| 165 |
+
ki = torch.arange(lo, end, device=x.device)[None, :]
|
| 166 |
+
allow = ki <= qi
|
| 167 |
+
if self.window:
|
| 168 |
+
allow = allow & (ki > qi - self.window)
|
| 169 |
+
allow = allow[None] & (doc[:, start:end, None] == doc[:, None, lo:end])
|
| 170 |
+
pieces.append(sdpa(q[:, :, start:end], k[:, :, lo:end], v[:, :, lo:end], allow[:, None]))
|
| 171 |
+
z = torch.cat(pieces, -2)
|
| 172 |
+
if self.xsa:
|
| 173 |
+
z = exclusive(z, v)
|
| 174 |
+
if gate is not None:
|
| 175 |
+
# Query-dependent bounded G1 sigmoid gate, not unbounded SiLU.
|
| 176 |
+
z = z * shape(gate).float().sigmoid().to(z.dtype)
|
| 177 |
+
return self.o(z.transpose(1, 2).reshape(b, t, d))
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class SwiGLU(nn.Module):
|
| 181 |
+
def __init__(self, d, ff):
|
| 182 |
+
super().__init__()
|
| 183 |
+
self.g = nn.Linear(d, ff, bias=False)
|
| 184 |
+
self.u = nn.Linear(d, ff, bias=False)
|
| 185 |
+
self.o = nn.Linear(ff, d, bias=False)
|
| 186 |
+
|
| 187 |
+
def forward(self, x):
|
| 188 |
+
return self.o(F.silu(self.g(x)) * self.u(x))
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class Block(nn.Module):
|
| 192 |
+
def __init__(self, d, ff, heads, *, window=None, theta=100000.0,
|
| 193 |
+
sandwich=False, ouro=False, query_block=128, norm_eps=1e-6,
|
| 194 |
+
unit_offset=False):
|
| 195 |
+
super().__init__()
|
| 196 |
+
self.n1, self.n2 = [RMSNorm(d, norm_eps, unit_offset) for _ in range(2)]
|
| 197 |
+
self.attn = Attention(d, heads, window, theta, ouro, ouro, ouro, query_block)
|
| 198 |
+
self.ff = SwiGLU(d, ff)
|
| 199 |
+
self.post_a = RMSNorm(d) if sandwich else nn.Identity()
|
| 200 |
+
self.post_f = RMSNorm(d) if sandwich else nn.Identity()
|
| 201 |
+
|
| 202 |
+
def forward(self, x, doc):
|
| 203 |
+
# Published Ouro: x + Norm(Attention(Norm(x))). Never Norm(x + branch).
|
| 204 |
+
x = x + self.post_a(self.attn(self.n1(x), doc))
|
| 205 |
+
return x + self.post_f(self.ff(self.n2(x)))
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def initialize(m):
|
| 209 |
+
if isinstance(m, (nn.Linear, nn.Embedding)):
|
| 210 |
+
nn.init.normal_(m.weight, std=0.02)
|
| 211 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 212 |
+
nn.init.zeros_(m.bias)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class OuroLM(nn.Module):
|
| 216 |
+
"""User's prelude/shared-body/coda baseline, with corrected branch norms.
|
| 217 |
+
|
| 218 |
+
This deliberately does not add ByteDance's early-exit training objective.
|
| 219 |
+
"""
|
| 220 |
+
def __init__(self, c):
|
| 221 |
+
super().__init__()
|
| 222 |
+
self.c = c
|
| 223 |
+
self.embed = nn.Embedding(VOCAB, c.dim)
|
| 224 |
+
make = lambda: Block(c.dim, c.ff, c.heads, sandwich=True, ouro=True,
|
| 225 |
+
query_block=c.query_block)
|
| 226 |
+
self.prelude, self.coda = make(), make()
|
| 227 |
+
self.body = nn.ModuleList([make() for _ in range(c.body_layers)])
|
| 228 |
+
self.loop_norm, self.norm = RMSNorm(c.dim), RMSNorm(c.dim)
|
| 229 |
+
self.apply(initialize)
|
| 230 |
+
|
| 231 |
+
def forward(self, ids, loops=None):
|
| 232 |
+
loops = self.c.loops if loops is None else loops
|
| 233 |
+
if loops < 1:
|
| 234 |
+
raise ValueError("loops must be positive")
|
| 235 |
+
doc = documents(ids)
|
| 236 |
+
h = ckpt(self.prelude, self.embed(ids), doc, enabled=self.c.grad_checkpoint)
|
| 237 |
+
for _ in range(loops):
|
| 238 |
+
for layer in self.body:
|
| 239 |
+
h = ckpt(layer, h, doc, enabled=self.c.grad_checkpoint)
|
| 240 |
+
h = self.loop_norm(h) # Also used at one pass; no unused DDP parameter.
|
| 241 |
+
h = ckpt(self.coda, h, doc, enabled=self.c.grad_checkpoint)
|
| 242 |
+
return {"logits": F.linear(self.norm(h), self.embed.weight), "heads": []}
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
class EVAAttention(nn.Module):
|
| 246 |
+
"""PyTorch reference-equation EVA for arbitrary lengths, including decode.
|
| 247 |
+
|
| 248 |
+
Exact intra-window attention + learned summaries from strictly earlier
|
| 249 |
+
windows. Document-crossing chunks are split into document-specific slots.
|
| 250 |
+
"""
|
| 251 |
+
def __init__(self, c):
|
| 252 |
+
super().__init__()
|
| 253 |
+
self.c, self.h, self.hd = c, c.heads, c.dim // c.heads
|
| 254 |
+
self.q, self.k, self.v, self.o = [nn.Linear(c.dim, c.dim, bias=False) for _ in range(4)]
|
| 255 |
+
self.mu = nn.Parameter(torch.randn(1, self.h, 1, 1, self.hd).clamp(-1, 1) / math.sqrt(self.hd))
|
| 256 |
+
self.phi = nn.Parameter(torch.randn(1, self.h, 1, 1, self.hd).clamp(-1, 1) / math.sqrt(self.hd))
|
| 257 |
+
|
| 258 |
+
def summaries(self, k, v, doc):
|
| 259 |
+
b, h, t, d = k.shape
|
| 260 |
+
chunk = self.c.eva_chunk
|
| 261 |
+
# A summary cannot mix documents. A chunk boundary or BOS starts a slot.
|
| 262 |
+
starts = torch.zeros_like(doc, dtype=torch.bool)
|
| 263 |
+
starts[:, ::chunk] = True
|
| 264 |
+
starts[:, 1:] |= doc[:, 1:] != doc[:, :-1]
|
| 265 |
+
seg, counts = starts.long().cumsum(-1) - 1, starts.sum(-1)
|
| 266 |
+
n = int(counts.max().item())
|
| 267 |
+
pos = torch.arange(t, device=k.device).expand(b, t)
|
| 268 |
+
first = torch.full((b, n), t, device=k.device, dtype=torch.long)
|
| 269 |
+
first.scatter_reduce_(1, seg, pos, reduce="amin", include_self=True)
|
| 270 |
+
last = torch.full_like(first, -1)
|
| 271 |
+
last.scatter_reduce_(1, seg, pos, reduce="amax", include_self=True)
|
| 272 |
+
offsets = pos - first.gather(1, seg)
|
| 273 |
+
flat = (seg * chunk + offsets)[:, None, :, None].expand(b, h, t, d)
|
| 274 |
+
# Accumulate products in FP32, including key squared norms.
|
| 275 |
+
kk = k.float().new_zeros(b, h, n * chunk, d).scatter(2, flat, k.float())
|
| 276 |
+
vv = v.float().new_zeros(b, h, n * chunk, d).scatter(2, flat, v.float())
|
| 277 |
+
kk, vv = kk.view(b, h, n, chunk, d), vv.view(b, h, n, chunk, d)
|
| 278 |
+
valid = torch.arange(chunk, device=k.device)[None, None, None, :] <= (last-first)[:, None, :, None]
|
| 279 |
+
active = first < t
|
| 280 |
+
safe = valid.clone()
|
| 281 |
+
safe[:, :, :, 0] |= ~active[:, None, :]
|
| 282 |
+
with torch.autocast(k.device.type, enabled=False):
|
| 283 |
+
wm = ((kk * self.mu.float()).sum(-1)).masked_fill(~safe, -torch.inf).softmax(-1)
|
| 284 |
+
wp = (((kk * self.phi.float()).sum(-1) - 0.5 * kk.square().sum(-1)) /
|
| 285 |
+
math.sqrt(d)).masked_fill(~safe, -torch.inf).softmax(-1)
|
| 286 |
+
kb = (wm[..., None] * kk).sum(-2)
|
| 287 |
+
vb = (wp[..., None] * vv).sum(-2)
|
| 288 |
+
sdoc = doc.gather(1, first.clamp_max(t - 1))
|
| 289 |
+
return kb.to(k.dtype), vb.to(v.dtype), last, sdoc, active
|
| 290 |
+
|
| 291 |
+
def forward(self, x, doc):
|
| 292 |
+
b, t, d = x.shape
|
| 293 |
+
reshape = lambda z: z.view(b, t, self.h, self.hd).transpose(1, 2)
|
| 294 |
+
q, k, v = reshape(self.q(x)), reshape(self.k(x)), reshape(self.v(x))
|
| 295 |
+
q, k = rotary(q, k, 100000.0)
|
| 296 |
+
kb, vb, last, sdoc, active = self.summaries(k, v, doc)
|
| 297 |
+
outputs = []
|
| 298 |
+
w = self.c.eva_window
|
| 299 |
+
for ws in range(0, t, w):
|
| 300 |
+
we = min(t, ws + w)
|
| 301 |
+
# Keep a zero dependency when there are no earlier slots; the
|
| 302 |
+
# mathematical EVA path is unchanged and reducer participation is safe.
|
| 303 |
+
keys = torch.cat((k[:, :, ws:we], kb), -2)
|
| 304 |
+
vals = torch.cat((v[:, :, ws:we], vb), -2)
|
| 305 |
+
for a in range(ws, we, self.c.query_block):
|
| 306 |
+
z = min(we, a + self.c.query_block)
|
| 307 |
+
qi = torch.arange(a, z, device=x.device)[:, None]
|
| 308 |
+
ki = torch.arange(ws, we, device=x.device)[None, :]
|
| 309 |
+
local = (ki <= qi)[None] & (doc[:, a:z, None] == doc[:, None, ws:we])
|
| 310 |
+
summary = (last[:, None, :] < ws) & active[:, None, :]
|
| 311 |
+
summary = summary & (doc[:, a:z, None] == sdoc[:, None, :])
|
| 312 |
+
allow = torch.cat((local, summary), -1)
|
| 313 |
+
outputs.append(sdpa(q[:, :, a:z], keys, vals, allow[:, None]))
|
| 314 |
+
h = torch.cat(outputs, -2).transpose(1, 2).reshape(b, t, d)
|
| 315 |
+
return self.o(h)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
class EVABlock(nn.Module):
|
| 319 |
+
def __init__(self, c):
|
| 320 |
+
super().__init__()
|
| 321 |
+
self.n1, self.n2 = [RMSNorm(c.dim, 1e-5, True) for _ in range(2)]
|
| 322 |
+
self.attn, self.ff = EVAAttention(c), SwiGLU(c.dim, c.ff)
|
| 323 |
+
|
| 324 |
+
def forward(self, x, doc):
|
| 325 |
+
x = x + self.attn(self.n1(x), doc)
|
| 326 |
+
return x + self.ff(self.n2(x))
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
class EvaByteLM(nn.Module):
|
| 330 |
+
def __init__(self, c, depth):
|
| 331 |
+
super().__init__()
|
| 332 |
+
self.c = c
|
| 333 |
+
self.embed = nn.Embedding(VOCAB, c.dim)
|
| 334 |
+
self.layers = nn.ModuleList([EVABlock(c) for _ in range(depth)])
|
| 335 |
+
self.norm = RMSNorm(c.dim, 1e-5, True)
|
| 336 |
+
self.output = nn.Linear(c.dim, VOCAB * c.eva_heads, bias=False)
|
| 337 |
+
self.apply(initialize)
|
| 338 |
+
|
| 339 |
+
def forward(self, ids, loops=None):
|
| 340 |
+
doc, h = documents(ids), self.embed(ids)
|
| 341 |
+
for layer in self.layers:
|
| 342 |
+
h = ckpt(layer, h, doc, enabled=self.c.grad_checkpoint)
|
| 343 |
+
all_heads = self.output(self.norm(h)).view(*ids.shape, self.c.eva_heads, VOCAB)
|
| 344 |
+
return {"logits": all_heads[:, :, 0], "heads": list(all_heads.unbind(2))}
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
class EntropyLM(nn.Module):
|
| 348 |
+
def __init__(self, c):
|
| 349 |
+
super().__init__()
|
| 350 |
+
self.c = c
|
| 351 |
+
self.embed = nn.Embedding(VOCAB, c.entropy_dim)
|
| 352 |
+
self.layers = nn.ModuleList([Block(c.entropy_dim, c.entropy_ff, c.entropy_heads,
|
| 353 |
+
window=c.local_window, theta=500000.0, query_block=c.query_block)
|
| 354 |
+
for _ in range(c.entropy_layers)])
|
| 355 |
+
self.norm = RMSNorm(c.entropy_dim)
|
| 356 |
+
self.output = nn.Linear(c.entropy_dim, VOCAB, bias=False)
|
| 357 |
+
self.register_buffer("threshold", torch.tensor(float("nan")))
|
| 358 |
+
self.apply(initialize)
|
| 359 |
+
|
| 360 |
+
def forward(self, ids, loops=None):
|
| 361 |
+
h, doc = self.embed(ids), documents(ids)
|
| 362 |
+
for layer in self.layers:
|
| 363 |
+
h = ckpt(layer, h, doc, enabled=self.c.grad_checkpoint)
|
| 364 |
+
return {"logits": self.output(self.norm(h)), "heads": []}
|
| 365 |
+
|
| 366 |
+
@torch.no_grad()
|
| 367 |
+
def entropy(self, ids):
|
| 368 |
+
logits = self(ids)["logits"].float()
|
| 369 |
+
lp = logits.log_softmax(-1)
|
| 370 |
+
return -(lp.exp() * lp).sum(-1)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def segments(starts):
|
| 374 |
+
"""Ragged routing metadata; never truncate excess patches."""
|
| 375 |
+
b, t = starts.shape
|
| 376 |
+
ids = starts.long().cumsum(-1) - 1
|
| 377 |
+
counts = starts.sum(-1)
|
| 378 |
+
n = int(counts.max().item())
|
| 379 |
+
pos = torch.arange(t, device=starts.device).expand(b, t)
|
| 380 |
+
first = torch.full((b, n), t, device=starts.device, dtype=torch.long)
|
| 381 |
+
first.scatter_reduce_(1, ids, pos, reduce="amin", include_self=True)
|
| 382 |
+
valid = torch.arange(n, device=starts.device)[None] < counts[:, None]
|
| 383 |
+
return ids, first.clamp_max(t - 1), valid
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def gather(states, idx):
|
| 387 |
+
return states.gather(1, idx[..., None].expand(*idx.shape, states.shape[-1]))
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
class CrossAttention(nn.Module):
|
| 391 |
+
def __init__(self, d, heads, block):
|
| 392 |
+
super().__init__()
|
| 393 |
+
self.h, self.hd, self.block = heads, d // heads, block
|
| 394 |
+
self.nq, self.nkv = RMSNorm(d), RMSNorm(d)
|
| 395 |
+
self.q, self.k, self.v, self.o = [nn.Linear(d, d, bias=False) for _ in range(4)]
|
| 396 |
+
|
| 397 |
+
def forward(self, x, kv, qgroup, kgroup, valid_q=None):
|
| 398 |
+
b, t, d = x.shape
|
| 399 |
+
nk = kv.shape[1]
|
| 400 |
+
shape = lambda a: a.view(b, -1, self.h, self.hd).transpose(1, 2)
|
| 401 |
+
q, kn = shape(self.q(self.nq(x))), self.nkv(kv)
|
| 402 |
+
k, v = shape(self.k(kn)), shape(self.v(kn))
|
| 403 |
+
chunks = []
|
| 404 |
+
for a in range(0, t, self.block):
|
| 405 |
+
z = min(t, a + self.block)
|
| 406 |
+
allow = qgroup[:, a:z, None] == kgroup[:, None, :]
|
| 407 |
+
real = allow.any(-1)
|
| 408 |
+
if valid_q is not None:
|
| 409 |
+
real = real & valid_q[:, a:z]
|
| 410 |
+
# Avoid all-masked softmax on padded/no-context queries.
|
| 411 |
+
allow[:, :, 0] |= ~real
|
| 412 |
+
y = sdpa(q[:, :, a:z], k, v, allow[:, None])
|
| 413 |
+
y = y * real[:, None, :, None]
|
| 414 |
+
chunks.append(y)
|
| 415 |
+
z = torch.cat(chunks, -2).transpose(1, 2).reshape(b, t, d)
|
| 416 |
+
return x + self.o(z)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
class GlobalStack(nn.Module):
|
| 420 |
+
def __init__(self, c, depth, theta=500000.0):
|
| 421 |
+
super().__init__()
|
| 422 |
+
self.c = c
|
| 423 |
+
self.layers = nn.ModuleList([Block(c.dim, c.ff, c.heads,
|
| 424 |
+
theta=theta, query_block=c.query_block) for _ in range(depth)])
|
| 425 |
+
self.norm = RMSNorm(c.dim)
|
| 426 |
+
|
| 427 |
+
def forward(self, x, doc):
|
| 428 |
+
for layer in self.layers:
|
| 429 |
+
x = ckpt(layer, x, doc, enabled=self.c.grad_checkpoint)
|
| 430 |
+
return self.norm(x)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
class BLTLM(nn.Module):
|
| 434 |
+
"""Scaled paper BLT: frozen independent patcher, ngrams, cross-attention.
|
| 435 |
+
|
| 436 |
+
Decoder access is aligned with the NEXT byte's patch. A current patch is
|
| 437 |
+
visible only when the prefix-only entropy decision has closed it.
|
| 438 |
+
"""
|
| 439 |
+
def __init__(self, c, depth):
|
| 440 |
+
super().__init__()
|
| 441 |
+
self.c = c
|
| 442 |
+
self.patcher = EntropyLM(c).requires_grad_(False)
|
| 443 |
+
self.embed = nn.Embedding(VOCAB, c.local_dim)
|
| 444 |
+
self.hashes = nn.ModuleList([nn.Embedding(c.hash_buckets, c.local_dim) for _ in c.hash_sizes])
|
| 445 |
+
make = lambda: Block(c.local_dim, c.local_ff, c.local_heads,
|
| 446 |
+
window=c.local_window, theta=500000.0, query_block=c.query_block)
|
| 447 |
+
self.enc = nn.ModuleList([make() for _ in range(c.encoder_layers)])
|
| 448 |
+
self.dec = nn.ModuleList([make() for _ in range(c.decoder_layers)])
|
| 449 |
+
make_cross = lambda: CrossAttention(c.local_dim, c.local_heads, c.query_block)
|
| 450 |
+
self.enc_cross = nn.ModuleList([make_cross() for _ in self.enc])
|
| 451 |
+
self.dec_cross = nn.ModuleList([make_cross() for _ in self.dec])
|
| 452 |
+
self.pool_proj = nn.Linear(c.local_dim, c.local_dim * c.cross_slots, bias=False)
|
| 453 |
+
self.to_global = nn.Linear(c.local_dim * c.cross_slots, c.dim, bias=False)
|
| 454 |
+
self.to_local = nn.Linear(c.dim, c.local_dim * c.cross_slots, bias=False)
|
| 455 |
+
self.global_stack = GlobalStack(c, depth)
|
| 456 |
+
self.norm = RMSNorm(c.local_dim)
|
| 457 |
+
self.output = nn.Linear(c.local_dim, VOCAB, bias=False)
|
| 458 |
+
self.apply(initialize)
|
| 459 |
+
|
| 460 |
+
def train(self, mode=True):
|
| 461 |
+
super().train(mode)
|
| 462 |
+
self.patcher.eval()
|
| 463 |
+
return self
|
| 464 |
+
|
| 465 |
+
def byte_embeddings(self, x, doc):
|
| 466 |
+
b, t = x.shape
|
| 467 |
+
emb = self.embed(x)
|
| 468 |
+
# Polynomial hashes, causal and reset at document boundaries. Modular
|
| 469 |
+
# arithmetic avoids signed int64 overflow and is platform-independent.
|
| 470 |
+
for size, table in zip(self.c.hash_sizes, self.hashes):
|
| 471 |
+
if size > t:
|
| 472 |
+
emb = emb + table.weight[0] * 0
|
| 473 |
+
continue
|
| 474 |
+
h = torch.zeros_like(x)
|
| 475 |
+
valid = torch.arange(t, device=x.device)[None] >= size - 1
|
| 476 |
+
valid = valid.expand(b, t).clone()
|
| 477 |
+
for lag in range(size - 1, -1, -1):
|
| 478 |
+
shifted = F.pad(x[:, :t-lag], (lag, 0), value=0) if lag else x
|
| 479 |
+
h = (h * 257 + shifted) % self.c.hash_buckets
|
| 480 |
+
valid &= (shifted < 256)
|
| 481 |
+
if lag:
|
| 482 |
+
prevdoc = F.pad(doc[:, :t-lag], (lag, 0), value=-1)
|
| 483 |
+
valid &= prevdoc == doc
|
| 484 |
+
emb = emb + table(h) * valid[..., None]
|
| 485 |
+
return emb / (1 + len(self.hashes))
|
| 486 |
+
|
| 487 |
+
def forward(self, x, loops=None):
|
| 488 |
+
if not torch.isfinite(self.patcher.threshold):
|
| 489 |
+
raise RuntimeError("BLT requires a trained, calibrated entropy checkpoint; run the patcher stage first.")
|
| 490 |
+
b, t = x.shape
|
| 491 |
+
doc = documents(x)
|
| 492 |
+
with torch.no_grad():
|
| 493 |
+
e = self.patcher.entropy(x)
|
| 494 |
+
close = (e > self.patcher.threshold) | (x == BOS) | (x == EOS)
|
| 495 |
+
starts = torch.cat((torch.ones_like(close[:, :1]), close[:, :-1]), 1)
|
| 496 |
+
starts |= x == BOS
|
| 497 |
+
pid, first, valid = segments(starts)
|
| 498 |
+
n = first.shape[1]
|
| 499 |
+
pdoc = doc.gather(1, first)
|
| 500 |
+
pdoc = torch.where(valid, pdoc, -1)
|
| 501 |
+
groups = torch.arange(n, device=x.device)[None].expand(b, n)
|
| 502 |
+
pgroups = groups.repeat_interleave(self.c.cross_slots, 1)
|
| 503 |
+
pvalid = valid.repeat_interleave(self.c.cross_slots, 1)
|
| 504 |
+
# No peeking at x[t+1] to decide decoder conditioning.
|
| 505 |
+
context_id = pid + close.long() - 1
|
| 506 |
+
ok = context_id >= 0
|
| 507 |
+
context_doc = pdoc.gather(1, context_id.clamp_min(0))
|
| 508 |
+
ok &= context_doc == doc
|
| 509 |
+
context_id = torch.where(ok, context_id, -2)
|
| 510 |
+
h = self.byte_embeddings(x, doc)
|
| 511 |
+
p = None
|
| 512 |
+
for layer, cross in zip(self.enc, self.enc_cross):
|
| 513 |
+
h = ckpt(layer, h, doc, enabled=self.c.grad_checkpoint)
|
| 514 |
+
if p is None:
|
| 515 |
+
pooled = h.float().new_full((b, n, h.shape[-1]), -torch.inf)
|
| 516 |
+
pooled = pooled.scatter_reduce(1, pid[..., None].expand_as(h), h.float(),
|
| 517 |
+
reduce="amax", include_self=True)
|
| 518 |
+
pooled = torch.where(valid[..., None], pooled, torch.zeros_like(pooled))
|
| 519 |
+
p = self.pool_proj(pooled.to(h.dtype)).reshape(b, n * self.c.cross_slots, -1)
|
| 520 |
+
p = ckpt(cross, p, h, pgroups, pid, pvalid, enabled=self.c.grad_checkpoint)
|
| 521 |
+
g = self.global_stack(self.to_global(p.reshape(b, n, -1)), pdoc)
|
| 522 |
+
kv = self.to_local(g).reshape(b, n * self.c.cross_slots, -1)
|
| 523 |
+
for cross, layer in zip(self.dec_cross, self.dec):
|
| 524 |
+
h = ckpt(cross, h, kv, context_id, pgroups, ok, enabled=self.c.grad_checkpoint)
|
| 525 |
+
h = ckpt(layer, h, doc, enabled=self.c.grad_checkpoint)
|
| 526 |
+
return {"logits": self.output(self.norm(h)), "heads": [],
|
| 527 |
+
"patches": valid.sum().detach(), "positions": x.numel()}
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
class Canon(nn.Module):
|
| 531 |
+
def __init__(self, d):
|
| 532 |
+
super().__init__()
|
| 533 |
+
# Identity initialization is stable without adding a second residual
|
| 534 |
+
# around Canon. The paper's operator replaces its input.
|
| 535 |
+
w = torch.zeros(4, d)
|
| 536 |
+
w[0] = 1
|
| 537 |
+
self.weight = nn.Parameter(w)
|
| 538 |
+
|
| 539 |
+
def forward(self, x, doc):
|
| 540 |
+
y = x * self.weight[0]
|
| 541 |
+
for lag in range(1, min(4, x.shape[1])):
|
| 542 |
+
h = F.pad(x[:, :-lag], (0, 0, lag, 0))
|
| 543 |
+
same = F.pad(doc[:, :-lag], (lag, 0), value=-1) == doc
|
| 544 |
+
y = y + h * self.weight[lag] * same[..., None]
|
| 545 |
+
return y
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
class CanonBlock(nn.Module):
|
| 549 |
+
def __init__(self, c):
|
| 550 |
+
super().__init__()
|
| 551 |
+
# Main paper equations explicitly specify LayerNorm in local modules.
|
| 552 |
+
self.n1, self.n2 = nn.LayerNorm(c.local_dim), nn.LayerNorm(c.local_dim)
|
| 553 |
+
self.a = Attention(c.local_dim, c.local_heads, c.local_window,
|
| 554 |
+
500000.0, query_block=c.query_block)
|
| 555 |
+
self.ff = SwiGLU(c.local_dim, c.local_ff)
|
| 556 |
+
self.c1, self.c2 = Canon(c.local_dim), Canon(c.local_dim)
|
| 557 |
+
|
| 558 |
+
def forward(self, x, doc):
|
| 559 |
+
x = self.c1(x + self.a(self.n1(x), doc), doc)
|
| 560 |
+
return self.c2(x + self.ff(self.n2(x)), doc)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
def coding_scores(h, eps2=1.0, mode="l2", doc=None):
|
| 564 |
+
# Detaching boundary decisions follows discrete Top-K selection. Selected
|
| 565 |
+
# encoder states still receive the normal language-model gradients.
|
| 566 |
+
with torch.no_grad(), torch.autocast(h.device.type, enabled=False):
|
| 567 |
+
x = h.detach().float()
|
| 568 |
+
b, t, d = x.shape
|
| 569 |
+
alpha = d / eps2
|
| 570 |
+
if mode == "l2":
|
| 571 |
+
return 0.5 * alpha * x.square().sum(-1)
|
| 572 |
+
if mode != "exact_smw":
|
| 573 |
+
raise ValueError(mode)
|
| 574 |
+
# Expensive diagnostic; unnormalized states are essential to Eq. 11.
|
| 575 |
+
eye = torch.eye(d, device=x.device, dtype=torch.float64).expand(b, d, d)
|
| 576 |
+
inv = eye.clone()
|
| 577 |
+
scores = []
|
| 578 |
+
for i in range(t):
|
| 579 |
+
if i and doc is not None:
|
| 580 |
+
inv = torch.where((doc[:, i] != doc[:, i-1])[:, None, None], eye, inv)
|
| 581 |
+
v = x[:, i].double().unsqueeze(-1)
|
| 582 |
+
av = inv @ v
|
| 583 |
+
q = (v.transpose(1, 2) @ av).flatten().clamp_min(0)
|
| 584 |
+
den = 1 + alpha * q
|
| 585 |
+
scores.append(0.5 * den.log())
|
| 586 |
+
inv = inv - (alpha / den)[:, None, None] * (av @ av.transpose(1, 2))
|
| 587 |
+
inv = (inv + inv.transpose(1, 2)) * 0.5
|
| 588 |
+
return torch.stack(scores, 1).float()
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
def causal_promotions(scores, doc, patch_size, window):
|
| 592 |
+
"""Irrevocable trailing-window rank gate. A disclosed ByteFlow adaptation.
|
| 593 |
+
|
| 594 |
+
Current position is selected if its score is in the top 1/patch_size of
|
| 595 |
+
the observed window. Ties choose earlier positions. No future rank or cap.
|
| 596 |
+
"""
|
| 597 |
+
s = F.pad(scores, (window - 1, 0), value=-torch.inf).unfold(1, window, 1)
|
| 598 |
+
d = F.pad(doc, (window - 1, 0), value=-1).unfold(1, window, 1)
|
| 599 |
+
valid = d == doc[..., None]
|
| 600 |
+
rank = ((s >= scores[..., None]) & valid).sum(-1)
|
| 601 |
+
quota = (valid.sum(-1).float() / patch_size).ceil().clamp_min(1)
|
| 602 |
+
starts = rank <= quota
|
| 603 |
+
starts[:, 0] = True
|
| 604 |
+
starts[:, 1:] |= doc[:, 1:] != doc[:, :-1]
|
| 605 |
+
return starts
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def paper_promotions(scores, patch_size):
|
| 609 |
+
b, t = scores.shape
|
| 610 |
+
k = max(1, min(t, math.ceil(t / patch_size)))
|
| 611 |
+
starts = torch.zeros_like(scores, dtype=torch.bool)
|
| 612 |
+
starts[:, 0] = True
|
| 613 |
+
if k > 1:
|
| 614 |
+
# Stable sort provides a deterministic tie policy.
|
| 615 |
+
ix = torch.argsort(scores[:, 1:], dim=-1, descending=True, stable=True)[:, :k-1] + 1
|
| 616 |
+
starts.scatter_(1, ix, True)
|
| 617 |
+
return starts
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
class ByteFlowLM(nn.Module):
|
| 621 |
+
def __init__(self, c, depth, paper=False):
|
| 622 |
+
super().__init__()
|
| 623 |
+
self.c, self.paper = c, paper
|
| 624 |
+
self.embed = nn.Embedding(VOCAB, c.local_dim)
|
| 625 |
+
self.enc = nn.ModuleList([CanonBlock(c) for _ in range(c.encoder_layers)])
|
| 626 |
+
self.dec = nn.ModuleList([CanonBlock(c) for _ in range(c.decoder_layers)])
|
| 627 |
+
self.to_global = nn.Linear(c.local_dim, c.dim, bias=False)
|
| 628 |
+
self.global_stack = GlobalStack(c, depth)
|
| 629 |
+
self.up = nn.ModuleList([nn.Linear(c.dim, c.local_dim, bias=False) for _ in range(c.byteflow_bins)])
|
| 630 |
+
self.norm = nn.LayerNorm(c.local_dim)
|
| 631 |
+
self.output = nn.Linear(c.local_dim, VOCAB, bias=False)
|
| 632 |
+
self.apply(initialize)
|
| 633 |
+
|
| 634 |
+
def forward(self, x, loops=None):
|
| 635 |
+
b, t = x.shape
|
| 636 |
+
doc, h = documents(x), self.embed(x)
|
| 637 |
+
for layer in self.enc:
|
| 638 |
+
h = ckpt(layer, h, doc, enabled=self.c.grad_checkpoint)
|
| 639 |
+
scores = coding_scores(h, self.c.byteflow_eps2, self.c.byteflow_score, doc)
|
| 640 |
+
if self.paper:
|
| 641 |
+
starts = paper_promotions(scores, self.c.patch_size)
|
| 642 |
+
# EOS/BOS isolation is a common data-protocol adaptation.
|
| 643 |
+
starts[:, 1:] |= doc[:, 1:] != doc[:, :-1]
|
| 644 |
+
else:
|
| 645 |
+
starts = causal_promotions(scores, doc, self.c.patch_size, self.c.router_window)
|
| 646 |
+
pid, first, valid = segments(starts)
|
| 647 |
+
pdoc = torch.where(valid, doc.gather(1, first), -1)
|
| 648 |
+
z = self.to_global(gather(h, first))
|
| 649 |
+
g = self.global_stack(z, pdoc)
|
| 650 |
+
lifted = gather(g, pid)
|
| 651 |
+
# Paper Eq. 15 is length-dependent. Causal mode fixes the horizon to
|
| 652 |
+
# the configured context so extending a prefix cannot change old bins.
|
| 653 |
+
horizon = t if self.paper else self.c.context
|
| 654 |
+
bins = (torch.arange(t, device=x.device) * self.c.byteflow_bins // horizon).clamp_max(self.c.byteflow_bins - 1)
|
| 655 |
+
# Compute by contiguous bin spans; never materialize [T, G, local_dim].
|
| 656 |
+
up = []
|
| 657 |
+
for i, proj in enumerate(self.up):
|
| 658 |
+
selected = torch.nonzero(bins == i, as_tuple=False).flatten()
|
| 659 |
+
if selected.numel():
|
| 660 |
+
up.append(proj(lifted[:, selected]))
|
| 661 |
+
s = h + torch.cat(up, 1)
|
| 662 |
+
# Unused bins at short prefixes should have zero gradients, not missing
|
| 663 |
+
# ones in DDP. This does not alter any activation.
|
| 664 |
+
s = s + sum(proj.weight.reshape(-1)[0] * 0 for proj in self.up)
|
| 665 |
+
for layer in self.dec:
|
| 666 |
+
s = ckpt(layer, s, doc, enabled=self.c.grad_checkpoint)
|
| 667 |
+
return {"logits": self.output(self.norm(s)), "heads": [],
|
| 668 |
+
"patches": valid.sum().detach(), "positions": x.numel()}
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
def nparams(model, trainable=False):
|
| 672 |
+
return sum(p.numel() for p in model.parameters() if not trainable or p.requires_grad)
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
def build_model(arch, c, seed=42):
|
| 676 |
+
if arch not in ARCHS + ("entropy",): raise ValueError("Unsupported architecture; recurrent ouro is removed")
|
| 677 |
+
c.validate()
|
| 678 |
+
if c.loops != 1: raise ValueError("This unified screen supports one-pass Ouro only")
|
| 679 |
+
builders = {
|
| 680 |
+
"evabyte": lambda n: EvaByteLM(c, n),
|
| 681 |
+
"blt": lambda n: BLTLM(c, n),
|
| 682 |
+
"byteflow_causal": lambda n: ByteFlowLM(c, n, False),
|
| 683 |
+
"byteflow_paper": lambda n: ByteFlowLM(c, n, True),
|
| 684 |
+
}
|
| 685 |
+
# Meta construction does not allocate model weights or perturb the seed
|
| 686 |
+
# subsequently used for initialization. Frozen patcher counts are INCLUDED.
|
| 687 |
+
with torch.device("meta"):
|
| 688 |
+
target = nparams(OuroLM(c))
|
| 689 |
+
depth = c.body_layers + 2
|
| 690 |
+
if arch in builders:
|
| 691 |
+
base, one = nparams(builders[arch](0)), nparams(builders[arch](1))
|
| 692 |
+
per = one - base
|
| 693 |
+
depth = min(range(1, 33), key=lambda n: abs(base + n * per - target))
|
| 694 |
+
seed_all(seed)
|
| 695 |
+
if arch in ("ouro", "ouro_1pass"):
|
| 696 |
+
model = OuroLM(c)
|
| 697 |
+
elif arch == "entropy":
|
| 698 |
+
model = EntropyLM(c)
|
| 699 |
+
depth = c.entropy_layers
|
| 700 |
+
else:
|
| 701 |
+
model = builders[arch](depth)
|
| 702 |
+
meta = {"arch": arch, "total_params": nparams(model), "trainable_params": nparams(model, True),
|
| 703 |
+
"target_params": target, "depth": depth, "frozen_params": nparams(model)-nparams(model, True),
|
| 704 |
+
"param_delta_pct": 100 * (nparams(model)-target)/target,
|
| 705 |
+
"causal_ranking_eligible": arch != "byteflow_paper",
|
| 706 |
+
"fidelity": "paper-style diagnostic" if arch == "byteflow_paper" else
|
| 707 |
+
"explicit causal adaptation" if arch == "byteflow_causal" else "scaled architecture"}
|
| 708 |
+
return model, meta
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
def forward_model(model, ids, arch, loops=None):
|
| 712 |
+
return model(ids, loops=1 if arch == "ouro_1pass" else loops)
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
def targets_and_mask(sequence, offset=1):
|
| 716 |
+
# The caller forwards sequence[:, :-1]. Auxiliary heads lose their tail.
|
| 717 |
+
n = sequence.shape[1] - offset
|
| 718 |
+
target = sequence[:, offset:]
|
| 719 |
+
doc = documents(sequence)
|
| 720 |
+
valid = (target != BOS) & (target != PAD) & (doc[:, :n] == doc[:, offset:])
|
| 721 |
+
return target, valid
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
def head_counts(batches, nheads):
|
| 725 |
+
return torch.tensor([sum(int(targets_and_mask(x, j+1)[1].sum()) for x in batches)
|
| 726 |
+
for j in range(nheads)], dtype=torch.float64)
|
| 727 |
+
|
| 728 |
+
|
| 729 |
+
def loss_and_stats(out, sequence, denominators=None, world=1):
|
| 730 |
+
heads = out.get("heads") or [out["logits"]]
|
| 731 |
+
loss = out["logits"].float().sum() * 0
|
| 732 |
+
for j, logits in enumerate(heads):
|
| 733 |
+
target, valid = targets_and_mask(sequence, j+1)
|
| 734 |
+
n = target.shape[1]
|
| 735 |
+
ce = F.cross_entropy(logits[:, :n].float().reshape(-1, VOCAB), target.reshape(-1), reduction="none").view_as(target)
|
| 736 |
+
denom = valid.sum().clamp_min(1) if denominators is None else denominators[j].clamp_min(1)
|
| 737 |
+
# DDP averages gradients; world/whole-update denominator makes this a
|
| 738 |
+
# true global token mean even with varying numbers of document markers.
|
| 739 |
+
loss = loss + (ce * valid).sum() * world / denom / len(heads)
|
| 740 |
+
if j == 0:
|
| 741 |
+
byte = valid & (target < 256)
|
| 742 |
+
correct = logits[:, :n].argmax(-1) == target
|
| 743 |
+
stats = torch.stack(((ce * valid).sum(), valid.sum(), (ce * byte).sum(),
|
| 744 |
+
byte.sum(), (correct & byte).sum())).detach().double()
|
| 745 |
+
return loss, stats
|
| 746 |
+
|
| 747 |
+
|
unified/ablations/unified-v1-blt-none-s42/source/checkpoints.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Atomic full-state checkpoints, verified newest-first local/Hub recovery."""
|
| 2 |
+
import os,json,time,hashlib,shutil,uuid
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import torch
|
| 5 |
+
from huggingface_hub import HfApi,hf_hub_download,CommitOperationAdd,CommitOperationDelete
|
| 6 |
+
from huggingface_hub.errors import RepositoryNotFoundError
|
| 7 |
+
from dataset_registry import retry
|
| 8 |
+
from bet_model import BETConfig,EXPECTED_ARCH_SHAPE_SHA256
|
| 9 |
+
|
| 10 |
+
FORMAT='cortex-sparkbet9m-full-v1'
|
| 11 |
+
DEFAULT_HUB_CHECKPOINT_PREFIX='checkpoints/sparkbet9m'
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def digest(path):
|
| 15 |
+
h=hashlib.sha256()
|
| 16 |
+
with open(path,'rb') as f:
|
| 17 |
+
for b in iter(lambda:f.read(4*1024*1024),b''):h.update(b)
|
| 18 |
+
return h.hexdigest()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def atomic_json(path,value):
|
| 22 |
+
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True);tmp=path.with_name(path.name+'.tmp')
|
| 23 |
+
with open(tmp,'w') as f:json.dump(value,f,indent=2);f.flush();os.fsync(f.fileno())
|
| 24 |
+
os.replace(tmp,path)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def pipeline_hash(root):
|
| 28 |
+
h=hashlib.sha256()
|
| 29 |
+
for p in sorted(Path(root).glob('*.py')):h.update(p.name.encode());h.update(p.read_bytes())
|
| 30 |
+
return h.hexdigest()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def save(run,state,keep=3):
|
| 34 |
+
root=Path(run)/'checkpoints';root.mkdir(parents=True,exist_ok=True)
|
| 35 |
+
name=f'checkpoint-{state["step"]:012d}';dest=root/name
|
| 36 |
+
if (dest/'COMPLETE').exists():return dest
|
| 37 |
+
tmp=root/('.incomplete-'+uuid.uuid4().hex);tmp.mkdir()
|
| 38 |
+
try:
|
| 39 |
+
with open(tmp/'training.pt','wb') as f:torch.save(state,f);f.flush();os.fsync(f.fileno())
|
| 40 |
+
meta=dict(format=FORMAT,step=state['step'],lineage=state['lineage'],architecture=EXPECTED_ARCH_SHAPE_SHA256,
|
| 41 |
+
pipeline=state['pipeline'],sha256=digest(tmp/'training.pt'),bytes=(tmp/'training.pt').stat().st_size,saved_at=time.time())
|
| 42 |
+
atomic_json(tmp/'metadata.json',meta);(tmp/'COMPLETE').write_text('complete\n')
|
| 43 |
+
if dest.exists():shutil.rmtree(dest)
|
| 44 |
+
os.replace(tmp,dest);atomic_json(Path(run)/'latest.json',dict(path=str(dest),**meta))
|
| 45 |
+
complete=sorted(p for p in root.glob('checkpoint-*') if (p/'COMPLETE').exists())
|
| 46 |
+
for p in complete[:-keep]:shutil.rmtree(p)
|
| 47 |
+
return dest
|
| 48 |
+
except BaseException:
|
| 49 |
+
if tmp.exists():shutil.rmtree(tmp)
|
| 50 |
+
raise
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def verified_load(path):
|
| 54 |
+
path=Path(path);meta=json.loads((path/'metadata.json').read_text())
|
| 55 |
+
if meta['format']!=FORMAT or meta['architecture']!=EXPECTED_ARCH_SHAPE_SHA256:raise ValueError('Incompatible checkpoint metadata')
|
| 56 |
+
file=path/'training.pt'
|
| 57 |
+
if file.stat().st_size!=meta['bytes'] or digest(file)!=meta['sha256']:raise ValueError('Checkpoint checksum mismatch')
|
| 58 |
+
state=torch.load(file,map_location='cpu',weights_only=True)
|
| 59 |
+
if state['format']!=FORMAT or state['step']!=meta['step'] or state['lineage']!=meta['lineage']:raise ValueError('Checkpoint state/metadata mismatch')
|
| 60 |
+
return state
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def candidates(roots,repo=None,cache=None,checkpoint_prefix=DEFAULT_HUB_CHECKPOINT_PREFIX):
|
| 64 |
+
items=[]
|
| 65 |
+
for root in roots:
|
| 66 |
+
root=Path(root)
|
| 67 |
+
if not root.exists():continue
|
| 68 |
+
for p in root.rglob('metadata.json'):
|
| 69 |
+
if not (p.parent/'COMPLETE').exists() or not (p.parent/'training.pt').exists():continue
|
| 70 |
+
try:
|
| 71 |
+
m=json.loads(p.read_text())
|
| 72 |
+
if m.get('format')==FORMAT and m.get('architecture')==EXPECTED_ARCH_SHAPE_SHA256:
|
| 73 |
+
items.append(dict(meta=m,path=p.parent,remote=False))
|
| 74 |
+
except (ValueError,OSError):continue
|
| 75 |
+
if repo:
|
| 76 |
+
api=HfApi()
|
| 77 |
+
try:
|
| 78 |
+
info=retry(lambda:api.model_info(repo));files=[f.rfilename for f in info.siblings];revision=info.sha
|
| 79 |
+
except RepositoryNotFoundError:files=[];revision=None
|
| 80 |
+
prefix=checkpoint_prefix.strip('/')+'/'
|
| 81 |
+
for f in files:
|
| 82 |
+
if f.startswith(prefix) and f.endswith('/metadata.json') and f.rsplit('/',1)[0]+'/COMPLETE' in files:
|
| 83 |
+
local=retry(lambda:hf_hub_download(repo,f,revision=revision,cache_dir=cache));m=json.loads(Path(local).read_text())
|
| 84 |
+
if m.get('format')==FORMAT and m.get('architecture')==EXPECTED_ARCH_SHAPE_SHA256:
|
| 85 |
+
items.append(dict(meta=m,prefix=f.rsplit('/',1)[0],remote=True,revision=revision))
|
| 86 |
+
return sorted(items,key=lambda x:(x['meta']['step'],x['meta']['saved_at']),reverse=True)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def resume(roots,repo,run,expected_pipeline,checkpoint_prefix=DEFAULT_HUB_CHECKPOINT_PREFIX,
|
| 90 |
+
allowed_previous_pipelines=(),migration_schedule='progressive_data_stages_v2',
|
| 91 |
+
aux_stage_base_updates=128):
|
| 92 |
+
items=candidates(roots,repo,str(Path(run)/'hub_cache'),checkpoint_prefix)
|
| 93 |
+
lineages={i['meta']['lineage'] for i in items}
|
| 94 |
+
if len(lineages)>1:
|
| 95 |
+
raise RuntimeError('Multiple SparkBET run lineages found. Select the intended run instead of mixing checkpoint histories.')
|
| 96 |
+
for item in items:
|
| 97 |
+
try:
|
| 98 |
+
if item['remote']:
|
| 99 |
+
dest=Path(run)/'hub_resume'/Path(item['prefix']).name;dest.mkdir(parents=True,exist_ok=True)
|
| 100 |
+
for n in ['metadata.json','training.pt','COMPLETE']:
|
| 101 |
+
retry(lambda n=n:hf_hub_download(repo,item['prefix']+'/'+n,revision=item['revision'],local_dir=Path(run)/'hub_download'))
|
| 102 |
+
source=Path(run)/'hub_download'/item['prefix']/n;os.replace(source,dest/n)
|
| 103 |
+
path=dest
|
| 104 |
+
else:path=item['path']
|
| 105 |
+
state=verified_load(path)
|
| 106 |
+
except (OSError,ValueError,EOFError,RuntimeError) as exc:
|
| 107 |
+
print(f'Resume candidate step {item["meta"]["step"]} unusable: {type(exc).__name__}; trying older complete checkpoint.',flush=True);continue
|
| 108 |
+
if state['pipeline']!=expected_pipeline:
|
| 109 |
+
# Only an explicitly listed, exact previous pipeline fingerprint can
|
| 110 |
+
# cross the auxiliary-scheduling objective boundary. Never accept
|
| 111 |
+
# unrelated code/data changes or silently restart data cursors.
|
| 112 |
+
previous=state['pipeline']
|
| 113 |
+
if previous not in allowed_previous_pipelines:
|
| 114 |
+
raise RuntimeError('Training/data code differs from this SparkBET checkpoint. Exact data cursors are preserved by refusing an implicit migration.')
|
| 115 |
+
# Start the new long-form curriculum at L1 right here, not at
|
| 116 |
+
# an arbitrary stage computed from the historical global step.
|
| 117 |
+
config=state.setdefault('config',{})
|
| 118 |
+
config['aux_loop_schedule']=migration_schedule
|
| 119 |
+
config['aux_stage_base_updates']=int(aux_stage_base_updates)
|
| 120 |
+
config['aux_curriculum_origin_step']=int(state['step'])
|
| 121 |
+
state.setdefault('schedule_migrations',[]).append(dict(
|
| 122 |
+
step=state['step'],previous_pipeline=previous,
|
| 123 |
+
new_pipeline=expected_pipeline,schedule=migration_schedule,
|
| 124 |
+
curriculum_origin_step=int(state['step']),
|
| 125 |
+
aux_stage_base_updates=int(aux_stage_base_updates)))
|
| 126 |
+
state['pipeline']=expected_pipeline
|
| 127 |
+
print(f'Verified data-stage migration at update {state["step"]:,}; preserving model, optimizer, scaler and data cursors. New curriculum begins at L1.',flush=True)
|
| 128 |
+
print(f'Resumed complete state at update {state["step"]:,} from {"Hub" if item["remote"] else "local"}.',flush=True)
|
| 129 |
+
return state
|
| 130 |
+
if items:raise RuntimeError('Checkpoints exist but none passed verification; refusing a silent fresh run')
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def legacy_import(roots,explicit=None):
|
| 135 |
+
# The previous BET-74M tensors/optimizer are shape-incompatible with SparkBET-9M.
|
| 136 |
+
# Do not silently splice two architectures. New SparkBET sessions can still
|
| 137 |
+
# recover complete SparkBET state from local/Hub checkpoints above.
|
| 138 |
+
if explicit:
|
| 139 |
+
raise ValueError('The legacy BET-74M checkpoint cannot be migrated into the 9.35M SparkBET architecture. Start fresh or use a SparkBET full-state checkpoint.')
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class Publisher:
|
| 144 |
+
def __init__(self,repo,run,keep=2,checkpoint_prefix=DEFAULT_HUB_CHECKPOINT_PREFIX):
|
| 145 |
+
self.repo,self.run,self.keep=repo,Path(run),keep
|
| 146 |
+
self.checkpoint_prefix=checkpoint_prefix.strip('/')
|
| 147 |
+
self.api=HfApi()
|
| 148 |
+
def authenticate(self):
|
| 149 |
+
retry(lambda:self.api.whoami());retry(lambda:self.api.create_repo(self.repo,repo_type='model',exist_ok=True))
|
| 150 |
+
def logs(self):
|
| 151 |
+
folder=self.run/'publish_logs'
|
| 152 |
+
if folder.exists():
|
| 153 |
+
retry(lambda:self.api.upload_folder(repo_id=self.repo,folder_path=folder,path_in_repo='runs',commit_message='Cortex training metrics'))
|
| 154 |
+
events=sorted(folder.rglob('*tfevents*'),key=lambda p:p.stat().st_mtime)
|
| 155 |
+
for p in events[:-2]:p.unlink()
|
| 156 |
+
def checkpoint(self,path):
|
| 157 |
+
path=Path(path);prefix=self.checkpoint_prefix+'/'+path.name;files=retry(lambda:self.api.list_repo_files(self.repo))
|
| 158 |
+
base=self.checkpoint_prefix+'/'
|
| 159 |
+
old=sorted({f[len(base):].split('/')[0] for f in files if f.startswith(base+'checkpoint-') and f.endswith('/COMPLETE')})
|
| 160 |
+
keep=set(sorted(set(old+[path.name]))[-self.keep:])
|
| 161 |
+
operations=[CommitOperationAdd(path_in_repo=prefix+'/'+p.name,path_or_fileobj=str(p)) for p in path.iterdir() if p.is_file()]
|
| 162 |
+
for f in files:
|
| 163 |
+
if f.startswith(base+'checkpoint-'):
|
| 164 |
+
name=f[len(base):].split('/')[0]
|
| 165 |
+
if name not in keep:operations.append(CommitOperationDelete(path_in_repo=f))
|
| 166 |
+
retry(lambda:self.api.create_commit(repo_id=self.repo,operations=operations,commit_message=f'Full resumable SparkBET-9M state at {path.name}'))
|
| 167 |
+
def export(self,model,step,manifest,source_root):
|
| 168 |
+
from safetensors.torch import save_file
|
| 169 |
+
from dataclasses import asdict
|
| 170 |
+
dest=self.run/'export';dest.mkdir(exist_ok=True)
|
| 171 |
+
# HF wrapper owns the training model as `core`, so exported tensor names are
|
| 172 |
+
# prefixed accordingly and load strictly through AutoModelForCausalLM.
|
| 173 |
+
weights={f'core.{k}':v.detach().cpu().contiguous() for k,v in model.state_dict().items()}
|
| 174 |
+
save_file(weights,str(dest/'model.safetensors'),metadata={'format':'pt'})
|
| 175 |
+
c=asdict(model.c)
|
| 176 |
+
config=dict(
|
| 177 |
+
model_type='bet',architectures=['BETForCausalLM'],step=step,
|
| 178 |
+
vocab_size=c['vocab_size'],hidden_size=c['hidden_size'],intermediate_size=c['intermediate_size'],
|
| 179 |
+
prelude_layers=c['prelude_layers'],body_blocks=c['body_blocks'],coda_layers=c['coda_layers'],
|
| 180 |
+
num_attention_heads=c['num_heads'],num_key_value_heads=c['num_kv_heads'],head_dim=c['head_dim'],
|
| 181 |
+
lora_rank=c['lora_rank'],hyper_lanes=c['hyper_lanes'],max_position_embeddings=c['max_seq_len'],max_loops=c['max_loops'],
|
| 182 |
+
rope_theta=c['rope_theta'],rms_norm_eps=c['rms_eps'],ddl_beta_init=c['ddl_beta_init'],ddl_k_eps=c['ddl_k_eps'],
|
| 183 |
+
ddl_v_sigmoid_scale=c['ddl_v_sigmoid_scale'],refinement_cycles=8,use_cache=False,tie_word_embeddings=True,
|
| 184 |
+
pad_token_id=256,bos_token_id=257,eos_token_id=258,precision='fp16 autocast / fp32 master',
|
| 185 |
+
auto_map={'AutoConfig':'configuration_bet.BETConfig','AutoModelForCausalLM':'modeling_bet.BETForCausalLM','AutoTokenizer':['tokenization_bet.BETByteTokenizer',None]},
|
| 186 |
+
)
|
| 187 |
+
atomic_json(dest/'config.json',config);atomic_json(dest/'dataset_manifest.json',manifest)
|
| 188 |
+
for name in ['bet_model.py','inference.py','records.py','README.md','configuration_bet.py','modeling_bet.py','tokenization_bet.py','tokenizer_config.json']:
|
| 189 |
+
shutil.copy2(Path(source_root)/name,dest/name)
|
| 190 |
+
retry(lambda:self.api.upload_folder(repo_id=self.repo,folder_path=dest,commit_message=f'SparkBET-9M inference export at update {step}'))
|
unified/ablations/unified-v1-blt-none-s42/source/configuration_bet.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import PretrainedConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class BETConfig(PretrainedConfig):
|
| 5 |
+
model_type = "bet"
|
| 6 |
+
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
vocab_size=259,
|
| 10 |
+
hidden_size=324,
|
| 11 |
+
intermediate_size=864,
|
| 12 |
+
prelude_layers=1,
|
| 13 |
+
body_blocks=6,
|
| 14 |
+
coda_layers=1,
|
| 15 |
+
num_attention_heads=6,
|
| 16 |
+
num_key_value_heads=2,
|
| 17 |
+
head_dim=54,
|
| 18 |
+
lora_rank=16,
|
| 19 |
+
hyper_lanes=2,
|
| 20 |
+
max_position_embeddings=1024,
|
| 21 |
+
max_loops=8,
|
| 22 |
+
rope_theta=10_000.0,
|
| 23 |
+
rms_norm_eps=1e-6,
|
| 24 |
+
ddl_beta_init=1.0,
|
| 25 |
+
ddl_k_eps=1e-2,
|
| 26 |
+
ddl_v_sigmoid_scale=4.0,
|
| 27 |
+
refinement_cycles=8,
|
| 28 |
+
use_cache=False,
|
| 29 |
+
tie_word_embeddings=True,
|
| 30 |
+
pad_token_id=256,
|
| 31 |
+
bos_token_id=257,
|
| 32 |
+
eos_token_id=258,
|
| 33 |
+
**kwargs,
|
| 34 |
+
):
|
| 35 |
+
super().__init__(
|
| 36 |
+
pad_token_id=pad_token_id,
|
| 37 |
+
bos_token_id=bos_token_id,
|
| 38 |
+
eos_token_id=eos_token_id,
|
| 39 |
+
tie_word_embeddings=tie_word_embeddings,
|
| 40 |
+
is_encoder_decoder=False,
|
| 41 |
+
**kwargs,
|
| 42 |
+
)
|
| 43 |
+
self.vocab_size=int(vocab_size)
|
| 44 |
+
self.hidden_size=int(hidden_size)
|
| 45 |
+
self.intermediate_size=int(intermediate_size)
|
| 46 |
+
self.prelude_layers=int(prelude_layers)
|
| 47 |
+
self.body_blocks=int(body_blocks)
|
| 48 |
+
self.coda_layers=int(coda_layers)
|
| 49 |
+
# Common HF tooling expects num_hidden_layers even though only the body loops.
|
| 50 |
+
self.num_hidden_layers=int(prelude_layers+body_blocks+coda_layers)
|
| 51 |
+
self.num_attention_heads=int(num_attention_heads)
|
| 52 |
+
self.num_key_value_heads=int(num_key_value_heads)
|
| 53 |
+
self.head_dim=int(head_dim)
|
| 54 |
+
self.lora_rank=int(lora_rank)
|
| 55 |
+
self.hyper_lanes=int(hyper_lanes)
|
| 56 |
+
self.max_position_embeddings=int(max_position_embeddings)
|
| 57 |
+
self.max_loops=int(max_loops)
|
| 58 |
+
self.rope_theta=float(rope_theta)
|
| 59 |
+
self.rms_norm_eps=float(rms_norm_eps)
|
| 60 |
+
self.ddl_beta_init=float(ddl_beta_init)
|
| 61 |
+
self.ddl_k_eps=float(ddl_k_eps)
|
| 62 |
+
self.ddl_v_sigmoid_scale=float(ddl_v_sigmoid_scale)
|
| 63 |
+
self.refinement_cycles=int(refinement_cycles)
|
| 64 |
+
self.use_cache=bool(use_cache)
|
unified/ablations/unified-v1-blt-none-s42/source/cortex_curriculum_v16.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
unified/ablations/unified-v1-blt-none-s42/source/cortex_data.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic answer supervision and executable compositional train/test splits."""
|
| 2 |
+
import hashlib
|
| 3 |
+
import random
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
REVISION = 'cortex-answer-execution-v1'
|
| 7 |
+
HELD_PAIRS = {(1, 2), (2, 1)} # rotate -> swap and swap -> rotate, by semantic operation
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def program_split(program):
|
| 11 |
+
return 'composition' if any(pair in HELD_PAIRS for pair in zip(program, program[1:])) else 'train'
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def sample_program(rng, split, depth):
|
| 15 |
+
for _ in range(10000):
|
| 16 |
+
program = tuple(rng.randrange(4) for _ in range(depth))
|
| 17 |
+
if program_split(program) == ('composition' if split == 'composition' else 'train'):
|
| 18 |
+
return program
|
| 19 |
+
raise RuntimeError('could not sample program split')
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def execute(values, program, permutation):
|
| 23 |
+
states = []
|
| 24 |
+
values = list(values)
|
| 25 |
+
for op in program:
|
| 26 |
+
if op == 0:
|
| 27 |
+
values = values[::-1]
|
| 28 |
+
elif op == 1:
|
| 29 |
+
values = values[1:] + values[:1]
|
| 30 |
+
elif op == 2:
|
| 31 |
+
values = values[:]
|
| 32 |
+
for i in range(0, len(values) - 1, 2):
|
| 33 |
+
values[i], values[i + 1] = values[i + 1], values[i]
|
| 34 |
+
elif op == 3:
|
| 35 |
+
values = [permutation[x] for x in values]
|
| 36 |
+
else:
|
| 37 |
+
raise ValueError(op)
|
| 38 |
+
states.append(values[:])
|
| 39 |
+
return states
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def composition_episode(c, seed, context, split='train', difficulty=1, trace=None):
|
| 43 |
+
"""Reusable sequence operations with held-out *semantic* ordered pairs.
|
| 44 |
+
|
| 45 |
+
Byte roles, alphabets and substitution maps are independently randomized.
|
| 46 |
+
Training/query demonstrations NEVER include held-out pairs or depths > 4.
|
| 47 |
+
Trace mode emits intermediate states autoregressively; direct mode emits only
|
| 48 |
+
the result. The mode and separators are grounded by demonstrations in both.
|
| 49 |
+
"""
|
| 50 |
+
rng = random.Random(int(seed))
|
| 51 |
+
surface = c.Surface(rng)
|
| 52 |
+
atoms = surface.atoms(6)
|
| 53 |
+
ops = surface.atoms(4)
|
| 54 |
+
begin, sep, end, chain, mode, state_sep = surface.take(6)
|
| 55 |
+
perm = list(range(6)); rng.shuffle(perm)
|
| 56 |
+
trace = bool(rng.randrange(2)) if trace is None else bool(trace)
|
| 57 |
+
depth = rng.randint(5, 8) if split == 'depth' else rng.randint(2 if split == 'composition' else 1, 2 if difficulty == 0 else 4)
|
| 58 |
+
length = rng.randint(13, 20) if split == 'length' else rng.randint(4, 8 if difficulty == 0 else 12)
|
| 59 |
+
program = sample_program(rng, split, depth)
|
| 60 |
+
b = c.Builder(25, 'compositional_execution')
|
| 61 |
+
def encode(xs): return [atoms[x] for x in xs]
|
| 62 |
+
def query(xs, prog): return [begin] + encode(xs) + [chain] + [ops[o] for o in prog] + [sep]
|
| 63 |
+
def answer(states, traced):
|
| 64 |
+
selected = states if traced else states[-1:]
|
| 65 |
+
result = []
|
| 66 |
+
for i, state in enumerate(selected):
|
| 67 |
+
if i: result.append(state_sep)
|
| 68 |
+
result.extend(encode(state))
|
| 69 |
+
return result
|
| 70 |
+
# Complete substitution table; unlike a few random demos, covers every query symbol.
|
| 71 |
+
for x in range(6):
|
| 72 |
+
b.emit(query([x], (3,)) + encode([perm[x]]) + [end])
|
| 73 |
+
# Distinct symbols and odd/even widths disambiguate positional primitives.
|
| 74 |
+
for op in range(3):
|
| 75 |
+
for width in (5, 6):
|
| 76 |
+
xs = rng.sample(range(6), width)
|
| 77 |
+
b.emit(query(xs, (op,)) + encode(execute(xs, (op,), perm)[-1]) + [end])
|
| 78 |
+
# The same local mode marker/grammar is used for demonstrations and query.
|
| 79 |
+
for demo_depth in (2, 3):
|
| 80 |
+
xs = [rng.randrange(6) for _ in range(5)]
|
| 81 |
+
prog = sample_program(rng, 'train', demo_depth)
|
| 82 |
+
b.emit([mode] + query(xs, prog) + answer(execute(xs, prog, perm), trace) + [end])
|
| 83 |
+
xs = [rng.randrange(6) for _ in range(length)]
|
| 84 |
+
b.emit([mode] + query(xs, program)); b.mark_prompt()
|
| 85 |
+
b.emit(answer(execute(xs, program, perm), trace), c.W_TARGET)
|
| 86 |
+
ep = b.finish(context)
|
| 87 |
+
ep.program = program; ep.split = split; ep.trace = trace
|
| 88 |
+
ep.latent_input = xs; ep.permutation = perm; ep.atoms = atoms; ep.state_sep = state_sep
|
| 89 |
+
ep.weights = [0] * ep.prompt_len + [c.W_TARGET] * (len(ep.tokens) - ep.prompt_len)
|
| 90 |
+
return ep
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def answer_episode(c, entry, difficulty, context, seed, candidates=1):
|
| 94 |
+
"""One inference protocol for both training and evaluation, including EOS.
|
| 95 |
+
|
| 96 |
+
Remove ignored suffix terminators instead of feeding an unsupervised byte
|
| 97 |
+
during generation. Keep grid shape/format tokens and supervise the complete
|
| 98 |
+
continuation. Random demonstration outputs are context, never loss targets.
|
| 99 |
+
"""
|
| 100 |
+
public = entry['name'] == 'gen_arc_static_public_train'
|
| 101 |
+
ep, _ = c._v12_generate_episode(entry, difficulty, context, seed,
|
| 102 |
+
'weighted' if public else 'answer', candidates)
|
| 103 |
+
tail=ep.tokens[ep.prompt_len:]
|
| 104 |
+
weights=ep.weights[ep.prompt_len:]
|
| 105 |
+
last=max((i for i,(t,w) in enumerate(zip(tail,weights)) if w>0 and t!=c.EOS),default=-1)
|
| 106 |
+
suffix=tail[:last+1]
|
| 107 |
+
if not suffix:
|
| 108 |
+
raise ValueError(f"empty answer: {entry['name']}")
|
| 109 |
+
ep.tokens = ep.tokens[:ep.prompt_len] + suffix + [c.EOS]
|
| 110 |
+
ep.weights = [0] * ep.prompt_len + [c.W_TARGET] * (len(suffix) + 1)
|
| 111 |
+
ep.validate(context)
|
| 112 |
+
return ep
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def padded_batch(c, episodes):
|
| 116 |
+
# Each row is an independent episode: no cross-example attention, no dropped
|
| 117 |
+
# overflow bins, no long-episode preference. Bucket only after sampling.
|
| 118 |
+
width = ((max(len(ep.tokens) - 1 for ep in episodes) + 31) // 32) * 32 + 1
|
| 119 |
+
tok = np.full((len(episodes), width), c.PAD, dtype=np.int64)
|
| 120 |
+
wt = np.zeros(tok.shape, dtype=np.float32)
|
| 121 |
+
seg = np.full(tok.shape, -1, dtype=np.int64)
|
| 122 |
+
for i, ep in enumerate(episodes):
|
| 123 |
+
n = len(ep.tokens)
|
| 124 |
+
tok[i, :n] = ep.tokens; wt[i, :n] = ep.weights; seg[i, :n] = i
|
| 125 |
+
return tok, wt, seg, len(episodes), float(np.mean(tok != c.PAD))
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def source_digest(*paths):
|
| 129 |
+
from pathlib import Path
|
| 130 |
+
h = hashlib.sha256()
|
| 131 |
+
for path in paths: h.update(Path(path).read_bytes())
|
| 132 |
+
return h.hexdigest()
|
unified/ablations/unified-v1-blt-none-s42/source/cortex_oracles.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Independent small-domain reference algorithms used only by tests/audits."""
|
| 2 |
+
from itertools import permutations, product
|
| 3 |
+
from functools import reduce
|
| 4 |
+
|
| 5 |
+
def paths(edges,start,goal=None):
|
| 6 |
+
out=[]
|
| 7 |
+
def visit(path):
|
| 8 |
+
if goal is None or path[-1]==goal:out.append(path)
|
| 9 |
+
for a,b in edges:
|
| 10 |
+
if a==path[-1] and b not in path:visit(path+[b])
|
| 11 |
+
visit([start]);return out
|
| 12 |
+
|
| 13 |
+
def program_reference(xs,program,mapping):
|
| 14 |
+
states=[];xs=list(xs)
|
| 15 |
+
for op in program:
|
| 16 |
+
n=len(xs)
|
| 17 |
+
if op=='reverse':indices=range(n-1,-1,-1)
|
| 18 |
+
elif op=='rotate':indices=[(i+1)%n for i in range(n)]
|
| 19 |
+
elif op=='swap':indices=[i+1 if i%2==0 and i+1<n else i-1 if i%2 else i for i in range(n)]
|
| 20 |
+
elif op=='map':xs=[dict(enumerate(mapping))[v] for v in xs];states.append(xs[:]);continue
|
| 21 |
+
elif op=='repeat':indices=[i for i in range(n) for _ in range(2)]
|
| 22 |
+
elif op=='evens':indices=range(0,n,2)
|
| 23 |
+
elif op=='dedup':indices=[i for i in range(n) if xs[i] not in xs[:i]]
|
| 24 |
+
else:raise KeyError(op)
|
| 25 |
+
xs=[xs[i] for i in indices];states.append(xs[:])
|
| 26 |
+
return states
|
| 27 |
+
|
| 28 |
+
def oracle(name,x):
|
| 29 |
+
if name=='selection_sort':
|
| 30 |
+
a=x[:];states=[]
|
| 31 |
+
for i in range(len(a)-1):
|
| 32 |
+
value=sorted(a[i:])[0];j=a.index(value,i);a[j],a[i]=a[i],a[j];states.append(a[:])
|
| 33 |
+
return states
|
| 34 |
+
if name=='insertion_sort':return [sorted(x[:i+1])+x[i+1:] for i in range(1,len(x))]
|
| 35 |
+
if name=='minmax_scan':return [[sorted(x[:i])[0],sorted(x[:i])[-1]] for i in range(1,len(x)+1)]
|
| 36 |
+
if name=='linear_search':
|
| 37 |
+
a,t=x;return [a.index(t) if t in a else -1]
|
| 38 |
+
if name=='binary_search':
|
| 39 |
+
a,t=x
|
| 40 |
+
def visit(lo,hi):
|
| 41 |
+
if lo>=hi:return []
|
| 42 |
+
m=(lo+hi-1)//2
|
| 43 |
+
return [m]+([] if a[m]==t else visit(m+1,hi) if a[m]<t else visit(lo,m))
|
| 44 |
+
return [visit(0,len(a)),a.index(t) if t in a else -1]
|
| 45 |
+
if name=='string_match':
|
| 46 |
+
a,b=x;return [i for i in range(len(a)) if tuple(a[i:i+len(b)])==tuple(b)]
|
| 47 |
+
if name in ('bfs_order','dfs_order'):
|
| 48 |
+
edges,start=x
|
| 49 |
+
if name=='bfs_order':
|
| 50 |
+
# Minimal distance and lexicographically smallest discovery path.
|
| 51 |
+
pp=paths(edges,start);best={}
|
| 52 |
+
for p in pp:
|
| 53 |
+
k=(len(p),p)
|
| 54 |
+
if p[-1] not in best or k<best[p[-1]]:best[p[-1]]=k
|
| 55 |
+
return sorted(best,key=best.__getitem__)
|
| 56 |
+
out=[]
|
| 57 |
+
def visit(v):
|
| 58 |
+
if v in out:return
|
| 59 |
+
out.append(v)
|
| 60 |
+
for z in sorted(b for a,b in edges if a==v):visit(z)
|
| 61 |
+
visit(start);return out
|
| 62 |
+
if name=='topo_sort':
|
| 63 |
+
nodes,edges=x
|
| 64 |
+
return list(min(p for p in permutations(nodes) if all(p.index(a)<p.index(b) for a,b in edges)))
|
| 65 |
+
if name=='shortest_path':
|
| 66 |
+
pp=paths(*x);return min(pp,key=lambda p:(len(p),p)) if pp else []
|
| 67 |
+
if name=='union_find':
|
| 68 |
+
edges,a,b=x;components=[{v} for edge in edges for v in edge]+[{a},{b}]
|
| 69 |
+
for u,v in edges:
|
| 70 |
+
joined=set().union(*(s for s in components if u in s or v in s));components=[s for s in components if not(s&joined)]+[joined]
|
| 71 |
+
return any({a,b}<=s for s in components)
|
| 72 |
+
if name=='interval_overlap':
|
| 73 |
+
a,b,c,d=x;return bool(set(range(a,b+1))&set(range(c,d+1)))
|
| 74 |
+
if name in ('bracket_balance','bracket_depth'):
|
| 75 |
+
balances=[sum(1 if t=='open' else -1 for t in x[:i]) for i in range(1,len(x)+1)]
|
| 76 |
+
return (all(v>=0 for v in balances) and (balances[-1] if balances else 0)==0) if name=='bracket_balance' else max([0]+balances)
|
| 77 |
+
if name in ('queue','deque'):
|
| 78 |
+
q=[];removed=[]
|
| 79 |
+
for cmd,*args in x:
|
| 80 |
+
if cmd=='left':q=[args[0]]+q
|
| 81 |
+
elif cmd=='right':q=q+[args[0]]
|
| 82 |
+
elif not q:removed.append('empty')
|
| 83 |
+
elif cmd=='pop_left':removed.append(q[0]);q=q[1:]
|
| 84 |
+
elif cmd=='pop_right':removed.append(q[-1]);q=q[:-1]
|
| 85 |
+
else:raise KeyError(cmd)
|
| 86 |
+
return [removed,q]
|
| 87 |
+
if name in ('kv_lookup','role_filler','assoc_distractors','nested_lookup','mutable_dictionary','delayed_recall'):
|
| 88 |
+
records,(key,depth)=x
|
| 89 |
+
for _ in range(depth):key=next(v for k,v in reversed(records) if k==key)
|
| 90 |
+
return key
|
| 91 |
+
if name in ('register_swap','register_update','multi_register','program_trace'):
|
| 92 |
+
state,cmds=x;versions=[tuple(state)]
|
| 93 |
+
for op,a,b in cmds:
|
| 94 |
+
old=versions[-1]
|
| 95 |
+
versions.append(tuple(b if op=='set' and i==a else old[b] if op in ('swap','copy') and i==a else old[a] if op=='swap' and i==b else old[i] for i in range(len(old))))
|
| 96 |
+
return [list(v) for v in versions[1:]] if name=='program_trace' else list(versions[-1])
|
| 97 |
+
if name=='conditional_branch':return x[1:][0 if x[0] else 1]
|
| 98 |
+
if name=='loop_counter':return sorted(range(x+1),reverse=True)
|
| 99 |
+
if name in ('stack_expr','prefix_expr','postfix_expr'):
|
| 100 |
+
# Convert either notation to a syntax tree, then evaluate the tree.
|
| 101 |
+
tokens=[t for t in x if t!='push'];stack=[]
|
| 102 |
+
for t in (reversed(tokens) if name=='prefix_expr' else tokens):
|
| 103 |
+
if type(t) is int:stack.append(t)
|
| 104 |
+
else:
|
| 105 |
+
a,b=stack.pop(),stack.pop();stack.append((t,a,b))
|
| 106 |
+
def evaluate(t):
|
| 107 |
+
if type(t) is int:return t
|
| 108 |
+
op,a,b=t;a,b=evaluate(a),evaluate(b)
|
| 109 |
+
return sum([a,b]) if op=='add' else sum(a for _ in range(b))
|
| 110 |
+
return evaluate(stack[0])
|
| 111 |
+
if name=='relative_coords':
|
| 112 |
+
n,pos,moves=x;out=list(pos)
|
| 113 |
+
for move in moves:out=[sorted([0,v+d,n-1])[1] for v,d in zip(out,move)]
|
| 114 |
+
return out
|
| 115 |
+
if name=='toroidal':
|
| 116 |
+
n,p,moves=x;return (p+sum(moves))%n
|
| 117 |
+
if name=='collision':
|
| 118 |
+
a,va,b,vb,t=x;out=[]
|
| 119 |
+
# Equal masses in 1D = freely crossing ghost particles with sorted positions.
|
| 120 |
+
for k in range(1,t+1):
|
| 121 |
+
ghosts=sorted([(a+k*va,va),(b+k*vb,vb)],key=lambda z:(z[0],z[1]))
|
| 122 |
+
out.append([ghosts[0][0],ghosts[1][0],ghosts[0][1],ghosts[1][1]])
|
| 123 |
+
return out
|
| 124 |
+
if name=='falling':return list(map(list,zip(*(sorted(col) for col in zip(*x)))))
|
| 125 |
+
if name=='obstacle_motion':
|
| 126 |
+
n,p,blocked,moves=x;history=[p]
|
| 127 |
+
for m in moves:history.append(history[-1]+m if history[-1]+m in set(range(n))-set(blocked) else history[-1])
|
| 128 |
+
return history
|
| 129 |
+
if name=='line_of_sight':
|
| 130 |
+
a,b,blocked=x;return len(set(range(min(a,b)+1,max(a,b)))&set(blocked))==0
|
| 131 |
+
if name=='containment':
|
| 132 |
+
(a,b),(c,d)=x;return set(range(c,d+1))<=set(range(a,b+1))
|
| 133 |
+
if name in ('partial_map','dynamic_plan'):
|
| 134 |
+
if name=='partial_map':
|
| 135 |
+
observations,start,goal=x;edges=set()
|
| 136 |
+
for view in observations:edges.update(map(tuple,view))
|
| 137 |
+
else:
|
| 138 |
+
original,changes,start,goal=x
|
| 139 |
+
# For each edge, its last update determines membership.
|
| 140 |
+
candidates=set(map(tuple,original))|{(a,b) for _,a,b in changes}
|
| 141 |
+
edges={edge for edge in candidates if next((op=='add' for op,a,b in reversed(changes) if (a,b)==edge),edge in set(map(tuple,original)))}
|
| 142 |
+
choices=paths(sorted(edges),start,goal)
|
| 143 |
+
return min(choices,key=lambda p:(len(p),p)) if choices else []
|
| 144 |
+
if name in ('path_keys','door_key','sparse_nav','irreversible_action'):
|
| 145 |
+
edges,start,goal,keys,locks=x;front=[(start,start in keys,[])];seen=set()
|
| 146 |
+
while front:
|
| 147 |
+
node,key,p=front.pop(0)
|
| 148 |
+
if node==goal:return p
|
| 149 |
+
if (node,key) in seen:continue
|
| 150 |
+
seen.add((node,key))
|
| 151 |
+
for i,(a,b) in enumerate(edges):
|
| 152 |
+
if a==node and (key or i not in locks):front.append((b,key or b in keys,p+[i]))
|
| 153 |
+
return []
|
| 154 |
+
if name=='causal_abduction':
|
| 155 |
+
a,b,y=x;return next(u for u in (0,1) if ((u+a+b)%2)==y)
|
| 156 |
+
if name=='anomaly_cause':
|
| 157 |
+
u,a,b,vx,vy=x;return [key for key,actual,expected in [('x',vx,(u+a)%2),('y',vy,(vx+b)%2)] if actual!=expected]
|
| 158 |
+
if name in ('direct_cause','confounding','causal_intervention'):
|
| 159 |
+
u,a,b,(target,value)=x
|
| 160 |
+
vx=(u+a)%2
|
| 161 |
+
if name=='confounding':return [vx,(u+b)%2]
|
| 162 |
+
if target=='x':vx=value
|
| 163 |
+
return [vx,value if target=='y' else (vx+b)%2]
|
| 164 |
+
if name in ('competing_hypotheses','noisy_evidence','qual_bayes'):
|
| 165 |
+
priors,table,obs=x;score=[p*reduce(lambda a,b:a*b,(row[e] for e in obs),1) for p,row in zip(priors,table)]
|
| 166 |
+
return [i for i,s in enumerate(score) if all(s>=other for other in score)]
|
| 167 |
+
if name=='active_disambiguation':
|
| 168 |
+
columns=list(zip(*x));counts=[sum(a!=b for i,a in enumerate(c) for b in c[i+1:]) for c in columns]
|
| 169 |
+
return sorted(range(len(counts)),key=lambda i:(-counts[i],i))[0]
|
| 170 |
+
if name in ('rule_change','rule_switch'):
|
| 171 |
+
maps,events,start=x;out=[]
|
| 172 |
+
for k in range(1,len(events)+1):
|
| 173 |
+
mode=0;v=start
|
| 174 |
+
for op,arg in events[:k]:
|
| 175 |
+
if op=='switch':mode=arg
|
| 176 |
+
else:v=dict(enumerate(maps[mode]))[v]
|
| 177 |
+
out.append(v)
|
| 178 |
+
return out
|
| 179 |
+
if name=='goal_switch':
|
| 180 |
+
edges,start,goals=x;return [oracle('shortest_path',[edges,a,b]) for a,b in zip([start]+goals,goals)]
|
| 181 |
+
if name=='unknown_goal':return sorted(set(x[0])&set(x[1])-set(x[2]))
|
| 182 |
+
if name=='tool_affordance':return [i for i,tool in enumerate(x[0]) if set(x[1])<=set(tool)]
|
| 183 |
+
if name=='select_cell':return [list(divmod(i,len(x[0][0]))) for i,v in enumerate(sum(x[0],[])) if v==x[1]]
|
| 184 |
+
if name=='hidden_rule':
|
| 185 |
+
rules,demos,q=x;answers={(q+r)%7 for r in rules if all((b-a)%7==r for a,b in demos)}
|
| 186 |
+
assert len(answers)==1;return next(iter(answers))
|
| 187 |
+
if name=='explore_infer_exploit':
|
| 188 |
+
tables,obs,state,goal=x;accepted=[t for t in tables if [t[s][a] for s,a,_ in obs]==[z for _,_,z in obs]]
|
| 189 |
+
choices=[tuple(i for i,v in enumerate(t[state]) if v==goal) for t in accepted]
|
| 190 |
+
assert choices and len(set(choices))==1;return list(choices[0])
|
| 191 |
+
if name in ('sequence_composition','instruction_comp','modifier_comp','compositional_holdout','program_states'):
|
| 192 |
+
states=program_reference(*x);return states if name=='program_states' else states[-1]
|
| 193 |
+
if name in ('novel_primitive','rule_remap'):return dict(enumerate(x[0]))[x[1]]
|
| 194 |
+
if name=='representation_remap':return [dict(enumerate(x[0]))[v] for v in x[1]]
|
| 195 |
+
if name=='cross_domain':
|
| 196 |
+
phi,p,values=x;return [phi[p[phi.index(v)]] for v in values]
|
| 197 |
+
if name=='context_operator':return program_reference(x[0],['reverse' if len(x[1])%2 else 'rotate'],list(range(5)))[0]
|
| 198 |
+
if name=='meta_rule':return program_reference(x[0],[x[2][op] for op in x[1]],x[3])[-1]
|
| 199 |
+
if name=='mini_dsl':
|
| 200 |
+
pending=[(x,False)];values=[]
|
| 201 |
+
while pending:
|
| 202 |
+
node,visited=pending.pop();op,*args=node
|
| 203 |
+
if op=='literal':values.append(args[0])
|
| 204 |
+
elif not visited:
|
| 205 |
+
pending.append((node,True))
|
| 206 |
+
kids=[args[1]] if op=='repeat' else args
|
| 207 |
+
pending.extend((z,False) for z in reversed(kids))
|
| 208 |
+
elif op=='reverse':values.append(list(reversed(values.pop())))
|
| 209 |
+
elif op=='repeat':values.append(sum([values.pop()]*args[0],[]))
|
| 210 |
+
else:b,a=values.pop(),values.pop();values.append(a+b)
|
| 211 |
+
return values[0]
|
| 212 |
+
if name=='recursive_dsl':
|
| 213 |
+
rules,root,depth=x;word=[root]
|
| 214 |
+
for _ in range(depth):word=[v for t in word for v in rules.get(t,[t])]
|
| 215 |
+
return word
|
| 216 |
+
if name=='grammar_expansion':
|
| 217 |
+
rules,word,depth=x
|
| 218 |
+
def expand(t,n):return [t] if n==0 else sum([expand(z,n-1) for z in rules.get(t,[t])],[])
|
| 219 |
+
return sum([expand(t,depth) for t in word],[])
|
| 220 |
+
if name=='stable_partition':return sorted(x[0],key=lambda v:v not in x[1])
|
| 221 |
+
if name=='run_length_codec':
|
| 222 |
+
from itertools import groupby
|
| 223 |
+
return [[v,len(list(group))] for v,group in groupby(x)]
|
| 224 |
+
if name=='scope_shadowing':
|
| 225 |
+
merged={}
|
| 226 |
+
for frame in x[0]:merged.update(frame)
|
| 227 |
+
return merged.get(x[1],'missing')
|
| 228 |
+
if name=='relational_join':return [[a[0],a[1],b[1]] for a,b in product(*x) if a[0]==b[0]]
|
| 229 |
+
if name=='edit_script':
|
| 230 |
+
seq,edits=x;seq=seq[:]
|
| 231 |
+
for op,i,v in edits:
|
| 232 |
+
if op=='insert':seq=seq[:i]+[v]+seq[i:]
|
| 233 |
+
elif op=='delete':seq=seq[:i]+seq[i+1:]
|
| 234 |
+
else:seq=seq[:i]+[v]+seq[i+1:]
|
| 235 |
+
return seq
|
| 236 |
+
if name=='conditional_rewrite':
|
| 237 |
+
xs,a,b,guard=x;return [b if (prev,value)==(guard,a) else value for prev,value in zip([None]+xs,xs)]
|
| 238 |
+
if name=='span_reordering':
|
| 239 |
+
xs,a,b=x;return [xs[a+b-i-1] if a<=i<b else xs[i] for i in range(len(xs))]
|
| 240 |
+
if name=='nested_substitution':
|
| 241 |
+
tree,mapping=x
|
| 242 |
+
import copy
|
| 243 |
+
out=copy.deepcopy(tree);pending=[out]
|
| 244 |
+
while pending:
|
| 245 |
+
node=pending.pop()
|
| 246 |
+
for i,v in enumerate(node):
|
| 247 |
+
if isinstance(v,list):pending.append(v)
|
| 248 |
+
else:node[i]=mapping.get(v,v)
|
| 249 |
+
return out
|
| 250 |
+
raise KeyError(f'No independent oracle for {name}')
|
unified/ablations/unified-v1-blt-none-s42/source/cortex_source.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Paired symbolic/nonce tasks with bounded rejection and preserved answer grammar."""
|
| 2 |
+
import random,hashlib,json,string
|
| 3 |
+
import cortex_curriculum_v16 as kernel
|
| 4 |
+
from cortex_data import answer_episode,composition_episode
|
| 5 |
+
from records import BOS,EOS,ids,validate,Oversize
|
| 6 |
+
CATALOG=kernel._v11_family_catalog(max(kernel.STAGES),False)
|
| 7 |
+
FAMILIES=[e['name'] for e in CATALOG]+['compositional_execution']
|
| 8 |
+
|
| 9 |
+
def seed_for(namespace,index):return int.from_bytes(hashlib.blake2b(f'{namespace}:{index}'.encode(),digest_size=8).digest(),'big')
|
| 10 |
+
|
| 11 |
+
class WordSurface:
|
| 12 |
+
def __init__(self,seed):
|
| 13 |
+
rng=random.Random(seed);self.codes=rng.sample([a+b for a in string.ascii_lowercase for b in string.ascii_lowercase],256)
|
| 14 |
+
self.inverse={v:i for i,v in enumerate(self.codes)}
|
| 15 |
+
def encode(self,raw):return ' '.join(self.codes[b] for b in raw)
|
| 16 |
+
def decode(self,text):return [self.inverse[s] for s in text.split()]
|
| 17 |
+
|
| 18 |
+
def render(ep,family,seed,wordlike,limit):
|
| 19 |
+
p=ep.tokens[1:ep.prompt_len];a=ep.tokens[ep.prompt_len:-1]
|
| 20 |
+
if not a or any(not 0<=t<256 for t in p+a):raise ValueError('Invalid kernel payload')
|
| 21 |
+
header='Cortex: infer the demonstrated rule and complete the query.\n'
|
| 22 |
+
if hasattr(ep,'audit'):
|
| 23 |
+
au=ep.audit
|
| 24 |
+
# Previously string operations (e.g. left/right/pop) had ungrounded random
|
| 25 |
+
# labels and sometimes appeared for the first time in the query. Ground
|
| 26 |
+
# their interpretation explicitly without printing any query solution.
|
| 27 |
+
bindings={f'{v:02x}':key[1] for key,v in au['symbols'].items() if key[0]!='int'}
|
| 28 |
+
start,close,sep,end,unit,neg=au['markers']
|
| 29 |
+
header+=f"Mechanism: {au['mechanism']}. Indices are zero-based; graph ties use increasing numeric order.\n"
|
| 30 |
+
header+='Byte bindings (hex): '+json.dumps(bindings,ensure_ascii=True,separators=(',',':'))+'\n'
|
| 31 |
+
header+=f'Containers {start:02x}/{close:02x}; input/output {sep:02x}; demo end {end:02x}. Numeric values are grounded by unary examples.\n'
|
| 32 |
+
# Every referenced binding byte is present in the binary context too.
|
| 33 |
+
p=list(bindings_byte for bindings_byte in au['symbols'].values() if bindings_byte not in p)+p
|
| 34 |
+
if wordlike:
|
| 35 |
+
surface=WordSurface(seed)
|
| 36 |
+
# Map only legend-referenced roles; the rest are learned by matching demos.
|
| 37 |
+
if hasattr(ep,'audit'):
|
| 38 |
+
used=ep.audit['symbols'].values();header+='Hex-to-nonce: '+' '.join(f'{b:02x}={surface.codes[b]}' for b in used)+'\n'
|
| 39 |
+
header+='Container codes: '+' '.join(f'{b:02x}={surface.codes[b]}' for b in ep.audit['markers'])+'\n'
|
| 40 |
+
prefix=ids(header+'Nonce examples:\n'+surface.encode(p)+'\nAnswer:\n')
|
| 41 |
+
answer=ids(surface.encode(a))
|
| 42 |
+
else:
|
| 43 |
+
prefix=ids(header+'Byte examples:\n')+p;answer=a
|
| 44 |
+
r=dict(ids=[BOS]+prefix+answer+[EOS],weights=[0]*(len(prefix)+1)+[1]*(len(answer)+1),
|
| 45 |
+
source='cortex',prompt_len=len(prefix)+1,meta={'family':family,'surface':'nonce' if wordlike else 'symbolic'})
|
| 46 |
+
if len(r['ids'])>limit+1:raise Oversize(f'{family}: expanded {len(r["ids"])}')
|
| 47 |
+
return validate(r,limit)
|
| 48 |
+
|
| 49 |
+
def episode_pair(index,namespace='cortex74/train/v2',family=None,split='train',limit=1024):
|
| 50 |
+
pair=index//2;cycle,offset=divmod(pair,len(FAMILIES));order=list(range(len(FAMILIES)))
|
| 51 |
+
random.Random(seed_for(namespace+'/order',cycle)).shuffle(order)
|
| 52 |
+
selected=order[offset] if family is None else FAMILIES.index(family)
|
| 53 |
+
name=FAMILIES[selected]
|
| 54 |
+
for attempt in range(96):
|
| 55 |
+
seed=seed_for(namespace,(pair,attempt));difficulty=seed%3
|
| 56 |
+
try:
|
| 57 |
+
if name=='compositional_execution':ep=composition_episode(kernel,seed,2048,split,difficulty)
|
| 58 |
+
else:ep=answer_episode(kernel,CATALOG[selected],difficulty,2048,seed)
|
| 59 |
+
# Preserve the alternating nonce surface whenever it fits. At the 1K
|
| 60 |
+
# model context, a few nonce expansions are intrinsically >1K even when
|
| 61 |
+
# the underlying symbolic task fits; those records fall back to the
|
| 62 |
+
# complete symbolic view rather than truncating or dropping the family.
|
| 63 |
+
want_wordlike=bool(index%2)
|
| 64 |
+
try:
|
| 65 |
+
result=render(ep,name,seed,want_wordlike,limit)
|
| 66 |
+
except Oversize:
|
| 67 |
+
if not want_wordlike:raise
|
| 68 |
+
result=render(ep,name,seed,False,limit)
|
| 69 |
+
result['meta']['surface_fallback']='symbolic_due_to_context'
|
| 70 |
+
result['meta']['split']=split
|
| 71 |
+
return result
|
| 72 |
+
except Oversize:continue
|
| 73 |
+
raise RuntimeError(f'{name}: no complete paired example fits {limit} bytes')
|
| 74 |
+
|
| 75 |
+
class CortexSource:
|
| 76 |
+
def __init__(self,state=None,limit=1024):self.s=dict(state or {'index':0});self.limit=limit
|
| 77 |
+
def record(self):
|
| 78 |
+
r=episode_pair(self.s['index'],limit=self.limit);self.s['index']+=1;return r
|
| 79 |
+
def state_dict(self):return dict(self.s)
|
| 80 |
+
def validation(self):
|
| 81 |
+
return [episode_pair(2*i+j,'cortex74/validation/v2',f,split,self.limit)
|
| 82 |
+
for i,(f,split) in enumerate([(FAMILIES[k],'train') for k in range(0,len(FAMILIES)-1,17)]+
|
| 83 |
+
[('compositional_execution',s) for s in ['train','composition','depth','length']]) for j in (0,1)]
|
unified/ablations/unified-v1-blt-none-s42/source/cortex_tasks.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit executable tasks. Shared encoding never substitutes one task for another."""
|
| 2 |
+
from collections import deque, Counter
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from itertools import product
|
| 5 |
+
import random
|
| 6 |
+
|
| 7 |
+
REVISION = 'cortex-explicit-mechanisms-v1'
|
| 8 |
+
|
| 9 |
+
# Operations deliberately have separate semantics; unknown operations are errors.
|
| 10 |
+
def sequence_execute(xs, program, mapping):
|
| 11 |
+
xs=list(xs);trace=[]
|
| 12 |
+
for op in program:
|
| 13 |
+
if op=='reverse': xs=list(reversed(xs))
|
| 14 |
+
elif op=='rotate': xs=xs[1:]+xs[:1]
|
| 15 |
+
elif op=='swap':
|
| 16 |
+
for i in range(0,len(xs)-1,2):xs[i],xs[i+1]=xs[i+1],xs[i]
|
| 17 |
+
elif op=='map':xs=[mapping[x] for x in xs]
|
| 18 |
+
elif op=='dedup':xs=list(dict.fromkeys(xs))
|
| 19 |
+
elif op=='repeat':xs=[x for x in xs for _ in range(2)]
|
| 20 |
+
elif op=='evens':xs=xs[::2]
|
| 21 |
+
else:raise ValueError(op)
|
| 22 |
+
trace.append(xs[:])
|
| 23 |
+
return trace
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def traverse(edges,start,depth_first=False):
|
| 27 |
+
adj={}
|
| 28 |
+
for a,b in edges:adj.setdefault(a,[]).append(b)
|
| 29 |
+
seen=set();out=[];frontier=[start]
|
| 30 |
+
while frontier:
|
| 31 |
+
x=frontier.pop() if depth_first else frontier.pop(0)
|
| 32 |
+
if x in seen:continue
|
| 33 |
+
seen.add(x);out.append(x)
|
| 34 |
+
children=sorted(adj.get(x,[]))
|
| 35 |
+
frontier.extend(reversed(children) if depth_first else children)
|
| 36 |
+
return out
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def shortest(edges,start,goal):
|
| 40 |
+
q=deque([(start,[start])]);seen={start}
|
| 41 |
+
while q:
|
| 42 |
+
x,path=q.popleft()
|
| 43 |
+
if x==goal:return path
|
| 44 |
+
for a,b in sorted(edges):
|
| 45 |
+
if a==x and b not in seen:seen.add(b);q.append((b,path+[b]))
|
| 46 |
+
return []
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def selection_trace(xs):
|
| 50 |
+
xs=xs[:];out=[]
|
| 51 |
+
for i in range(len(xs)-1):
|
| 52 |
+
j=min(range(i,len(xs)),key=xs.__getitem__);xs[i],xs[j]=xs[j],xs[i];out.append(xs[:])
|
| 53 |
+
return out
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def insertion_trace(xs):
|
| 57 |
+
xs=xs[:];out=[]
|
| 58 |
+
for i in range(1,len(xs)):
|
| 59 |
+
x=xs[i];j=i
|
| 60 |
+
while j and xs[j-1]>x:xs[j]=xs[j-1];j-=1
|
| 61 |
+
xs[j]=x;out.append(xs[:])
|
| 62 |
+
return out
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def solve(name,x):
|
| 66 |
+
"""Production semantics on structured latent inputs; no family-name fallback."""
|
| 67 |
+
if name=='selection_sort':return selection_trace(x)
|
| 68 |
+
if name=='insertion_sort':return insertion_trace(x)
|
| 69 |
+
if name=='minmax_scan':return [[min(x[:i]),max(x[:i])] for i in range(1,len(x)+1)]
|
| 70 |
+
if name=='linear_search':
|
| 71 |
+
arr,target=x;return [i for i,v in enumerate(arr) if v==target][:1] or [-1]
|
| 72 |
+
if name=='binary_search':
|
| 73 |
+
arr,target=x;lo,hi=0,len(arr)-1;visited=[]
|
| 74 |
+
while lo<=hi:
|
| 75 |
+
m=(lo+hi)//2;visited.append(m)
|
| 76 |
+
if arr[m]==target:break
|
| 77 |
+
if arr[m]<target:lo=m+1
|
| 78 |
+
else:hi=m-1
|
| 79 |
+
return [visited, m if lo<=hi else -1]
|
| 80 |
+
if name=='string_match':
|
| 81 |
+
text,pat=x;return [i for i in range(len(text)-len(pat)+1) if text[i:i+len(pat)]==pat]
|
| 82 |
+
if name in ('bfs_order','dfs_order'):return traverse(x[0],x[1],name=='dfs_order')
|
| 83 |
+
if name=='topo_sort':
|
| 84 |
+
nodes,edges=x;out=[]
|
| 85 |
+
while len(out)<len(nodes):
|
| 86 |
+
ready=[n for n in nodes if n not in out and all(a in out for a,b in edges if b==n)]
|
| 87 |
+
if not ready:raise ValueError('Cycle in topological task')
|
| 88 |
+
out.append(min(ready))
|
| 89 |
+
return out
|
| 90 |
+
if name=='shortest_path':return shortest(*x)
|
| 91 |
+
if name=='union_find':
|
| 92 |
+
edges,a,b=x;return b in traverse(edges+[[v,u] for u,v in edges],a)
|
| 93 |
+
if name=='interval_overlap':
|
| 94 |
+
a,b,c,d=x;return max(a,c)<=min(b,d)
|
| 95 |
+
if name=='bracket_balance':
|
| 96 |
+
depth=0
|
| 97 |
+
for op in x:
|
| 98 |
+
depth+=1 if op=='open' else -1
|
| 99 |
+
if depth<0:return False
|
| 100 |
+
return depth==0
|
| 101 |
+
if name=='bracket_depth':
|
| 102 |
+
d=best=0
|
| 103 |
+
for op in x:d+=1 if op=='open' else -1;best=max(best,d)
|
| 104 |
+
return best
|
| 105 |
+
if name in ('queue','deque'):
|
| 106 |
+
q=deque();out=[]
|
| 107 |
+
for op,*args in x:
|
| 108 |
+
if op=='right':q.append(args[0])
|
| 109 |
+
elif op=='left':q.appendleft(args[0])
|
| 110 |
+
elif op=='pop_left':out.append(q.popleft() if q else 'empty')
|
| 111 |
+
elif op=='pop_right':out.append(q.pop() if q else 'empty')
|
| 112 |
+
else:raise ValueError(op)
|
| 113 |
+
return [out,list(q)]
|
| 114 |
+
if name in ('kv_lookup','role_filler','assoc_distractors','nested_lookup','mutable_dictionary','delayed_recall'):
|
| 115 |
+
records,query=x;table={}
|
| 116 |
+
for k,v in records:table[k]=v
|
| 117 |
+
value=query[0]
|
| 118 |
+
for _ in range(query[1]):value=table[value]
|
| 119 |
+
return value
|
| 120 |
+
if name in ('register_swap','register_update','multi_register','program_trace'):
|
| 121 |
+
state,program=x;state=state[:];trace=[]
|
| 122 |
+
for op,a,b in program:
|
| 123 |
+
if op=='set':state[a]=b
|
| 124 |
+
elif op=='swap':state[a],state[b]=state[b],state[a]
|
| 125 |
+
elif op=='copy':state[a]=state[b]
|
| 126 |
+
else:raise ValueError(op)
|
| 127 |
+
trace.append(state[:])
|
| 128 |
+
return trace if name=='program_trace' else state
|
| 129 |
+
if name=='conditional_branch':
|
| 130 |
+
pred,a,b=x;return a if pred else b
|
| 131 |
+
if name=='loop_counter':return list(range(x,-1,-1))
|
| 132 |
+
if name in ('stack_expr','prefix_expr','postfix_expr'):
|
| 133 |
+
if name=='prefix_expr':
|
| 134 |
+
it=iter(x)
|
| 135 |
+
def parse():
|
| 136 |
+
t=next(it)
|
| 137 |
+
if isinstance(t,int):return t
|
| 138 |
+
a,b=parse(),parse()
|
| 139 |
+
return a+b if t=='add' else a*b
|
| 140 |
+
return parse()
|
| 141 |
+
stack=[]
|
| 142 |
+
for t in x:
|
| 143 |
+
if isinstance(t,int):stack.append(t)
|
| 144 |
+
elif t=='push':continue
|
| 145 |
+
else:
|
| 146 |
+
b,a=stack.pop(),stack.pop();stack.append(a+b if t=='add' else a*b)
|
| 147 |
+
if len(stack)!=1:raise ValueError('Bad expression')
|
| 148 |
+
return stack[0]
|
| 149 |
+
if name=='relative_coords':
|
| 150 |
+
n,(a,b),moves=x
|
| 151 |
+
for dx,dy in moves:a=max(0,min(n-1,a+dx));b=max(0,min(n-1,b+dy))
|
| 152 |
+
return [a,b]
|
| 153 |
+
if name=='toroidal':
|
| 154 |
+
n,p,moves=x
|
| 155 |
+
for delta in moves:p=(p+delta)%n
|
| 156 |
+
return p
|
| 157 |
+
if name=='collision':
|
| 158 |
+
a,va,b,vb,steps=x;trace=[]
|
| 159 |
+
for _ in range(steps):
|
| 160 |
+
na,nb=a+va,b+vb
|
| 161 |
+
a,b=min(na,nb),max(na,nb)
|
| 162 |
+
if na>=nb:va,vb=vb,va
|
| 163 |
+
trace.append([a,b,va,vb])
|
| 164 |
+
return trace
|
| 165 |
+
if name=='falling':
|
| 166 |
+
# Downward simultaneous settling: each column conserves occupied cells.
|
| 167 |
+
h=len(x);return [[1 if r>=h-sum(row[c] for row in x) else 0 for c in range(len(x[0]))] for r in range(h)]
|
| 168 |
+
if name=='obstacle_motion':
|
| 169 |
+
n,p,blocked,moves=x;visited=[p]
|
| 170 |
+
for delta in moves:
|
| 171 |
+
q=p+delta
|
| 172 |
+
if 0<=q<n and q not in blocked:p=q
|
| 173 |
+
visited.append(p)
|
| 174 |
+
return visited
|
| 175 |
+
if name=='line_of_sight':
|
| 176 |
+
a,b,blocked=x;return not any(min(a,b)<v<max(a,b) for v in blocked)
|
| 177 |
+
if name=='containment':
|
| 178 |
+
outer,inner=x;return outer[0]<=inner[0] and inner[1]<=outer[1]
|
| 179 |
+
if name=='partial_map':
|
| 180 |
+
observations,start,goal=x
|
| 181 |
+
edges=sorted({tuple(edge) for observation in observations for edge in observation})
|
| 182 |
+
return shortest(edges,start,goal)
|
| 183 |
+
if name=='dynamic_plan':
|
| 184 |
+
edges,changes,start,goal=x; edges=[e[:] for e in edges]
|
| 185 |
+
for op,a,b in changes:
|
| 186 |
+
if op=='remove':edges=[e for e in edges if e!=[a,b]]
|
| 187 |
+
elif op=='add' and [a,b] not in edges:edges.append([a,b])
|
| 188 |
+
return shortest(edges,start,goal)
|
| 189 |
+
if name in ('path_keys','door_key','sparse_nav','irreversible_action'):
|
| 190 |
+
# Real search in a directed graph; keys change which edges are traversable.
|
| 191 |
+
edges,start,goal,key_nodes,locks=x;q=deque([(start,0,[])]);seen={(start,0)}
|
| 192 |
+
while q:
|
| 193 |
+
node,key,path=q.popleft();key=key or int(node in key_nodes)
|
| 194 |
+
if node==goal:return path
|
| 195 |
+
for action,(a,b) in enumerate(edges):
|
| 196 |
+
if a!=node or (action in locks and not key):continue
|
| 197 |
+
state=(b,key)
|
| 198 |
+
if state not in seen:seen.add(state);q.append((b,key,path+[action]))
|
| 199 |
+
return []
|
| 200 |
+
if name=='causal_abduction':
|
| 201 |
+
a,b,y=x;return a^b^y
|
| 202 |
+
if name=='anomaly_cause':
|
| 203 |
+
u,a,b,observed_x,observed_y=x
|
| 204 |
+
return (['x'] if observed_x!=(u^a) else [])+(['y'] if observed_y!=(observed_x^b) else [])
|
| 205 |
+
if name in ('direct_cause','causal_intervention','confounding'):
|
| 206 |
+
# X=U xor A; Y=X xor B, except explicit interventions override equations.
|
| 207 |
+
u,a,b,intervention=x
|
| 208 |
+
xv=u^a;yv=xv^b
|
| 209 |
+
if intervention[0]=='x':xv=intervention[1];yv=xv^b
|
| 210 |
+
elif intervention[0]=='y':yv=intervention[1]
|
| 211 |
+
if name=='confounding':return [u^a,u^b] # fork U -> X and U -> Y
|
| 212 |
+
return [xv,yv]
|
| 213 |
+
if name=='abduce_noise':
|
| 214 |
+
a,b,observed_y=x;return observed_y^b^a
|
| 215 |
+
if name in ('competing_hypotheses','noisy_evidence','qual_bayes'):
|
| 216 |
+
# Integer likelihoods: exact posterior ranking, with priors and repeated evidence.
|
| 217 |
+
priors,likelihood,observations=x;score=priors[:]
|
| 218 |
+
for obs in observations:score=[s*row[obs] for s,row in zip(score,likelihood)]
|
| 219 |
+
best=max(score);return [i for i,v in enumerate(score) if v==best]
|
| 220 |
+
if name=='active_disambiguation':
|
| 221 |
+
# Pick the smallest test separating the largest number of hypothesis pairs.
|
| 222 |
+
table=x;counts=[sum(row[i]!=other[i] for j,row in enumerate(table) for other in table[j+1:]) for i in range(len(table[0]))]
|
| 223 |
+
return counts.index(max(counts))
|
| 224 |
+
if name in ('rule_change','rule_switch'):
|
| 225 |
+
maps,events,start=x;regime=0;state=start;out=[]
|
| 226 |
+
for op,arg in events:
|
| 227 |
+
if op=='switch':regime=arg
|
| 228 |
+
elif op=='step':state=maps[regime][state]
|
| 229 |
+
else:raise ValueError(op)
|
| 230 |
+
out.append(state)
|
| 231 |
+
return out
|
| 232 |
+
if name=='goal_switch':
|
| 233 |
+
edges,start,goals=x;out=[]
|
| 234 |
+
for goal in goals:
|
| 235 |
+
path=shortest(edges,start,goal);out.append(path);start=goal
|
| 236 |
+
return out
|
| 237 |
+
if name=='unknown_goal':
|
| 238 |
+
candidates,rewarded,rejected=x;return [n for n in candidates if n in rewarded and n not in rejected]
|
| 239 |
+
if name=='tool_affordance':
|
| 240 |
+
tools,requirements=x;return [i for i,t in enumerate(tools) if all(v in t for v in requirements)]
|
| 241 |
+
if name=='select_cell':
|
| 242 |
+
grid,color=x;return [[r,c] for r,row in enumerate(grid) for c,v in enumerate(row) if v==color]
|
| 243 |
+
if name=='hidden_rule':
|
| 244 |
+
rules,demos,query=x;valid=[r for r in rules if all((a+r)%7==b for a,b in demos)]
|
| 245 |
+
out={(query+r)%7 for r in valid}
|
| 246 |
+
if len(out)!=1:raise ValueError('Unidentifiable hidden rule')
|
| 247 |
+
return out.pop()
|
| 248 |
+
if name=='explore_infer_exploit':
|
| 249 |
+
# Hidden environment version; observations identify it, then pick rewarding action.
|
| 250 |
+
tables,observations,state,goal=x
|
| 251 |
+
valid=[t for t in tables if all(t[s][a]==z for s,a,z in observations)]
|
| 252 |
+
answers={tuple(a for a,z in enumerate(t[state]) if z==goal) for t in valid}
|
| 253 |
+
if len(answers)!=1:raise ValueError('Unidentifiable action')
|
| 254 |
+
return list(answers.pop())
|
| 255 |
+
if name in ('sequence_composition','instruction_comp','modifier_comp','compositional_holdout'):
|
| 256 |
+
xs,program,mapping=x;return sequence_execute(xs,program,mapping)[-1]
|
| 257 |
+
if name=='program_states':
|
| 258 |
+
xs,program,mapping=x;return sequence_execute(xs,program,mapping)
|
| 259 |
+
if name in ('novel_primitive','rule_remap'):
|
| 260 |
+
table,xv=x;return table[xv]
|
| 261 |
+
if name=='representation_remap':
|
| 262 |
+
mapping,values=x;return [mapping[v] for v in values]
|
| 263 |
+
if name=='cross_domain':
|
| 264 |
+
phi,perm,values=x;inv={v:i for i,v in enumerate(phi)};return [phi[perm[inv[v]]] for v in values]
|
| 265 |
+
if name=='context_operator':
|
| 266 |
+
xs,context=x;return list(reversed(xs)) if len(context)%2 else xs[1:]+xs[:1]
|
| 267 |
+
if name=='meta_rule':
|
| 268 |
+
xs,program,remap,perm=x;return sequence_execute(xs,[remap[o] for o in program],perm)[-1]
|
| 269 |
+
if name=='mini_dsl':
|
| 270 |
+
def run(node):
|
| 271 |
+
op,*args=node
|
| 272 |
+
if op=='literal':return args[0]
|
| 273 |
+
if op=='reverse':return run(args[0])[::-1]
|
| 274 |
+
if op=='concat':return run(args[0])+run(args[1])
|
| 275 |
+
if op=='repeat':return run(args[1])*args[0]
|
| 276 |
+
raise ValueError(op)
|
| 277 |
+
return run(x)
|
| 278 |
+
if name=='grammar_expansion':
|
| 279 |
+
rules,word,steps=x
|
| 280 |
+
for _ in range(steps):word=sum((rules.get(t,[t]) for t in word),[])
|
| 281 |
+
return word
|
| 282 |
+
if name=='recursive_dsl':
|
| 283 |
+
rules,root,depth=x
|
| 284 |
+
def expand(t,d):return [t] if not d or t not in rules else sum((expand(z,d-1) for z in rules[t]),[])
|
| 285 |
+
return expand(root,depth)
|
| 286 |
+
if name=='stable_partition':
|
| 287 |
+
xs,selected=x;return [v for v in xs if v in selected]+[v for v in xs if v not in selected]
|
| 288 |
+
if name=='run_length_codec':
|
| 289 |
+
out=[]
|
| 290 |
+
for v in x:
|
| 291 |
+
if out and out[-1][0]==v:out[-1][1]+=1
|
| 292 |
+
else:out.append([v,1])
|
| 293 |
+
return out
|
| 294 |
+
if name=='scope_shadowing':
|
| 295 |
+
frames,key=x
|
| 296 |
+
for frame in reversed(frames):
|
| 297 |
+
if key in frame:return frame[key]
|
| 298 |
+
return 'missing'
|
| 299 |
+
if name=='relational_join':
|
| 300 |
+
left,right=x;return [[k,a,b] for k,a in left for j,b in right if k==j]
|
| 301 |
+
if name=='edit_script':
|
| 302 |
+
xs,edits=x;xs=xs[:]
|
| 303 |
+
for op,i,v in edits:
|
| 304 |
+
if op=='insert':xs.insert(i,v)
|
| 305 |
+
elif op=='replace':xs[i]=v
|
| 306 |
+
elif op=='delete':xs.pop(i)
|
| 307 |
+
else:raise ValueError(op)
|
| 308 |
+
return xs
|
| 309 |
+
if name=='conditional_rewrite':
|
| 310 |
+
xs,a,b,guard=x;return [b if v==a and i and xs[i-1]==guard else v for i,v in enumerate(xs)]
|
| 311 |
+
if name=='span_reordering':
|
| 312 |
+
xs,start,end=x;return xs[:start]+list(reversed(xs[start:end]))+xs[end:]
|
| 313 |
+
if name=='nested_substitution':
|
| 314 |
+
tree,mapping=x
|
| 315 |
+
def walk(v):return [walk(z) for z in v] if isinstance(v,list) else mapping.get(v,v)
|
| 316 |
+
return walk(tree)
|
| 317 |
+
raise KeyError(f'No semantics for {name}')
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
ALGORITHM_NAMES = 'selection_sort insertion_sort minmax_scan linear_search binary_search string_match bfs_order dfs_order topo_sort union_find shortest_path interval_overlap'.split()
|
| 321 |
+
MEMORY_NAMES = 'kv_lookup role_filler assoc_distractors nested_lookup mutable_dictionary queue deque delayed_recall register_swap register_update conditional_branch loop_counter multi_register program_trace stack_expr prefix_expr postfix_expr bracket_balance bracket_depth'.split()
|
| 322 |
+
WORLD_NAMES = 'relative_coords line_of_sight containment collision toroidal falling obstacle_motion path_keys dynamic_plan direct_cause confounding competing_hypotheses noisy_evidence active_disambiguation qual_bayes causal_intervention causal_abduction rule_change anomaly_cause'.split()
|
| 323 |
+
META_NAMES = 'novel_primitive modifier_comp mini_dsl cross_domain context_operator meta_rule representation_remap compositional_holdout recursive_dsl instruction_comp rule_remap'.split()
|
| 324 |
+
INTERACTIVE_NAMES = 'hidden_rule unknown_goal door_key tool_affordance sparse_nav partial_map irreversible_action explore_infer_exploit rule_switch goal_switch select_cell'.split()
|
| 325 |
+
NEW_NAMES = 'sequence_composition program_states grammar_expansion stable_partition run_length_codec scope_shadowing relational_join edit_script conditional_rewrite span_reordering nested_substitution'.split()
|
| 326 |
+
EXPLICIT_NAMES = ALGORITHM_NAMES+MEMORY_NAMES+WORLD_NAMES+META_NAMES+INTERACTIVE_NAMES+NEW_NAMES
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def sample_input(name,rng,difficulty=1,split='train'):
|
| 330 |
+
n=rng.randint(3,5) if split!='length' else rng.randint(8,10)
|
| 331 |
+
arr=lambda:[rng.randrange(5) for _ in range(n)]
|
| 332 |
+
order=rng.sample(range(6),6)
|
| 333 |
+
edges=[[order[i],order[i+1]] for i in range(5)]
|
| 334 |
+
edges += [[order[i],order[j]] for i in range(4) for j in range(i+2,6) if rng.random()<.35]
|
| 335 |
+
if name in ('selection_sort','insertion_sort','minmax_scan','run_length_codec'):return arr()
|
| 336 |
+
if name in ('linear_search','binary_search'):
|
| 337 |
+
xs=sorted(set(arr())) if name=='binary_search' else arr();return [xs,rng.choice(xs+[7])]
|
| 338 |
+
if name=='string_match':
|
| 339 |
+
xs=arr()+arr();i=rng.randrange(len(xs)-2);return [xs,xs[i:i+2]]
|
| 340 |
+
if name in ('bfs_order','dfs_order'):return [edges,rng.choice(order[:3])]
|
| 341 |
+
if name=='topo_sort':return [list(range(6)),[e for e in edges if rng.random()<.8]]
|
| 342 |
+
if name=='shortest_path':return [edges,order[0],rng.choice(order[2:])]
|
| 343 |
+
if name=='union_find':return [[[0,1],[1,2],[3,4]],rng.randrange(5),rng.randrange(5)]
|
| 344 |
+
if name=='interval_overlap':
|
| 345 |
+
return sorted(rng.sample(range(7),2))+sorted(rng.sample(range(7),2))
|
| 346 |
+
if name in ('bracket_balance','bracket_depth'):
|
| 347 |
+
depth=rng.randint(1,4 if split!='depth' else 7);seq=['open']*depth+['close']*depth
|
| 348 |
+
if name=='bracket_balance' and rng.random()<.5:seq=rng.choice([seq[:-1],['close']+seq])
|
| 349 |
+
return seq
|
| 350 |
+
if name in ('queue','deque'):
|
| 351 |
+
ops=[['right',rng.randrange(5)] for _ in range(n)]
|
| 352 |
+
if name=='deque':ops.insert(1,['left',rng.randrange(5)]);ops.append(['pop_right'])
|
| 353 |
+
return ops+[['pop_left'] for _ in range(rng.randint(1,n+1))]
|
| 354 |
+
if name in ('kv_lookup','role_filler','assoc_distractors','nested_lookup','mutable_dictionary','delayed_recall'):
|
| 355 |
+
keys=['a','b','c'];records=[[k,rng.randrange(5)] for k in keys];key=rng.choice(keys)
|
| 356 |
+
if name=='role_filler':records=[['agent',records[0][1]],['object',records[1][1]],['location',records[2][1]]];key=rng.choice(['agent','object','location'])
|
| 357 |
+
if name=='mutable_dictionary':records.append([key,rng.randrange(5)])
|
| 358 |
+
if name in ('assoc_distractors','delayed_recall'):records.extend([[f'noise{i}',rng.randrange(5)] for i in range(n)])
|
| 359 |
+
if name=='nested_lookup':
|
| 360 |
+
records=[['a','b'],['b','c'],['c',rng.randrange(5)]];return [records,['a',3]]
|
| 361 |
+
return [records,[key,1]]
|
| 362 |
+
if name in ('register_swap','register_update','multi_register','program_trace'):
|
| 363 |
+
ops=['swap'] if name=='register_swap' else ['set'] if name=='register_update' else ['set','copy','swap']
|
| 364 |
+
program=[[rng.choice(ops),rng.randrange(3),rng.randrange(3)] for _ in range(n)]
|
| 365 |
+
return [arr()[:3],program]
|
| 366 |
+
if name=='conditional_branch':return [bool(rng.randrange(2)),arr(),arr()]
|
| 367 |
+
if name=='loop_counter':return rng.randint(2,7)
|
| 368 |
+
if name in ('stack_expr','prefix_expr','postfix_expr'):
|
| 369 |
+
a,b,c=[rng.randint(0,2) for _ in range(3)];op=rng.choice(['add','mul'])
|
| 370 |
+
if name=='prefix_expr':return [op,'add',a,b,c]
|
| 371 |
+
if name=='postfix_expr':return [a,b,'add',c,op]
|
| 372 |
+
return ['push',a,'push',b,'add','push',c,op]
|
| 373 |
+
if name=='relative_coords':return [7,[rng.randrange(7),rng.randrange(7)],[rng.choice([[1,0],[-1,0],[0,1],[0,-1]]) for _ in range(n)]]
|
| 374 |
+
if name=='toroidal':return [5,rng.randrange(5),[rng.choice([-1,1]) for _ in range(n)]]
|
| 375 |
+
if name=='collision':return [0,1,rng.choice([2,3,4]),-1,rng.randint(2,3)]
|
| 376 |
+
if name=='falling':return [[rng.randrange(2) for _ in range(3)] for _ in range(4)]
|
| 377 |
+
if name=='obstacle_motion':return [7,2,[4],[rng.choice([-1,1]) for _ in range(n)]]
|
| 378 |
+
if name=='line_of_sight':return [0,6,rng.sample(range(8),rng.randrange(4))]
|
| 379 |
+
if name=='containment':return [sorted(rng.sample(range(8),2)),sorted(rng.sample(range(8),2))]
|
| 380 |
+
if name=='partial_map':
|
| 381 |
+
shuffled=edges[:];rng.shuffle(shuffled)
|
| 382 |
+
observations=[shuffled[::2],shuffled[1::2]]
|
| 383 |
+
# Neither observation alone is guaranteed to expose the full route.
|
| 384 |
+
return [observations,order[0],order[-1]]
|
| 385 |
+
if name=='dynamic_plan':
|
| 386 |
+
# A formerly shortest direct route disappears; another edge may appear.
|
| 387 |
+
start,goal=order[0],order[-1]
|
| 388 |
+
base=edges+[[start,goal]]
|
| 389 |
+
changes=[['remove',start,goal]]
|
| 390 |
+
if rng.random()<.5:changes.append(['add',order[1],goal])
|
| 391 |
+
return [base,changes,start,goal]
|
| 392 |
+
if name in ('path_keys','door_key'):
|
| 393 |
+
nodes=rng.sample(range(8),8);start,key,trap=nodes[:3]
|
| 394 |
+
path=[start]+nodes[3:3+rng.randint(2,4)]
|
| 395 |
+
road=[[a,b] for a,b in zip(path,path[1:])]
|
| 396 |
+
gate=road[rng.randrange(len(road))]
|
| 397 |
+
graph=road+[[start,key],[key,start],[start,trap]]
|
| 398 |
+
rng.shuffle(graph)
|
| 399 |
+
return [graph,start,path[-1],[key],[graph.index(gate)]]
|
| 400 |
+
if name=='irreversible_action':
|
| 401 |
+
nodes=rng.sample(range(7),7);start,trap=nodes[:2]
|
| 402 |
+
route=[start]+nodes[2:rng.randint(5,7)]
|
| 403 |
+
graph=[[a,b] for a,b in zip(route,route[1:])]+[[start,trap],[trap,trap]]
|
| 404 |
+
rng.shuffle(graph)
|
| 405 |
+
return [graph,start,route[-1],[],[]]
|
| 406 |
+
if name=='sparse_nav':
|
| 407 |
+
rng.shuffle(edges)
|
| 408 |
+
return [edges,order[0],rng.choice(order[2:]),[],[]]
|
| 409 |
+
if name=='causal_abduction':return [rng.randrange(2),rng.randrange(2),rng.randrange(2)]
|
| 410 |
+
if name=='anomaly_cause':
|
| 411 |
+
u,a,b=[rng.randrange(2) for _ in range(3)];vx=u^a;vy=vx^b
|
| 412 |
+
if rng.random()<.5:vx^=1;vy=vx^b
|
| 413 |
+
else:vy^=1
|
| 414 |
+
return [u,a,b,vx,vy]
|
| 415 |
+
if name in ('direct_cause','causal_intervention','confounding'):
|
| 416 |
+
iv=['none',0] if name in ('direct_cause','confounding') else [rng.choice(['x','y']),rng.randrange(2)]
|
| 417 |
+
return [rng.randrange(2),rng.randrange(2),rng.randrange(2),iv]
|
| 418 |
+
if name in ('competing_hypotheses','noisy_evidence','qual_bayes'):
|
| 419 |
+
prior=[1,1] if name=='competing_hypotheses' else [rng.randint(1,3),rng.randint(1,3)]
|
| 420 |
+
return [prior,[[3,1],[1,3]],[rng.randrange(2) for _ in range(n if name!='qual_bayes' else 1)]]
|
| 421 |
+
if name=='active_disambiguation':return [[rng.randrange(2) for _ in range(4)] for _ in range(3)]
|
| 422 |
+
if name in ('rule_change','rule_switch'):
|
| 423 |
+
maps=[rng.sample(range(5),5),rng.sample(range(5),5)]
|
| 424 |
+
return [maps,[['step',0],['switch',1],['step',0],['step',0]],rng.randrange(5)]
|
| 425 |
+
if name=='goal_switch':return [edges+[[5,0]],0,[rng.choice([3,4]),5,0]]
|
| 426 |
+
if name=='unknown_goal':
|
| 427 |
+
goal=rng.randrange(5);return [list(range(5)),[goal],[v for v in range(5) if v!=goal]]
|
| 428 |
+
if name=='tool_affordance':return [[rng.sample(range(5),rng.randint(1,5)) for _ in range(3)],rng.sample(range(5),2)]
|
| 429 |
+
if name=='select_cell':return [[[rng.randrange(3) for _ in range(3)] for _ in range(3)],rng.randrange(3)]
|
| 430 |
+
if name=='hidden_rule':
|
| 431 |
+
delta=rng.randrange(7);a=rng.randrange(7);return [list(range(7)),[[a,(a+delta)%7]],rng.randrange(7)]
|
| 432 |
+
if name=='explore_infer_exploit':
|
| 433 |
+
tables=[[[0,1],[1,0]],[[1,0],[0,1]]];world=rng.randrange(2);return [tables,[[0,0,tables[world][0][0]]],rng.randrange(2),1]
|
| 434 |
+
if name in ('sequence_composition','program_states','instruction_comp','modifier_comp','compositional_holdout','meta_rule'):
|
| 435 |
+
mapping=rng.sample(range(5),5);ops=['reverse','rotate','swap','map'];depth=rng.randint(1,3) if split!='depth' else rng.randint(4,6)
|
| 436 |
+
if split=='composition':program=['rotate','swap']
|
| 437 |
+
else:
|
| 438 |
+
while True:
|
| 439 |
+
program=[rng.choice(ops) for _ in range(depth)]
|
| 440 |
+
if all((a,b) not in [('rotate','swap'),('swap','rotate')] for a,b in zip(program,program[1:])):break
|
| 441 |
+
if name=='modifier_comp':program=['reverse']+program[:1]+['reverse']
|
| 442 |
+
if name=='meta_rule':return [arr(),program,dict(zip(ops,rng.sample(ops,len(ops)))),mapping]
|
| 443 |
+
return [arr(),program,mapping]
|
| 444 |
+
if name in ('novel_primitive','rule_remap'):return [rng.sample(range(7),7),rng.randrange(7)]
|
| 445 |
+
if name=='representation_remap':return [rng.sample(range(5),5),arr()]
|
| 446 |
+
if name=='cross_domain':return [rng.sample(range(5),5),rng.sample(range(5),5),arr()]
|
| 447 |
+
if name=='context_operator':return [arr(),arr()[:rng.randint(1,n)]]
|
| 448 |
+
if name=='mini_dsl':return ['concat',['reverse',['literal',arr()]],['repeat',2,['literal',arr()[:2]]]]
|
| 449 |
+
if name=='grammar_expansion':return [{0:[1,2],1:[0],2:[3]},arr()[:2],rng.randint(1,3)]
|
| 450 |
+
if name=='recursive_dsl':
|
| 451 |
+
depth=rng.randint(1,3) if split!='depth' else 4
|
| 452 |
+
return [{'a':['b','c'],'b':['a'],'c':['d']},rng.choice(['a','b']),depth]
|
| 453 |
+
if name=='stable_partition':return [arr(),rng.sample(range(5),2)]
|
| 454 |
+
if name=='scope_shadowing':return [[{'a':1,'b':2},{'a':rng.randrange(5)},{'b':rng.randrange(5)}],rng.choice(['a','b'])]
|
| 455 |
+
if name=='relational_join':return [[[rng.randrange(3),rng.randrange(5)] for _ in range(3)],[[rng.randrange(3),rng.randrange(5)] for _ in range(3)]]
|
| 456 |
+
if name=='edit_script':return [arr(),[['insert',1,rng.randrange(5)],['replace',0,rng.randrange(5)],['delete',2,0]]]
|
| 457 |
+
if name=='conditional_rewrite':return [arr()+[1,2],2,3,1]
|
| 458 |
+
if name=='span_reordering':return [arr(),1,n]
|
| 459 |
+
if name=='nested_substitution':return [[arr()[:2],[arr()[:2],rng.randrange(5)]],{0:1,1:2,2:3,3:4,4:0}]
|
| 460 |
+
raise KeyError(f'No sampler for {name}')
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def answer(name,x):return solve(name,x)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def build_episode(c,name,rng,context,stage,split='train',difficulty=1):
|
| 467 |
+
cases=[]
|
| 468 |
+
if name in ('queue','deque'):
|
| 469 |
+
v=[['pop_left']];cases.append((v,answer(name,v)))
|
| 470 |
+
if name in ('sequence_composition','program_states','instruction_comp','modifier_comp','compositional_holdout'):
|
| 471 |
+
for op in ('reverse','rotate','swap','map'):
|
| 472 |
+
v=[[0,1,2,3,4],[op],[1,2,3,4,0]]
|
| 473 |
+
cases.append((v,answer(name,v)))
|
| 474 |
+
for _ in range(3):
|
| 475 |
+
x=sample_input(name,rng,difficulty,'train');cases.append((x,answer(name,x)))
|
| 476 |
+
x=sample_input(name,rng,difficulty,split)
|
| 477 |
+
# Avoid exact query replay from the demonstrations when the support permits it.
|
| 478 |
+
for _ in range(30):
|
| 479 |
+
if all(x!=a for a,_ in cases):break
|
| 480 |
+
x=sample_input(name,rng,difficulty,split)
|
| 481 |
+
query=(x,answer(name,x))
|
| 482 |
+
s=c.Surface(rng);start,close,sep,end,unit,neg=s.take(6);symbols={}
|
| 483 |
+
def symbol(v):
|
| 484 |
+
key=(type(v).__name__,v)
|
| 485 |
+
if key not in symbols:symbols[key]=s.one()
|
| 486 |
+
return symbols[key]
|
| 487 |
+
def encode(v):
|
| 488 |
+
if isinstance(v,dict):return [start]+sum((encode([k,z]) for k,z in v.items()),[])+[close]
|
| 489 |
+
if isinstance(v,(list,tuple)):return [start]+sum((encode(z) for z in v),[])+[close]
|
| 490 |
+
return [symbol(v)]
|
| 491 |
+
# Ground numeric order and magnitude once, not English number words.
|
| 492 |
+
def integers(v):
|
| 493 |
+
if type(v) is int:yield v
|
| 494 |
+
elif isinstance(v,dict):
|
| 495 |
+
for a,b in v.items():yield from integers(a);yield from integers(b)
|
| 496 |
+
elif isinstance(v,(list,tuple)):
|
| 497 |
+
for z in v:yield from integers(z)
|
| 498 |
+
values=set(integers([cases,query]));b=c.Builder(stage,name)
|
| 499 |
+
if not values.issubset(set(range(-8,17))):raise ValueError(f'Numeric domain overflow: {name}')
|
| 500 |
+
for n in range(-8,17):b.emit(([neg] if n<0 else [])+[unit]*abs(n)+[sep,symbol(n),end])
|
| 501 |
+
b.emit([symbol(False),sep,symbol(0),end,symbol(True),sep,symbol(1),end])
|
| 502 |
+
for a,y in cases:b.emit(encode(a)+[sep]+encode(y)+[end])
|
| 503 |
+
b.emit(encode(query[0])+[sep]);b.mark_prompt();b.emit(encode(query[1]),c.W_TARGET)
|
| 504 |
+
ep=b.finish(context);ep.audit={'mechanism':name,'cases':cases,'query':query,'symbols':symbols,'markers':[start,close,sep,end,unit,neg],'split':split}
|
| 505 |
+
return ep
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def install(namespace):
|
| 509 |
+
# Replace each misleading v16 alias with an explicitly implemented mechanism.
|
| 510 |
+
for stage,names in [(23,MEMORY_NAMES),(25,WORLD_NAMES),(26,ALGORITHM_NAMES),(27,META_NAMES),(28,INTERACTIVE_NAMES),(29,NEW_NAMES)]:
|
| 511 |
+
funcs=[]
|
| 512 |
+
for name in names:
|
| 513 |
+
def generator(rng,context,st=stage,_name=name):
|
| 514 |
+
class Kernel:pass
|
| 515 |
+
c=Kernel()
|
| 516 |
+
for key in ['Surface','Builder','W_TARGET']:setattr(c,key,namespace[key])
|
| 517 |
+
return build_episode(c,_name,rng,context,st,difficulty=namespace['_g16_diff']())
|
| 518 |
+
generator.__name__='gen_'+name;namespace[generator.__name__]=generator;funcs.append(generator)
|
| 519 |
+
namespace['STAGES'][stage]=funcs
|
| 520 |
+
namespace['_V10_CONTEXT_TABLE'][stage]=(1536,1536,1536)
|
| 521 |
+
namespace['STAGE_NAMES'][29]='compositional_structures_and_pseudoword_transfer'
|
unified/ablations/unified-v1-blt-none-s42/source/data_stream.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pinned, repeatable shard streams. A single coordinator owns all cursors."""
|
| 2 |
+
import io,json,gzip,copy,hashlib,random,math
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from collections import Counter
|
| 5 |
+
import numpy as np
|
| 6 |
+
import httpx,requests,time
|
| 7 |
+
from huggingface_hub import HfFileSystem,get_token
|
| 8 |
+
from parquet_stream import ParquetStream
|
| 9 |
+
from records import record,plain_chunks,validate,Oversize,InvalidRecord,PAD
|
| 10 |
+
from media_data import visual_records,audio_records
|
| 11 |
+
from cortex_source import CortexSource
|
| 12 |
+
from dataset_registry import SOURCES,CORTEX_WEIGHT,retry
|
| 13 |
+
|
| 14 |
+
HOLDOUT=2
|
| 15 |
+
|
| 16 |
+
def row_key(spec,row):
|
| 17 |
+
# Same source media bytes map to the same key across repositories and captions.
|
| 18 |
+
h=hashlib.sha256()
|
| 19 |
+
media=row.get('images', [row.get('image')]) if spec['kind']=='image' else [row.get('audio')] if spec['kind']=='audio' else []
|
| 20 |
+
for v in media:
|
| 21 |
+
if isinstance(v,dict) and v.get('bytes') is not None:h.update(v['bytes'])
|
| 22 |
+
else:h.update(repr(v).encode())
|
| 23 |
+
if not media:h.update(json.dumps(row,sort_keys=True,ensure_ascii=False,default=str).encode())
|
| 24 |
+
return h.hexdigest()
|
| 25 |
+
|
| 26 |
+
class JSONRows:
|
| 27 |
+
"""Compressed JSONL resume replays only the current pinned shard, never the corpus."""
|
| 28 |
+
def __init__(self,spec,filename,state=None,opener=None):
|
| 29 |
+
self.spec,self.filename=spec,filename;self.row=int((state or {}).get('row',0));self.opener=opener
|
| 30 |
+
def state_dict(self):return {'format':'jsonl-row-v1','row':self.row}
|
| 31 |
+
def __iter__(self):
|
| 32 |
+
handle=self.opener() if self.opener else HfFileSystem(token=get_token()).open(f'datasets/{self.spec["repo"]}@{self.spec["revision"]}/{self.filename}','rb',block_size=256*1024,cache_type='readahead')
|
| 33 |
+
with handle:
|
| 34 |
+
reader=handle
|
| 35 |
+
if self.filename.endswith('.zst'):
|
| 36 |
+
import zstandard
|
| 37 |
+
reader=zstandard.ZstdDecompressor().stream_reader(handle)
|
| 38 |
+
elif self.filename.endswith('.gz'):reader=gzip.GzipFile(fileobj=handle)
|
| 39 |
+
with io.TextIOWrapper(reader,encoding='utf-8') as lines:
|
| 40 |
+
for i,line in enumerate(lines):
|
| 41 |
+
if i<self.row:continue
|
| 42 |
+
if not line.strip():self.row=i+1;continue
|
| 43 |
+
data=json.loads(line);self.row=i+1;yield data
|
| 44 |
+
|
| 45 |
+
class Source:
|
| 46 |
+
def __init__(self,name,spec,state=None,limit=1024,loader=None):
|
| 47 |
+
self.name,self.spec,self.limit,self.loader=name,spec,limit,loader
|
| 48 |
+
self.s=copy.deepcopy(state) if state else dict(shard=0,epoch=0,cursor=None,rows=0,pass_rows=0,pending=[],held=[],held_keys=[],initialized=False,rejected=0,accepted=0)
|
| 49 |
+
self.reader=self.it=None
|
| 50 |
+
def close(self):
|
| 51 |
+
if self.it is not None and hasattr(self.it,'close'):self.it.close()
|
| 52 |
+
self.it=self.reader=None
|
| 53 |
+
def open(self):
|
| 54 |
+
self.close();f=self.spec['files'][self.s['shard']]
|
| 55 |
+
if self.loader:self.reader=self.loader(f,self.s['cursor'])
|
| 56 |
+
elif f.endswith('.parquet'):
|
| 57 |
+
self.reader=ParquetStream(self.spec['repo'],f,self.spec['revision'],list(self.spec['columns']),batch_size=1 if self.spec['kind'] in ('image','audio') else 64)
|
| 58 |
+
if self.s['cursor']:self.reader.load_state_dict(self.s['cursor'])
|
| 59 |
+
else:self.reader=JSONRows(self.spec,f,self.s['cursor'])
|
| 60 |
+
self.it=iter(self.reader)
|
| 61 |
+
def raw(self):
|
| 62 |
+
attempts=0
|
| 63 |
+
while True:
|
| 64 |
+
if self.it is None:retry(self.open)
|
| 65 |
+
try:
|
| 66 |
+
row=next(self.it);self.s['cursor']=copy.deepcopy(self.reader.state_dict());self.s['rows']+=1;self.s['pass_rows']+=1;return row
|
| 67 |
+
except StopIteration:
|
| 68 |
+
self.close();self.s['cursor']=None;self.s['shard']+=1
|
| 69 |
+
if self.s['shard']==len(self.spec['files']):
|
| 70 |
+
if not self.s['pass_rows']:raise RuntimeError(self.name+': empty dataset')
|
| 71 |
+
self.s['shard']=0;self.s['epoch']+=1;self.s['pass_rows']=0
|
| 72 |
+
except (OSError,TimeoutError,ConnectionError,httpx.HTTPError,requests.RequestException) as e:
|
| 73 |
+
# Reopen at the last committed row, never move past a failed fetch.
|
| 74 |
+
attempts+=1
|
| 75 |
+
if attempts>=5:raise RuntimeError(self.name+': stream retry limit reached; resume from the durable checkpoint') from e
|
| 76 |
+
print(self.name+': retrying current shard at the saved row cursor',flush=True)
|
| 77 |
+
time.sleep(min(16,2**attempts));self.close()
|
| 78 |
+
def transform(self,row,key):
|
| 79 |
+
kind=self.spec['kind']
|
| 80 |
+
if kind=='image':return visual_records(self.name,row,self.limit)
|
| 81 |
+
if kind=='audio':return audio_records(self.name,row,self.limit,self.s['epoch'],key)
|
| 82 |
+
if kind in ('rewrite','openhermes'):
|
| 83 |
+
instruction=row.get('instruction');text=row.get('text') if kind=='rewrite' else row.get('input')
|
| 84 |
+
if kind=='rewrite' or (isinstance(text,str) and text.strip()):
|
| 85 |
+
output=row.get('output')
|
| 86 |
+
if not all(isinstance(v,str) for v in [instruction,text,output]):raise InvalidRecord(self.name+': expected instruction/text/output strings')
|
| 87 |
+
if kind=='rewrite':
|
| 88 |
+
# rewrite-plus format: no labels, exactly one blank line between fields.
|
| 89 |
+
# Train causally on every byte so rewrite6 behaves like corpus text.
|
| 90 |
+
return plain_chunks(f'{instruction}\n\n{text}\n\n{output}',self.name,self.limit)
|
| 91 |
+
return [record(f'Instruction:\n{instruction}\n\nText:\n{text}\n\nRewrite:\n',output,self.name,self.limit,supervise_all=True)]
|
| 92 |
+
text=row.get(self.spec['column'])
|
| 93 |
+
if not isinstance(text,str):raise InvalidRecord(self.name+': missing text column '+self.spec['column'])
|
| 94 |
+
return plain_chunks(text,self.name,self.limit)
|
| 95 |
+
def initialize(self):
|
| 96 |
+
if self.s['initialized']:return
|
| 97 |
+
for _ in range(128):
|
| 98 |
+
row=self.raw();key=row_key(self.spec,row)
|
| 99 |
+
try:records=self.transform(row,key)
|
| 100 |
+
except (Oversize,InvalidRecord,ValueError):self.s['rejected']+=1;continue
|
| 101 |
+
self.s['held_keys'].append(key)
|
| 102 |
+
# One complete record per held-out source row; fixed across restarts.
|
| 103 |
+
self.s['held'].append(records[0])
|
| 104 |
+
if self.spec['kind']=='audio':
|
| 105 |
+
self.s['held'].extend(r for r in records[1:] if r['meta'].get('task')=='generation')
|
| 106 |
+
if len(self.s['held_keys'])==HOLDOUT:break
|
| 107 |
+
if len(self.s['held_keys'])<HOLDOUT:raise RuntimeError(self.name+': unable to construct validation records')
|
| 108 |
+
self.s['initialized']=True
|
| 109 |
+
def next_record(self,excluded):
|
| 110 |
+
if self.s['pending']:return self.s['pending'].pop(0)
|
| 111 |
+
for _ in range(256):
|
| 112 |
+
row=self.raw();key=row_key(self.spec,row)
|
| 113 |
+
if key in excluded:continue
|
| 114 |
+
try:rs=self.transform(row,key)
|
| 115 |
+
except Oversize:self.s['rejected']+=1;continue
|
| 116 |
+
except (InvalidRecord,ValueError) as e:
|
| 117 |
+
self.s['rejected']+=1
|
| 118 |
+
if self.s['rejected']<=3:print(f'{self.name}: rejected {type(e).__name__}: {e}',flush=True)
|
| 119 |
+
continue
|
| 120 |
+
self.s['accepted']+=1;self.s['pending']=rs[1:];return validate(rs[0],self.limit)
|
| 121 |
+
raise RuntimeError(self.name+': 256 consecutive unusable/held-out rows; fix the schema or byte budget')
|
| 122 |
+
def state_dict(self):return copy.deepcopy(self.s)
|
| 123 |
+
|
| 124 |
+
class Mixer:
|
| 125 |
+
"""Source weights choose blocks, not rows. Complete structured records never split.
|
| 126 |
+
|
| 127 |
+
Text chunks carry the boundary byte. Packing is ordinary causal document packing
|
| 128 |
+
with BOS/EOS boundaries; there is no quadratic block-diagonal attention mask.
|
| 129 |
+
"""
|
| 130 |
+
def __init__(self,manifest,limit=1024,state=None,loaders=None,cortex=True,mix_weights=None):
|
| 131 |
+
state=state or {};self.limit=limit;self.manifest=manifest
|
| 132 |
+
self.sources={n:Source(n,spec,state.get('sources',{}).get(n),limit,(loaders or {}).get(n)) for n,spec in manifest.items()}
|
| 133 |
+
self.cortex=CortexSource(state.get('cortex'),limit) if cortex else None
|
| 134 |
+
self.rng=random.Random(20260911)
|
| 135 |
+
if 'rng' in state:self.rng.setstate(state['rng'])
|
| 136 |
+
defaults={n:int(spec['weight']) for n,spec in manifest.items()}
|
| 137 |
+
if self.cortex:defaults['cortex']=int(CORTEX_WEIGHT)
|
| 138 |
+
requested=dict(state.get('mix_weights',{}));requested.update(mix_weights or {})
|
| 139 |
+
self.mix_weights={n:int(requested.get(n,defaults[n])) for n in defaults}
|
| 140 |
+
if any(v<=0 for v in self.mix_weights.values()):raise ValueError('All active mix weights must be positive integers')
|
| 141 |
+
self.mix_queue=list(state.get('mix_queue',[]))
|
| 142 |
+
if any(n not in self.mix_weights for n in self.mix_queue):self.mix_queue=[]
|
| 143 |
+
self.pending=copy.deepcopy(state.get('pending',{}));self.stats=Counter(state.get('stats',{}))
|
| 144 |
+
self.excluded=set(state.get('excluded',[]))
|
| 145 |
+
def initialize(self):
|
| 146 |
+
for s in self.sources.values():
|
| 147 |
+
s.initialize();self.excluded.update(s.s['held_keys'])
|
| 148 |
+
# Validate one real training record per source before GPU allocation.
|
| 149 |
+
for name,s in self.sources.items():
|
| 150 |
+
if name not in self.pending:self.pending[name]=s.next_record(self.excluded)
|
| 151 |
+
return self
|
| 152 |
+
def next_for(self,name):
|
| 153 |
+
if name in self.pending:return self.pending.pop(name)
|
| 154 |
+
return self.cortex.record() if name=='cortex' else self.sources[name].next_record(self.excluded)
|
| 155 |
+
def _refill_mix_queue(self):
|
| 156 |
+
# Low-discrepancy weighted window: exact long-run quotas, randomized phase,
|
| 157 |
+
# and roughly even spacing. This keeps small rewrite6/Cortex shares present
|
| 158 |
+
# across the entire run without random multi-thousand-block droughts.
|
| 159 |
+
slots=[]
|
| 160 |
+
for name,weight in self.mix_weights.items():
|
| 161 |
+
phase=self.rng.random()
|
| 162 |
+
for k in range(weight):
|
| 163 |
+
position=(k+phase)/weight
|
| 164 |
+
jitter=(self.rng.random()-.5)*(.30/weight)
|
| 165 |
+
slots.append((position+jitter,self.rng.random(),name))
|
| 166 |
+
slots.sort();self.mix_queue=[name for _,__,name in slots]
|
| 167 |
+
self.stats['mix/windows']+=1
|
| 168 |
+
def block(self):
|
| 169 |
+
if not self.mix_queue:self._refill_mix_queue()
|
| 170 |
+
name=self.mix_queue.pop(0);tokens=[];mask=[]
|
| 171 |
+
while True:
|
| 172 |
+
r=self.next_for(name);validate(r,self.limit)
|
| 173 |
+
if tokens and len(tokens)+len(r['ids'])>self.limit+1:self.pending[name]=r;break
|
| 174 |
+
tokens.extend(r['ids']);mask.extend(r['weights'])
|
| 175 |
+
if len(tokens)==self.limit+1 or len(tokens)>=int(self.limit*.90):break
|
| 176 |
+
# Large language chunks are already context-sized; small rows pack.
|
| 177 |
+
used=len(tokens);n=self.limit+1-used
|
| 178 |
+
tokens.extend([PAD]*n);mask.extend([0]*n)
|
| 179 |
+
self.stats[name+'/blocks']+=1;self.stats[name+'/targets']+=sum(mask[1:]);self.stats[name+'/tokens']+=used-1
|
| 180 |
+
return tokens,mask,name,used
|
| 181 |
+
def batch(self,count):
|
| 182 |
+
blocks=[self.block() for _ in range(count)]
|
| 183 |
+
# All ranks receive the same trimmed width, bounded by the full 1024 limit.
|
| 184 |
+
width=min(self.limit+1,1+math.ceil(max(b[3]-1 for b in blocks)/128)*128)
|
| 185 |
+
return {'ids':np.array([b[0][:width] for b in blocks],dtype=np.int64),'weights':np.array([b[1][:width] for b in blocks],dtype=np.float32),'sources':[b[2] for b in blocks]}
|
| 186 |
+
def validation(self):
|
| 187 |
+
return [r for s in self.sources.values() for r in s.s['held']]+(self.cortex.validation() if self.cortex else [])
|
| 188 |
+
def state_dict(self):
|
| 189 |
+
return dict(sources={n:s.state_dict() for n,s in self.sources.items()},cortex=self.cortex.state_dict() if self.cortex else None,
|
| 190 |
+
rng=self.rng.getstate(),mix_weights=dict(self.mix_weights),mix_queue=list(self.mix_queue),
|
| 191 |
+
pending=copy.deepcopy(self.pending),stats=dict(self.stats),excluded=sorted(self.excluded))
|
| 192 |
+
def close(self):
|
| 193 |
+
for s in self.sources.values():s.close()
|
unified/ablations/unified-v1-blt-none-s42/source/dataset_registry.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Enabled language-training sources for the current SparkBET run."""
|
| 2 |
+
from dataclasses import dataclass,asdict
|
| 3 |
+
import fnmatch,time
|
| 4 |
+
from huggingface_hub import HfApi
|
| 5 |
+
|
| 6 |
+
@dataclass(frozen=True)
|
| 7 |
+
class Spec:
|
| 8 |
+
repo:str
|
| 9 |
+
patterns:tuple
|
| 10 |
+
columns:tuple
|
| 11 |
+
weight:float
|
| 12 |
+
kind:str='text'
|
| 13 |
+
column:str='text'
|
| 14 |
+
|
| 15 |
+
# Keep the full project data framework while the current run uses language-only
|
| 16 |
+
# repositories. Non-language dataset entries are intentionally inactive.
|
| 17 |
+
SOURCES={
|
| 18 |
+
'climbmix':Spec('karpathy/climbmix-400b-shuffle',('*.parquet','**/*.parquet'),('text',),50,column='text'),
|
| 19 |
+
'rewrite6':Spec('appvoid/rewrite6',('**/rewrite.jsonl','rewrite.jsonl','**/*.jsonl'),('instruction','text','output'),10,'rewrite'),
|
| 20 |
+
'ultra_style':Spec('openbmb/Ultra-FineWeb-L3',('data/ultrafineweb_en_l3/multi_style/*.parquet',),('content',),20,column='content'),
|
| 21 |
+
'ultra_qa':Spec('openbmb/Ultra-FineWeb-L3',('data/ultrafineweb_en_l3/qa/*.parquet',),('content',),20,column='content'),
|
| 22 |
+
}
|
| 23 |
+
CORTEX_WEIGHT=15
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def retry(fn,attempts=5):
|
| 27 |
+
for i in range(attempts):
|
| 28 |
+
try:return fn()
|
| 29 |
+
except Exception as e:
|
| 30 |
+
status=getattr(getattr(e,'response',None),'status_code',None)
|
| 31 |
+
if status in (401,403,404):raise
|
| 32 |
+
if i==attempts-1:raise
|
| 33 |
+
print(f'Network retry {i+1}/{attempts}: {type(e).__name__}',flush=True);time.sleep(min(16,2**i))
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def make_manifest(overrides=None,disabled=()):
|
| 37 |
+
api=HfApi();cache={};manifest={};overrides=overrides or {}
|
| 38 |
+
for name,spec in SOURCES.items():
|
| 39 |
+
if name in disabled:continue
|
| 40 |
+
if spec.repo not in cache:cache[spec.repo]=retry(lambda:api.dataset_info(spec.repo))
|
| 41 |
+
info=cache[spec.repo];patterns=overrides.get(name,spec.patterns)
|
| 42 |
+
files=sorted({f.rfilename for f in info.siblings if any(fnmatch.fnmatch(f.rfilename,p) for p in patterns)})
|
| 43 |
+
if not files:raise RuntimeError(f'{name}: no training files match {patterns} in {spec.repo}. Set DATA_FILE_OVERRIDES after inspecting the repository.')
|
| 44 |
+
manifest[name]={**asdict(spec),'revision':info.sha,'files':files}
|
| 45 |
+
print(f'{name}: pinned {len(files)} shards at {info.sha[:12]}',flush=True)
|
| 46 |
+
return manifest
|
unified/ablations/unified-v1-blt-none-s42/source/inference.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small text-generation helper for an exported SparkBET repository."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import torch
|
| 4 |
+
from safetensors.torch import load_file
|
| 5 |
+
from bet_model import SparkBET,BETConfig,uniform_steps
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Cortex:
|
| 9 |
+
def __init__(self,model,device=None):
|
| 10 |
+
self.model=model
|
| 11 |
+
self.device=torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
|
| 12 |
+
self.model.to(self.device).eval()
|
| 13 |
+
|
| 14 |
+
@classmethod
|
| 15 |
+
def from_export(cls,folder,device=None):
|
| 16 |
+
folder=Path(folder);model=SparkBET(BETConfig())
|
| 17 |
+
state=load_file(str(folder/"model.safetensors"),device="cpu")
|
| 18 |
+
if state and all(k.startswith("core.") for k in state):state={k[5:]:v for k,v in state.items()}
|
| 19 |
+
model.load_state_dict(state,strict=True)
|
| 20 |
+
return cls(model,device)
|
| 21 |
+
|
| 22 |
+
def generate_ids(self,ids,max_new_tokens=128,loops=8,temperature=0.0,top_k=None):
|
| 23 |
+
out=list(map(int,ids))
|
| 24 |
+
for _ in range(int(max_new_tokens)):
|
| 25 |
+
current=out[-self.model.c.max_seq_len:]
|
| 26 |
+
x=torch.tensor([current],device=self.device,dtype=torch.long)
|
| 27 |
+
with torch.inference_mode(),torch.autocast(self.device.type,dtype=torch.float16,enabled=self.device.type=="cuda"):
|
| 28 |
+
logits=self.model(x,uniform_steps(loops))[0,-1].float()
|
| 29 |
+
if temperature and temperature>0:
|
| 30 |
+
logits=logits/float(temperature)
|
| 31 |
+
if top_k:
|
| 32 |
+
values,_=torch.topk(logits,min(int(top_k),logits.numel()));logits[logits<values[-1]]=-float("inf")
|
| 33 |
+
nxt=int(torch.multinomial(torch.softmax(logits,-1),1))
|
| 34 |
+
else:nxt=int(logits.argmax())
|
| 35 |
+
out.append(nxt)
|
| 36 |
+
if nxt==258:break
|
| 37 |
+
return out
|
| 38 |
+
|
| 39 |
+
def generate(self,text,max_new_tokens=128,loops=8,temperature=0.0,top_k=None):
|
| 40 |
+
ids=[257]+list(text.encode("utf-8"))
|
| 41 |
+
out=self.generate_ids(ids,max_new_tokens,loops,temperature,top_k)
|
| 42 |
+
body=bytes(i for i in out[1:] if 0<=i<=255)
|
| 43 |
+
return body.decode("utf-8",errors="replace")
|
unified/ablations/unified-v1-blt-none-s42/source/media_data.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Complete caption/chat and audio records with strict byte budgets."""
|
| 2 |
+
import io,json,hashlib,math
|
| 3 |
+
from PIL import Image,ImageOps
|
| 4 |
+
from records import record,Oversize,InvalidRecord
|
| 5 |
+
from braille_encoder import encode_image,fit_box_to_square,local_contrast,floyd_steinberg,binary_to_braille
|
| 6 |
+
from audio_codec import canonical_audio,overview,encode_tile,CHUNK,RATE
|
| 7 |
+
|
| 8 |
+
def load_image(value):
|
| 9 |
+
if isinstance(value,Image.Image):im=value.copy()
|
| 10 |
+
elif isinstance(value,dict):
|
| 11 |
+
src=io.BytesIO(value['bytes']) if value.get('bytes') is not None else value.get('path')
|
| 12 |
+
if src is None:raise InvalidRecord('Image has no embedded data or local path')
|
| 13 |
+
with Image.open(src) as source:im=source.copy()
|
| 14 |
+
else:
|
| 15 |
+
with Image.open(value) as source:im=source.copy()
|
| 16 |
+
im=ImageOps.exif_transpose(im).convert('RGBA');im.thumbnail((1024,1024))
|
| 17 |
+
return im
|
| 18 |
+
|
| 19 |
+
def image_text(value,grid=80):
|
| 20 |
+
if grid>80 or grid<8 or grid%4:raise ValueError('Grid must be a multiple of four, at most 80')
|
| 21 |
+
im=load_image(value)
|
| 22 |
+
# Full-frame fit uses the supplied contrast/dither/dot layout. Saliency crops
|
| 23 |
+
# can discard the object asked about, so dataset examples preserve the frame.
|
| 24 |
+
small=fit_box_to_square(im,(0,0,*im.size),grid)
|
| 25 |
+
gray,alpha=local_contrast(small)
|
| 26 |
+
return '<image>\n'+binary_to_braille(floyd_steinberg(gray,alpha),alpha)+'\n</image>'
|
| 27 |
+
|
| 28 |
+
def content_text(content):
|
| 29 |
+
if isinstance(content,str):return content.replace('<image>','').strip()
|
| 30 |
+
if not isinstance(content,list):raise InvalidRecord('Unknown chat content')
|
| 31 |
+
return '\n'.join(p['text'] for p in content if p.get('type')=='text' and p.get('text')).strip()
|
| 32 |
+
|
| 33 |
+
def visual_records(name,row,limit=8192):
|
| 34 |
+
pictures=row['images'] if name=='llava' else [row['image']]
|
| 35 |
+
if not pictures:raise InvalidRecord('No image')
|
| 36 |
+
# Prefer full 80x80 resolution; reduce only to fit multiple complete images.
|
| 37 |
+
for grid in [80,64,48,32,16,8]:
|
| 38 |
+
image='\n'.join(image_text(v,grid) for v in pictures)
|
| 39 |
+
if len(image.encode())<limit-1024:break
|
| 40 |
+
else:raise Oversize('Too many images')
|
| 41 |
+
out=[]
|
| 42 |
+
def add(prefix,answer):
|
| 43 |
+
if not isinstance(answer,str) or not answer.strip():raise InvalidRecord('Empty visual answer')
|
| 44 |
+
try:out.append(record(prefix,answer,name,limit,meta={'grid':grid}))
|
| 45 |
+
except Oversize:pass # Whole target rejected; no fabricated answer fragments.
|
| 46 |
+
if name=='flickr8k':
|
| 47 |
+
for i in range(5):add(image+'\n\nCaption:\n',row[f'caption_{i}'])
|
| 48 |
+
elif name=='coco_captions':
|
| 49 |
+
values=row['caption'] if isinstance(row['caption'],list) else [row['caption']]
|
| 50 |
+
for caption in values:add(image+'\n\nCaption:\n',caption)
|
| 51 |
+
elif name=='chartqa':
|
| 52 |
+
values=row['label'] if isinstance(row['label'],list) else [row['label']]
|
| 53 |
+
for answer in values:add(image+'\n\nUser:\n'+row['query']+'\n\nAssistant:\n',answer)
|
| 54 |
+
else:
|
| 55 |
+
messages=row['messages'] if name=='llava' else [{'role':{'human':'user','gpt':'assistant'}[m['from']],'content':m['value']} for m in row['conversations']]
|
| 56 |
+
history=image;last=None
|
| 57 |
+
for m in messages:
|
| 58 |
+
role=m['role'];text=content_text(m['content'])
|
| 59 |
+
if role not in ('user','assistant','system'):raise InvalidRecord('Unknown role')
|
| 60 |
+
if role=='assistant':
|
| 61 |
+
if last!='user':raise InvalidRecord('Assistant without user context')
|
| 62 |
+
prefix=history+'\n\nAssistant:\n';add(prefix,text)
|
| 63 |
+
# Preserve full history for follow-ups; never reset to question only.
|
| 64 |
+
history+='\n\n'+role.title()+':\n'+text;last=role
|
| 65 |
+
if not out:raise Oversize('No complete visual turn fits the context')
|
| 66 |
+
return out
|
| 67 |
+
|
| 68 |
+
def audio_records(name,row,limit=8192,epoch=0,identity=''):
|
| 69 |
+
wave=canonical_audio(row['audio']);caption=row['text'] if name=='librispeech' else row['caption']
|
| 70 |
+
if not isinstance(caption,str) or not caption.strip():raise InvalidRecord('Missing audio text')
|
| 71 |
+
if len(wave)>RATE*30:raise Oversize('Whole-clip overview restricted to 30 seconds; no partial audio/full transcript pairs')
|
| 72 |
+
length=len(wave)/RATE;out=[]
|
| 73 |
+
kind='Transcript' if name=='librispeech' else 'Caption'
|
| 74 |
+
prefix=f'<audio_overview sr={RATE} seconds={length:.6f} freq_low=bottom time=right>\n'+overview(wave)+'\n</audio_overview>\n\n'+kind+':\n'
|
| 75 |
+
try:out.append(record(prefix,caption,name,limit,meta={'task':'understanding','seconds':length}))
|
| 76 |
+
except Oversize:pass
|
| 77 |
+
total=math.ceil(len(wave)/CHUNK)
|
| 78 |
+
# Each source-row pass advances its selected tile. Every tile is eventually
|
| 79 |
+
# visited without storing thousands of expanded samples in checkpoint state.
|
| 80 |
+
offset=int.from_bytes(hashlib.blake2b(identity.encode(),digest_size=8).digest(),'big')
|
| 81 |
+
tile=(offset+epoch)%total
|
| 82 |
+
target=encode_tile(wave[tile*CHUNK:(tile+1)*CHUNK])
|
| 83 |
+
previous=encode_tile(wave[(tile-1)*CHUNK:tile*CHUNK]) if tile else '<audio_start>'
|
| 84 |
+
prefix=f'{kind}:\n{caption}\n\nGenerate audio tile {tile+1}/{total}; start_sample={tile*CHUNK}; sample_rate={RATE}.\nPrevious:\n{previous}\nNext:\n'
|
| 85 |
+
try:out.append(record(prefix,target,name,limit,meta={'task':'generation','tile':tile,'tiles':total}))
|
| 86 |
+
except Oversize:pass
|
| 87 |
+
if not out:raise Oversize('No complete audio record fits')
|
| 88 |
+
return out
|
unified/ablations/unified-v1-blt-none-s42/source/modeling_bet.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from transformers import PreTrainedModel, GenerationMixin
|
| 5 |
+
from transformers.modeling_outputs import CausalLMOutput
|
| 6 |
+
|
| 7 |
+
from .configuration_bet import BETConfig
|
| 8 |
+
from .bet_model import BETConfig as CoreConfig, SparkBET, uniform_steps
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class BETPreTrainedModel(PreTrainedModel):
|
| 12 |
+
config_class=BETConfig
|
| 13 |
+
base_model_prefix="core"
|
| 14 |
+
supports_gradient_checkpointing=False
|
| 15 |
+
_no_split_modules=["PlainBlock","LoopedBlock"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class BETForCausalLM(BETPreTrainedModel,GenerationMixin):
|
| 19 |
+
def __init__(self,config):
|
| 20 |
+
super().__init__(config)
|
| 21 |
+
core_cfg=CoreConfig(
|
| 22 |
+
vocab_size=config.vocab_size,
|
| 23 |
+
hidden_size=config.hidden_size,
|
| 24 |
+
intermediate_size=config.intermediate_size,
|
| 25 |
+
prelude_layers=config.prelude_layers,
|
| 26 |
+
body_blocks=config.body_blocks,
|
| 27 |
+
coda_layers=config.coda_layers,
|
| 28 |
+
num_heads=config.num_attention_heads,
|
| 29 |
+
num_kv_heads=config.num_key_value_heads,
|
| 30 |
+
head_dim=config.head_dim,
|
| 31 |
+
lora_rank=config.lora_rank,
|
| 32 |
+
hyper_lanes=config.hyper_lanes,
|
| 33 |
+
max_seq_len=config.max_position_embeddings,
|
| 34 |
+
max_loops=config.max_loops,
|
| 35 |
+
rope_theta=config.rope_theta,
|
| 36 |
+
rms_eps=config.rms_norm_eps,
|
| 37 |
+
ddl_beta_init=config.ddl_beta_init,
|
| 38 |
+
ddl_k_eps=config.ddl_k_eps,
|
| 39 |
+
ddl_v_sigmoid_scale=config.ddl_v_sigmoid_scale,
|
| 40 |
+
)
|
| 41 |
+
self.core=SparkBET(core_cfg)
|
| 42 |
+
|
| 43 |
+
def get_input_embeddings(self):return self.core.embed
|
| 44 |
+
def set_input_embeddings(self,value):self.core.embed=value
|
| 45 |
+
def get_output_embeddings(self):return None
|
| 46 |
+
def set_output_embeddings(self,value):
|
| 47 |
+
if value is not None:raise ValueError("SparkBET uses tied input/output embeddings")
|
| 48 |
+
|
| 49 |
+
def _cycles(self,cycles=None):
|
| 50 |
+
if cycles is None:
|
| 51 |
+
cycles=int(os.environ.get("BET_EVAL_CYCLES",self.config.refinement_cycles))
|
| 52 |
+
cycles=int(cycles)
|
| 53 |
+
if not 1<=cycles<=self.config.max_loops:
|
| 54 |
+
raise ValueError(f"refinement cycles must be in [1,{self.config.max_loops}]")
|
| 55 |
+
return cycles
|
| 56 |
+
|
| 57 |
+
def forward(
|
| 58 |
+
self,input_ids=None,attention_mask=None,labels=None,cycles=None,
|
| 59 |
+
past_key_values=None,use_cache=None,return_dict=True,**kwargs,
|
| 60 |
+
):
|
| 61 |
+
if input_ids is None:raise ValueError("input_ids is required")
|
| 62 |
+
if past_key_values is not None:raise ValueError("SparkBET does not implement a KV cache")
|
| 63 |
+
logits=self.core(input_ids,uniform_steps(self._cycles(cycles)),attention_mask=attention_mask)
|
| 64 |
+
loss=None
|
| 65 |
+
if labels is not None:
|
| 66 |
+
shift_logits=logits[:,:-1].contiguous().float();shift_labels=labels[:,1:].contiguous()
|
| 67 |
+
loss=F.cross_entropy(shift_logits.view(-1,shift_logits.size(-1)),shift_labels.view(-1),ignore_index=-100)
|
| 68 |
+
if not return_dict:return tuple(v for v in (loss,logits) if v is not None)
|
| 69 |
+
return CausalLMOutput(loss=loss,logits=logits)
|
| 70 |
+
|
| 71 |
+
def prepare_inputs_for_generation(self,input_ids,attention_mask=None,**kwargs):
|
| 72 |
+
max_len=self.config.max_position_embeddings
|
| 73 |
+
if input_ids.shape[1]>max_len:
|
| 74 |
+
input_ids=input_ids[:,-max_len:]
|
| 75 |
+
if attention_mask is not None:attention_mask=attention_mask[:,-max_len:]
|
| 76 |
+
return {"input_ids":input_ids,"attention_mask":attention_mask,"use_cache":False}
|
unified/ablations/unified-v1-blt-none-s42/source/parquet_stream.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synchronous Parquet row-group streaming with an explicit resumable row cursor.
|
| 2 |
+
|
| 3 |
+
Avoids background Arrow scanner callbacks into Python file objects during shutdown.
|
| 4 |
+
Only selected columns and the current batch are materialized; no local corpus cache.
|
| 5 |
+
"""
|
| 6 |
+
import copy
|
| 7 |
+
import pyarrow.parquet as pq
|
| 8 |
+
from huggingface_hub import HfFileSystem,get_token
|
| 9 |
+
class ParquetStream:
|
| 10 |
+
def __init__(self,repo,filename,revision,columns,batch_size=128,opener=None):
|
| 11 |
+
self.columns,self.batch_size=columns,batch_size
|
| 12 |
+
self.cursor={'format':'parquet-row-v1','row_group':0,'row':0}
|
| 13 |
+
self.opener=opener or (lambda:HfFileSystem(token=get_token()).open(
|
| 14 |
+
f'datasets/{repo}@{revision}/{filename}','rb',block_size=65536,cache_type='bytes'))
|
| 15 |
+
def state_dict(self):return copy.deepcopy(self.cursor)
|
| 16 |
+
def load_state_dict(self,state):
|
| 17 |
+
if state.get('format')!='parquet-row-v1':
|
| 18 |
+
raise ValueError('Unsupported Parquet cursor; preserve the previous trainer for older runs')
|
| 19 |
+
self.cursor=copy.deepcopy(state)
|
| 20 |
+
def __iter__(self):
|
| 21 |
+
with self.opener() as handle:
|
| 22 |
+
reader=pq.ParquetFile(handle,pre_buffer=False,buffer_size=65536)
|
| 23 |
+
try:
|
| 24 |
+
for group in range(self.cursor['row_group'],reader.num_row_groups):
|
| 25 |
+
skip=self.cursor['row'] if group==self.cursor['row_group'] else 0
|
| 26 |
+
position=0
|
| 27 |
+
for batch in reader.iter_batches(batch_size=self.batch_size,row_groups=[group],
|
| 28 |
+
columns=self.columns,use_threads=False):
|
| 29 |
+
if position+batch.num_rows<=skip:
|
| 30 |
+
position+=batch.num_rows;continue
|
| 31 |
+
for row in batch.to_pylist():
|
| 32 |
+
position+=1
|
| 33 |
+
if position<=skip:continue
|
| 34 |
+
self.cursor={'format':'parquet-row-v1','row_group':group,'row':position}
|
| 35 |
+
yield row
|
| 36 |
+
self.cursor={'format':'parquet-row-v1','row_group':group+1,'row':0}
|
| 37 |
+
finally:reader.close()
|
unified/ablations/unified-v1-blt-none-s42/source/prepared_data.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""rewrite7: immutable prepared records, epoch reuse and bounded disk prefetch.
|
| 2 |
+
|
| 3 |
+
Preparation and observation are distinct. Only a successful optimizer update may
|
| 4 |
+
advance the consumer cursor or enter the observation ledger. Dataset uploads
|
| 5 |
+
precede consumption; the model checkpoint is the durable recovery authority.
|
| 6 |
+
"""
|
| 7 |
+
from collections import OrderedDict
|
| 8 |
+
import bisect,copy,hashlib,json,math,os,queue,threading,time,traceback
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pyarrow as pa
|
| 12 |
+
import pyarrow.parquet as pq
|
| 13 |
+
import torch
|
| 14 |
+
from huggingface_hub import HfApi,hf_hub_download,CommitOperationAdd
|
| 15 |
+
from checkpoints import atomic_json,digest
|
| 16 |
+
from dataset_registry import retry
|
| 17 |
+
from unified_data import CoverageSource,CoverageMixer,CodecCortex
|
| 18 |
+
from cortex_source import CortexSource
|
| 19 |
+
from records import PAD,validate
|
| 20 |
+
|
| 21 |
+
SCHEMA=1
|
| 22 |
+
|
| 23 |
+
def sha(value):return hashlib.sha256(json.dumps(value,sort_keys=True,default=str).encode()).hexdigest()
|
| 24 |
+
|
| 25 |
+
def recipe_hash():
|
| 26 |
+
root=Path(__file__).parent;h=hashlib.sha256()
|
| 27 |
+
for name in ('prepared_data.py','unified_data.py','records.py','cortex_source.py','cortex_data.py','cortex_tasks.py','cortex_curriculum_v16.py','canvas_flow.py'):
|
| 28 |
+
h.update(name.encode());h.update((root/name).read_bytes())
|
| 29 |
+
return h.hexdigest()
|
| 30 |
+
|
| 31 |
+
def cursor_key(cursor):
|
| 32 |
+
if cursor is None:return (-1,-1)
|
| 33 |
+
if 'row_group' in cursor:return (int(cursor['row_group']),int(cursor['row']))
|
| 34 |
+
return (0,int(cursor.get('row',cursor.get('i',0))))
|
| 35 |
+
|
| 36 |
+
class EndFile(Exception):pass
|
| 37 |
+
|
| 38 |
+
class OneFile(CoverageSource):
|
| 39 |
+
def raw(self):
|
| 40 |
+
if self.it is None:retry(self.open)
|
| 41 |
+
try:row=next(self.it)
|
| 42 |
+
except StopIteration:self.close();raise EndFile
|
| 43 |
+
self.s['cursor']=copy.deepcopy(self.reader.state_dict());self.s['rows']+=1;self.s['pass_rows']+=1
|
| 44 |
+
return row
|
| 45 |
+
|
| 46 |
+
def _worker_init():
|
| 47 |
+
torch.set_num_threads(1)
|
| 48 |
+
|
| 49 |
+
def _cortex_record(args):
|
| 50 |
+
kind,index,limit,codec=args
|
| 51 |
+
if kind=='kernel':return CortexSource({'index':index},limit).record()
|
| 52 |
+
return CodecCortex({'visual_index':index,'until_visual':0},limit,codec).record()
|
| 53 |
+
|
| 54 |
+
class Generated:
|
| 55 |
+
def __init__(self,kind,start,count,limit,codec,state=None):
|
| 56 |
+
self.kind,self.start,self.count,self.limit,self.codec=kind,start,count,limit,codec
|
| 57 |
+
self.s=copy.deepcopy(state) if state else dict(index=0,cursor=None,rows=0,pending_document=None)
|
| 58 |
+
def initialize(self):pass
|
| 59 |
+
def next_record(self):
|
| 60 |
+
i=self.s['index']
|
| 61 |
+
if i>=self.count:raise EndFile
|
| 62 |
+
if self.kind=='kernel':r=CortexSource({'index':self.start+i},self.limit).record()
|
| 63 |
+
else:r=CodecCortex({'visual_index':self.start+i,'until_visual':0},self.limit,self.codec).record()
|
| 64 |
+
self.s.update(index=i+1,rows=i+1,cursor={'row':i+1})
|
| 65 |
+
return r
|
| 66 |
+
def records(self,count,pool=None):
|
| 67 |
+
n=min(count,self.count-self.s['index'])
|
| 68 |
+
if pool is None:
|
| 69 |
+
for _ in range(n):yield self.next_record()
|
| 70 |
+
else:
|
| 71 |
+
start=self.s['index'];args=[(self.kind,self.start+i,self.limit,self.codec) for i in range(start,start+n)]
|
| 72 |
+
for r in pool.map(_cortex_record,args,chunksize=8):
|
| 73 |
+
i=self.s['index']+1;self.s.update(index=i,rows=i,cursor={'row':i});yield r
|
| 74 |
+
def state_dict(self):return copy.deepcopy(self.s)
|
| 75 |
+
def close(self):pass
|
| 76 |
+
|
| 77 |
+
class Archive:
|
| 78 |
+
def __init__(self,cfg,api=None):
|
| 79 |
+
self.cfg=cfg;self.repo=cfg.get('prepared_repo','appvoid/rewrite7')
|
| 80 |
+
self.root=Path(cfg['work_dir'])/'rewrite7_cache';self.root.mkdir(parents=True,exist_ok=True)
|
| 81 |
+
self.online=bool(cfg.get('upload',False));self.read_remote=bool(cfg.get('hub_resume',False))
|
| 82 |
+
self.api=api or HfApi();self.head=None;self.remote=set();self.lock=threading.RLock();self.partitions={};self.tables=OrderedDict();self.pool=None
|
| 83 |
+
self.recipe=recipe_hash();self.prepared_records=0
|
| 84 |
+
if self.online:retry(lambda:self.api.create_repo(self.repo,repo_type='dataset',private=True,exist_ok=True))
|
| 85 |
+
if self.online or self.read_remote:
|
| 86 |
+
info=retry(lambda:self.api.dataset_info(self.repo));self.head=info.sha;self.remote={s.rfilename for s in info.siblings}
|
| 87 |
+
card=self.root/'README.md'
|
| 88 |
+
card.write_text('''---
|
| 89 |
+
pretty_name: rewrite7 prepared Cortex mixture
|
| 90 |
+
configs:
|
| 91 |
+
- config_name: default
|
| 92 |
+
data_files:
|
| 93 |
+
- split: train
|
| 94 |
+
path: data/**/*.parquet
|
| 95 |
+
---
|
| 96 |
+
# rewrite7
|
| 97 |
+
|
| 98 |
+
Canonical prepared UTF-8 byte records from ClimbMix, Ultra-FineWeb-L3 **English QA/multi-style only**, rewrite6, and the deterministic Cortex generator. Original repository revisions, source filenames, row cursors, processing recipe and sample IDs are preserved. Upstream licenses and provenance remain applicable; this does not assign a new license to upstream content.
|
| 99 |
+
|
| 100 |
+
`data/<source>/<partition>/part-*.parquet` stores each source position once per processing recipe, **not once per epoch or training run**. Index manifests store the independent preparation cursor. `observed/<run>/<lineage>/...json` logs successful sample presentations and identifies the corresponding model checkpoint SHA. Repeated epochs reuse IDs and files. Prepared rows include a lookahead buffer; they must not all be counted as observed. Model checkpoints remain authoritative for durable recovery because the model and dataset repositories cannot commit atomically together.
|
| 101 |
+
|
| 102 |
+
Record fields: sample_id, source, ids (uint16), weights (uint8), prompt_len, metadata JSON, original cursor JSON, row ordinal, and last_chunk. The model's committed source/shard/record/epoch cursors provide the exact next training position. Canvas layout/image tensors are prepared in local dispatch shards; their deterministic recipe/seed/update are recorded in the observation ledger rather than duplicating corpus records for every view or epoch.
|
| 103 |
+
''')
|
| 104 |
+
if self.online and 'README.md' not in self.remote:self.publish({'README.md':card},'Initialize rewrite7 prepared-data schema')
|
| 105 |
+
def fetch(self,relative):
|
| 106 |
+
p=self.root/relative
|
| 107 |
+
if p.exists():os.utime(p,None);return p
|
| 108 |
+
if relative not in self.remote:raise FileNotFoundError(relative)
|
| 109 |
+
# local_dir avoids a second full copy in the global Hugging Face cache.
|
| 110 |
+
retry(lambda:hf_hub_download(self.repo,relative,repo_type='dataset',revision=self.head,local_dir=str(self.root)))
|
| 111 |
+
return p
|
| 112 |
+
def publish(self,files,message):
|
| 113 |
+
if not self.online:return
|
| 114 |
+
with self.lock:
|
| 115 |
+
operations=[CommitOperationAdd(path_in_repo=n,path_or_fileobj=str(p)) for n,p in files.items()]
|
| 116 |
+
result=self.api.create_commit(repo_id=self.repo,repo_type='dataset',operations=operations,
|
| 117 |
+
parent_commit=self.head,commit_message=message)
|
| 118 |
+
self.head=result.oid;self.remote.update(files)
|
| 119 |
+
def cortex_pool(self):
|
| 120 |
+
workers=self.cfg.get('prepared_workers',1)
|
| 121 |
+
if workers<=1:return None
|
| 122 |
+
if self.pool is None:
|
| 123 |
+
import multiprocessing as mp
|
| 124 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 125 |
+
self.pool=ProcessPoolExecutor(max_workers=workers,mp_context=mp.get_context('spawn'),initializer=_worker_init)
|
| 126 |
+
return self.pool
|
| 127 |
+
def touch_table(self,part):
|
| 128 |
+
self.tables.pop(part.prefix,None);self.tables[part.prefix]=part
|
| 129 |
+
while len(self.tables)>self.cfg.get('prepared_memory_shards',6):
|
| 130 |
+
_,old=self.tables.popitem(last=False);old.table=None;old.table_number=None
|
| 131 |
+
def evict_disk(self):
|
| 132 |
+
if not self.online:return
|
| 133 |
+
budget=int(self.cfg.get('prepared_cache_gb',40)*1024**3)
|
| 134 |
+
files=list(self.root.glob('data/**/*.parquet'))+list(self.root.glob('data/**/cursor-*.pt'))
|
| 135 |
+
total=sum(p.stat().st_size for p in files)
|
| 136 |
+
for p in sorted(files,key=lambda p:p.stat().st_mtime):
|
| 137 |
+
if total<=budget:break
|
| 138 |
+
if p.relative_to(self.root).as_posix() in self.remote:
|
| 139 |
+
total-=p.stat().st_size;p.unlink()
|
| 140 |
+
def partition(self,name,spec,filename,limit,loader=None):
|
| 141 |
+
descriptor=dict(schema=SCHEMA,recipe=self.recipe,source=name,repo=spec['repo'],revision=spec['revision'],file=filename,context=limit)
|
| 142 |
+
key=sha(descriptor)
|
| 143 |
+
if key not in self.partitions:
|
| 144 |
+
single={**spec,'files':[filename]}
|
| 145 |
+
factory=lambda state:OneFile(name,single,state,limit,loader)
|
| 146 |
+
self.partitions[key]=Partition(self,'data/'+name+'/'+key,descriptor,factory,self.cfg.get('prepared_records_per_shard',32768))
|
| 147 |
+
return self.partitions[key]
|
| 148 |
+
def generated(self,kind,index,limit,codec):
|
| 149 |
+
count=65536;start=index//count*count # Canonical span is independent of physical shard tuning.
|
| 150 |
+
descriptor=dict(schema=SCHEMA,recipe=self.recipe,source='cortex',kind=kind,start=start,count=count,context=limit,codec=codec if kind=='visual' else 'none')
|
| 151 |
+
key=sha(descriptor)
|
| 152 |
+
if key not in self.partitions:
|
| 153 |
+
factory=lambda state:Generated(kind,start,count,limit,codec,state)
|
| 154 |
+
self.partitions[key]=Partition(self,'data/cortex/'+key,descriptor,factory,self.cfg.get('cortex_records_per_shard',512))
|
| 155 |
+
return self.partitions[key].get(index-start)
|
| 156 |
+
def observe(self,run_id,lineage,step,path,events):
|
| 157 |
+
if not events:return
|
| 158 |
+
# Immutable, checkpoint-identified presentation log. The same update's
|
| 159 |
+
# retry/resume reuses the same ledger path and never appends corpus rows.
|
| 160 |
+
relative=f'observed/{run_id}/{lineage}/step-{step:012d}.json';dest=self.root/relative
|
| 161 |
+
value=dict(schema=SCHEMA,run_id=run_id,lineage=lineage,through_step=step,
|
| 162 |
+
model_checkpoint_sha256=digest(Path(path) if Path(path).is_file() else Path(path)/'training.pt'),recipe=self.recipe,
|
| 163 |
+
prepared_revision=self.head,events=events,
|
| 164 |
+
presentations=sum(len(e['sample_ids']) for e in events),
|
| 165 |
+
unique_ids_in_segment=len({s for e in events for s in e['sample_ids']}))
|
| 166 |
+
atomic_json(dest,value);self.publish({relative:dest},f'{run_id}: observed sample references through update {step}')
|
| 167 |
+
def close(self):
|
| 168 |
+
for p in self.partitions.values():p.close()
|
| 169 |
+
if self.pool is not None:self.pool.shutdown(wait=True,cancel_futures=True);self.pool=None
|
| 170 |
+
|
| 171 |
+
class Partition:
|
| 172 |
+
def __init__(self,archive,prefix,descriptor,factory,chunk):
|
| 173 |
+
self.a,self.prefix,self.descriptor,self.factory,self.chunk=archive,prefix,descriptor,factory,chunk
|
| 174 |
+
self.root=archive.root/prefix;self.root.mkdir(parents=True,exist_ok=True)
|
| 175 |
+
self.index=self.root/'index.json';self.builder=None;self.table=None;self.table_number=None
|
| 176 |
+
rel=prefix+'/index.json'
|
| 177 |
+
if rel in archive.remote:
|
| 178 |
+
# A fresh run reads the remote catalog; existing local progress may
|
| 179 |
+
# be newer after an upload failure and is republished before use.
|
| 180 |
+
remote_path=hf_hub_download(archive.repo,rel,repo_type='dataset',revision=archive.head,cache_dir=str(archive.root/'index_cache'))
|
| 181 |
+
remote=json.loads(Path(remote_path).read_text())
|
| 182 |
+
local=json.loads(self.index.read_text()) if self.index.exists() else None
|
| 183 |
+
if local is None or remote['count']>local['count']:atomic_json(self.index,remote)
|
| 184 |
+
self.meta=json.loads(self.index.read_text()) if self.index.exists() else dict(descriptor=descriptor,count=0,complete=False,shards=[])
|
| 185 |
+
if self.meta['descriptor']!=descriptor:raise RuntimeError('Prepared-data recipe mismatch')
|
| 186 |
+
def _builder(self):
|
| 187 |
+
if self.builder is None:
|
| 188 |
+
state=None
|
| 189 |
+
if self.meta['shards']:
|
| 190 |
+
tail=self.meta['shards'][-1];p=self.a.fetch(self.prefix+'/'+tail['state'])
|
| 191 |
+
if digest(p)!=tail['state_sha']:raise ValueError('Prepared source cursor checksum mismatch')
|
| 192 |
+
state=torch.load(p,map_location='cpu',weights_only=True)
|
| 193 |
+
self.builder=self.factory(state);self.builder.initialize()
|
| 194 |
+
return self.builder
|
| 195 |
+
def prepare(self):
|
| 196 |
+
if self.meta['complete']:return
|
| 197 |
+
builder=self._builder();rows=[];finished=False
|
| 198 |
+
# Read a large bounded block once. Cortex CPU work can run in spawned
|
| 199 |
+
# workers without forking an initialized CUDA runtime.
|
| 200 |
+
generated=iter(builder.records(self.chunk,self.a.cortex_pool())) if isinstance(builder,Generated) else None
|
| 201 |
+
for _ in range(self.chunk):
|
| 202 |
+
try:r=next(generated) if generated is not None else builder.next_record()
|
| 203 |
+
except (EndFile,StopIteration):finished=True;break
|
| 204 |
+
cursor=copy.deepcopy(builder.s['cursor']);offset=r.get('meta',{}).get('chunk_offset',0)
|
| 205 |
+
identity=sha(dict(partition=self.descriptor,cursor=cursor,offset=offset))
|
| 206 |
+
meta={**r.get('meta',{}),'sample_id':identity,
|
| 207 |
+
'prepared_ref':self.prefix+'#'+str(self.meta['count']+len(rows)),
|
| 208 |
+
'origin':self.descriptor}
|
| 209 |
+
rows.append(dict(sample_id=identity,source=r['source'],ids=r['ids'],weights=r['weights'],prompt_len=r['prompt_len'],
|
| 210 |
+
metadata=json.dumps(meta,sort_keys=True),cursor=json.dumps(cursor,sort_keys=True),row_ordinal=builder.s['rows'],last_chunk=builder.s.get('pending_document') is None))
|
| 211 |
+
if isinstance(builder,Generated) and builder.s['index']>=builder.count:finished=True
|
| 212 |
+
number=len(self.meta['shards']);files={}
|
| 213 |
+
if rows:
|
| 214 |
+
name=f'part-{number:06d}.parquet';dest=self.root/name
|
| 215 |
+
schema=pa.schema([('sample_id',pa.string()),('source',pa.string()),('ids',pa.list_(pa.uint16())),('weights',pa.list_(pa.uint8())),
|
| 216 |
+
('prompt_len',pa.int32()),('metadata',pa.string()),('cursor',pa.string()),('row_ordinal',pa.int64()),('last_chunk',pa.bool_())])
|
| 217 |
+
temp=dest.with_suffix('.tmp');pq.write_table(pa.Table.from_pylist(rows,schema=schema),temp,compression='zstd',row_group_size=1024);os.replace(temp,dest)
|
| 218 |
+
state_name=f'cursor-{number:06d}.pt';state_path=self.root/state_name
|
| 219 |
+
temp=state_path.with_suffix('.tmp');torch.save(builder.state_dict(),temp);os.replace(temp,state_path)
|
| 220 |
+
self.meta['shards'].append(dict(file=name,start=self.meta['count'],count=len(rows),sha256=digest(dest),
|
| 221 |
+
state=state_name,state_sha=digest(state_path),first_cursor=json.loads(rows[0]['cursor']),last_cursor=json.loads(rows[-1]['cursor'])))
|
| 222 |
+
self.meta['count']+=len(rows);self.a.prepared_records+=len(rows)
|
| 223 |
+
files.update({self.prefix+'/'+name:dest,self.prefix+'/'+state_name:state_path})
|
| 224 |
+
self.meta['complete']=finished;atomic_json(self.index,self.meta);files[self.prefix+'/index.json']=self.index
|
| 225 |
+
self.a.publish(files,f'Prepare {self.descriptor["source"]}: {self.meta["count"]} unique records')
|
| 226 |
+
self.a.evict_disk()
|
| 227 |
+
def _load(self,number):
|
| 228 |
+
shard=self.meta['shards'][number]
|
| 229 |
+
if self.table_number!=number:
|
| 230 |
+
p=self.a.fetch(self.prefix+'/'+shard['file'])
|
| 231 |
+
if digest(p)!=shard['sha256']:raise ValueError('Prepared record shard checksum mismatch')
|
| 232 |
+
self.table=pq.read_table(p);self.table_number=number
|
| 233 |
+
# Recover a locally completed but not yet remotely committed shard.
|
| 234 |
+
if self.a.online and self.prefix+'/'+shard['file'] not in self.a.remote:
|
| 235 |
+
names=[shard['file'],shard['state'],'index.json']
|
| 236 |
+
self.a.publish({self.prefix+'/'+n:self.root/n for n in names},'Recover prepared shard upload')
|
| 237 |
+
self.a.touch_table(self)
|
| 238 |
+
return self.table
|
| 239 |
+
def get(self,index):
|
| 240 |
+
while index>=self.meta['count'] and not self.meta['complete']:self.prepare()
|
| 241 |
+
if index>=self.meta['count']:raise EndFile
|
| 242 |
+
number=bisect.bisect_right([s['start'] for s in self.meta['shards']],index)-1
|
| 243 |
+
table=self._load(number);row=table.slice(index-self.meta['shards'][number]['start'],1).to_pylist()[0]
|
| 244 |
+
return dict(ids=row['ids'],weights=row['weights'],source=row['source'],prompt_len=row['prompt_len'],meta={**json.loads(row['metadata']),'prepared_ref':self.prefix+'#'+str(index)},
|
| 245 |
+
prepared_cursor=json.loads(row['cursor']),prepared_last=row['last_chunk'])
|
| 246 |
+
def after_cursor(self,cursor):
|
| 247 |
+
if cursor is None:return 0
|
| 248 |
+
i=0
|
| 249 |
+
while True:
|
| 250 |
+
try:r=self.get(i)
|
| 251 |
+
except EndFile:return self.meta['count']
|
| 252 |
+
if cursor_key(r['prepared_cursor'])>cursor_key(cursor):return i
|
| 253 |
+
i+=1
|
| 254 |
+
def row_records(self,cursor):
|
| 255 |
+
out=[];i=0;target=cursor_key(cursor)
|
| 256 |
+
while True:
|
| 257 |
+
try:r=self.get(i)
|
| 258 |
+
except EndFile:break
|
| 259 |
+
current=cursor_key(r['prepared_cursor'])
|
| 260 |
+
if current>target:break
|
| 261 |
+
if current==target:out.append(r)
|
| 262 |
+
i+=1
|
| 263 |
+
if not out:raise ValueError('Legacy cursor not found in its pinned prepared shard')
|
| 264 |
+
return out
|
| 265 |
+
def close(self):
|
| 266 |
+
if self.builder:self.builder.close()
|
| 267 |
+
self.builder=None;self.table=None;self.table_number=None
|
| 268 |
+
|
| 269 |
+
class PreparedSource(CoverageSource):
|
| 270 |
+
def __init__(self,*args,archive,**kwargs):
|
| 271 |
+
super().__init__(*args,**kwargs);self.archive=archive
|
| 272 |
+
def initialize(self):
|
| 273 |
+
super().initialize();self.s.setdefault('prepared_position',None)
|
| 274 |
+
def partition(self):return self.archive.partition(self.name,self.spec,self.spec['files'][self.s['shard']],self.limit,self.loader)
|
| 275 |
+
def identify_borrowed(self,r):
|
| 276 |
+
if r.get('meta',{}).get('sample_id'):return r
|
| 277 |
+
rows=self.partition().row_records(self.s['cursor'])
|
| 278 |
+
doc=self.s.get('pending_document')
|
| 279 |
+
idx=(doc['offset']//self.limit-1) if doc else len(rows)-len(self.s['pending'])-1
|
| 280 |
+
candidate=rows[idx]
|
| 281 |
+
if candidate['ids']!=r['ids'] or candidate['weights']!=r['weights']:raise ValueError('Legacy buffered record differs from canonical prepared record')
|
| 282 |
+
return candidate
|
| 283 |
+
def next_record(self,excluded=()):
|
| 284 |
+
if self.s['pending']:
|
| 285 |
+
rows=self.partition().row_records(self.s['cursor']);r=rows[-len(self.s['pending'])];old=self.s['pending'].pop(0)
|
| 286 |
+
if r['ids']!=old['ids']:raise ValueError('Legacy pending chunk mismatch')
|
| 287 |
+
return r
|
| 288 |
+
if self.s.get('pending_document'):
|
| 289 |
+
rows=self.partition().row_records(self.s['cursor']);r=rows[self.s['pending_document']['offset']//self.limit]
|
| 290 |
+
old=super()._chunk()
|
| 291 |
+
if old['ids']!=r['ids']:raise ValueError('Legacy partial-document mismatch')
|
| 292 |
+
return r
|
| 293 |
+
visited=0
|
| 294 |
+
while True:
|
| 295 |
+
part=self.partition()
|
| 296 |
+
if self.s['prepared_position'] is None:self.s['prepared_position']=part.after_cursor(self.s['cursor'])
|
| 297 |
+
try:r=part.get(self.s['prepared_position'])
|
| 298 |
+
except EndFile:
|
| 299 |
+
visited+=1
|
| 300 |
+
if visited>len(self.spec['files']):raise RuntimeError(self.name+': empty prepared corpus')
|
| 301 |
+
self.s['shard']+=1;self.s['cursor']=None;self.s['prepared_position']=0
|
| 302 |
+
if self.s['shard']==len(self.spec['files']):self.s['shard']=0;self.s['epoch']+=1;self.s['pass_rows']=0
|
| 303 |
+
continue
|
| 304 |
+
if r['prepared_cursor']!=self.s['cursor']:
|
| 305 |
+
self.s['rows']+=1;self.s['pass_rows']+=1;self.s['accepted']+=1
|
| 306 |
+
self.s['cursor']=r['prepared_cursor'];self.s['prepared_position']+=1
|
| 307 |
+
self.s['documents_emitted']+=int(r['prepared_last'])
|
| 308 |
+
return r
|
| 309 |
+
|
| 310 |
+
class PreparedCortex(CodecCortex):
|
| 311 |
+
def __init__(self,*args,archive,**kwargs):super().__init__(*args,**kwargs);self.archive=archive
|
| 312 |
+
def record(self):
|
| 313 |
+
if self.codec=='none' or self.until_visual:
|
| 314 |
+
r=self.archive.generated('kernel',self.base.s['index'],self.limit,'none')
|
| 315 |
+
self.base.s['index']+=1;self.until_visual=max(0,self.until_visual-1);return r
|
| 316 |
+
r=self.archive.generated('visual',self.visual_index,self.limit,self.codec)
|
| 317 |
+
self.visual_index+=1;self.until_visual=3;return r
|
| 318 |
+
|
| 319 |
+
class PreparedMixer(CoverageMixer):
|
| 320 |
+
def __init__(self,manifest,limit=1024,state=None,loaders=None,group_weights=None,codec='none',archive=None):
|
| 321 |
+
super().__init__(manifest,limit,state,loaders,group_weights,codec);state=state or {};self.archive=archive
|
| 322 |
+
self.sources={n:PreparedSource(n,s,state.get('sources',{}).get(n),limit,(loaders or {}).get(n),archive=archive) for n,s in manifest.items()}
|
| 323 |
+
self.cortex=PreparedCortex(state.get('cortex'),limit,codec,archive=archive);self.batch_refs=[];self.batch_positions=[]
|
| 324 |
+
def initialize(self):
|
| 325 |
+
super().initialize()
|
| 326 |
+
for name,r in list(self.pending.items()):
|
| 327 |
+
if name in self.sources:self.pending[name]=self.sources[name].identify_borrowed(r)
|
| 328 |
+
elif name=='cortex' and not r.get('meta',{}).get('sample_id'):
|
| 329 |
+
# The already borrowed legacy Cortex record is the immediately
|
| 330 |
+
# preceding generator item. Match against both possible kinds.
|
| 331 |
+
candidates=[]
|
| 332 |
+
if self.cortex.base.s['index']>0:candidates.append(self.archive.generated('kernel',self.cortex.base.s['index']-1,self.limit,'none'))
|
| 333 |
+
if self.cortex.codec!='none' and self.cortex.visual_index>0:candidates.append(self.archive.generated('visual',self.cortex.visual_index-1,self.limit,self.cortex.codec))
|
| 334 |
+
matches=[x for x in candidates if x['ids']==r['ids'] and x['weights']==r['weights']]
|
| 335 |
+
if not matches:raise ValueError('Legacy Cortex buffer could not be identified')
|
| 336 |
+
self.pending[name]=matches[0]
|
| 337 |
+
return self
|
| 338 |
+
def block(self):
|
| 339 |
+
if not self.mix_queue:self._refill_mix_queue()
|
| 340 |
+
name=self.mix_queue.pop(0);tokens=[];weights=[];refs=[];positions=[]
|
| 341 |
+
while True:
|
| 342 |
+
r=self.next_for(name);validate(r,self.limit)
|
| 343 |
+
if tokens and len(tokens)+len(r['ids'])>self.limit+1:self.pending[name]=r;break
|
| 344 |
+
tokens.extend(r['ids']);weights.extend(r['weights']);refs.append(r['meta']['sample_id']);positions.append(r['meta']['prepared_ref'])
|
| 345 |
+
if len(tokens)==self.limit+1 or len(tokens)>=int(self.limit*.90):break
|
| 346 |
+
used=len(tokens);padding=self.limit+1-used;tokens.extend([PAD]*padding);weights.extend([0]*padding)
|
| 347 |
+
self.stats[name+'/blocks']+=1;self.stats[name+'/targets']+=sum(weights[1:]);self.stats[name+'/tokens']+=used-1
|
| 348 |
+
self.batch_refs.extend(refs);self.batch_positions.extend(positions)
|
| 349 |
+
return tokens,weights,name,used
|
| 350 |
+
def batch(self,count):
|
| 351 |
+
self.batch_refs=[];self.batch_positions=[];data=super().batch(count);data['sample_ids']=list(self.batch_refs);data['sample_refs']=list(self.batch_positions);return data
|
| 352 |
+
|
| 353 |
+
class ShardPrefetcher:
|
| 354 |
+
"""One CPU producer, bounded disk dispatch shards, exact consumer snapshots."""
|
| 355 |
+
def __init__(self,mixer,cfg,run_id,start_step,remaining=None,variant='sparkbet',codec='none',mapper=None,observations=None):
|
| 356 |
+
self.mixer,self.cfg,self.archive=mixer,cfg,mixer.archive;self.manifest=mixer.manifest
|
| 357 |
+
self.delivered=mixer.state_dict();self.initial_step=start_step;self.variant=variant;self.codec=codec;self.mapper=mapper
|
| 358 |
+
self.root=Path(cfg['work_dir'])/run_id/'dispatch';self.root.mkdir(parents=True,exist_ok=True)
|
| 359 |
+
self.depth=cfg.get('prefetch_shards',2);self.per=cfg.get('dispatch_batches_per_shard',16)
|
| 360 |
+
self.queue=queue.Queue(maxsize=self.depth);self.stop_event=threading.Event();self.ready=threading.Condition()
|
| 361 |
+
self.made=0;self.error=None;self.done=False;self.current=[];self.cursor=0;self.current_path=None;self.pending_event=None;self.events=[]
|
| 362 |
+
self.remaining=remaining;self.panel=mixer.validation()
|
| 363 |
+
self.observations=copy.deepcopy(observations or dict(presentations=0,unique_samples=0,ranges={}))
|
| 364 |
+
for old in self.root.glob('dispatch-*.pt'):old.unlink()
|
| 365 |
+
self.worker=threading.Thread(target=self._produce,name='rewrite7-shard-producer',daemon=True);self.worker.start()
|
| 366 |
+
def _produce(self):
|
| 367 |
+
produced=0;number=0
|
| 368 |
+
try:
|
| 369 |
+
while not self.stop_event.is_set() and (self.remaining is None or produced<self.remaining):
|
| 370 |
+
# Backpressure BEFORE construction bounds the extra in-flight shard.
|
| 371 |
+
while self.queue.full() and not self.stop_event.wait(.1):pass
|
| 372 |
+
if self.stop_event.is_set():break
|
| 373 |
+
entries=[]
|
| 374 |
+
for _ in range(self.per):
|
| 375 |
+
if self.stop_event.is_set() or (self.remaining is not None and produced>=self.remaining):break
|
| 376 |
+
data=self.mixer.batch(self.cfg['global_batch']);step=self.initial_step+produced
|
| 377 |
+
if self.mapper:data['canvas']=self.mapper(data,step)
|
| 378 |
+
# Torch serialization contains tensors/primitive state only.
|
| 379 |
+
data['ids']=torch.from_numpy(data['ids']);data['weights']=torch.from_numpy(data['weights'])
|
| 380 |
+
entries.append(dict(data=data,after=self.mixer.state_dict(),step=step));produced+=1
|
| 381 |
+
if not entries:break
|
| 382 |
+
path=self.root/f'dispatch-{self.initial_step+produced-len(entries):012d}.pt';tmp=path.with_suffix('.tmp')
|
| 383 |
+
torch.save(entries,tmp);os.replace(tmp,path)
|
| 384 |
+
while not self.stop_event.is_set():
|
| 385 |
+
try:self.queue.put(path,timeout=.1);break
|
| 386 |
+
except queue.Full:pass
|
| 387 |
+
number+=1
|
| 388 |
+
with self.ready:self.made=number;self.ready.notify_all()
|
| 389 |
+
with self.ready:self.done=True;self.ready.notify_all()
|
| 390 |
+
except BaseException as e:
|
| 391 |
+
self.error=(e,traceback.format_exc())
|
| 392 |
+
with self.ready:self.done=True;self.ready.notify_all()
|
| 393 |
+
finally:self.mixer.close()
|
| 394 |
+
def prewarm(self):
|
| 395 |
+
target=self.depth if self.remaining is None else min(self.depth,math.ceil(self.remaining/self.per))
|
| 396 |
+
began=time.monotonic();last=began
|
| 397 |
+
with self.ready:
|
| 398 |
+
while self.made<target and not self.done:
|
| 399 |
+
self.ready.wait(timeout=5)
|
| 400 |
+
if time.monotonic()-last>=30:print(f'Preparing rewrite7 startup buffer: {self.made}/{target} dispatch shards ready',flush=True);last=time.monotonic()
|
| 401 |
+
self._raise();self.preparation_seconds=time.monotonic()-began
|
| 402 |
+
print(f'rewrite7 startup buffer ready: {self.made} shards, {self.preparation_seconds:.1f}s',flush=True)
|
| 403 |
+
def _raise(self):
|
| 404 |
+
if self.error:raise RuntimeError('Background shard preparation failed; committed model cursor is unchanged.\n'+self.error[1]) from self.error[0]
|
| 405 |
+
def batch(self,count):
|
| 406 |
+
if count!=self.cfg['global_batch']:raise ValueError('Prefetch logical batch changed')
|
| 407 |
+
if self.pending_event is not None:raise RuntimeError('Commit the previous successful batch before advancing')
|
| 408 |
+
if self.cursor==len(self.current):
|
| 409 |
+
if self.current_path:self.current_path.unlink(missing_ok=True)
|
| 410 |
+
while True:
|
| 411 |
+
self._raise()
|
| 412 |
+
try:self.current_path=self.queue.get(timeout=.5);break
|
| 413 |
+
except queue.Empty:
|
| 414 |
+
if self.done:raise StopIteration('Prepared finite run exhausted')
|
| 415 |
+
self.current=torch.load(self.current_path,map_location='cpu',weights_only=True);self.cursor=0
|
| 416 |
+
entry=self.current[self.cursor];self.cursor+=1;data=entry['data']
|
| 417 |
+
data={**data,'ids':data['ids'].numpy(),'weights':data['weights'].numpy()}
|
| 418 |
+
self.pending_after=entry['after'];self.pending_event=dict(update=entry['step']+1,sample_ids=data['sample_ids'],sample_refs=data['sample_refs'],
|
| 419 |
+
canvas_seed=self.cfg.get('seed'),canvas_step=entry['step'] if self.mapper else None,variant=self.variant,codec=self.codec)
|
| 420 |
+
return data
|
| 421 |
+
def commit_batch(self):
|
| 422 |
+
if self.pending_event is None:raise RuntimeError('No prepared batch to commit')
|
| 423 |
+
self.delivered=self.pending_after
|
| 424 |
+
for ref in self.pending_event['sample_refs']:
|
| 425 |
+
prefix,position=ref.rsplit('#',1);position=int(position)
|
| 426 |
+
ranges=self.observations['ranges'].setdefault(prefix,[])
|
| 427 |
+
if not any(a<=position<b for a,b in ranges):
|
| 428 |
+
ranges.append([position,position+1]);ranges.sort();merged=[]
|
| 429 |
+
for a,b in ranges:
|
| 430 |
+
if merged and a<=merged[-1][1]:merged[-1][1]=max(merged[-1][1],b)
|
| 431 |
+
else:merged.append([a,b])
|
| 432 |
+
self.observations['ranges'][prefix]=merged;self.observations['unique_samples']+=1
|
| 433 |
+
self.observations['presentations']+=1
|
| 434 |
+
self.events.append(self.pending_event);self.pending_event=None
|
| 435 |
+
def state_dict(self):return copy.deepcopy(self.delivered)
|
| 436 |
+
def validation(self):return self.panel
|
| 437 |
+
def coverage(self):
|
| 438 |
+
return {n:dict(epoch=s['epoch'],shard=s['shard'],shards=len(self.manifest[n]['files']),rows_read=s['rows'],
|
| 439 |
+
prepared_position=s.get('prepared_position'),documents_emitted=s.get('documents_emitted',0)) for n,s in self.delivered['sources'].items()}
|
| 440 |
+
def flush_observed(self,run_id,lineage,step,path):
|
| 441 |
+
self.archive.observe(run_id,lineage,step,path,self.events);self.events=[]
|
| 442 |
+
def close(self):
|
| 443 |
+
self.stop_event.set();self.worker.join(timeout=30)
|
| 444 |
+
if self.worker.is_alive():raise RuntimeError('Background preparation has not stopped; retry after its current network operation ends')
|
| 445 |
+
self.archive.close()
|
unified/ablations/unified-v1-blt-none-s42/source/records.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Direct UTF-8 bytes. Special IDs match BET, not the old Cortex tokenizer."""
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
PAD,BOS,EOS=256,257,258
|
| 4 |
+
|
| 5 |
+
class Oversize(ValueError):pass
|
| 6 |
+
class InvalidRecord(ValueError):pass
|
| 7 |
+
|
| 8 |
+
def ids(text):return list(text.encode('utf-8'))
|
| 9 |
+
def decode(tokens):return bytes(t for t in tokens if 0<=t<256).decode('utf-8',errors='replace')
|
| 10 |
+
|
| 11 |
+
def record(prefix,answer,source,limit=1024,supervise_all=False,meta=None):
|
| 12 |
+
p,a=ids(prefix),ids(answer)
|
| 13 |
+
if not a:raise InvalidRecord('Empty target: '+source)
|
| 14 |
+
tokens=[BOS]+p+a+[EOS]
|
| 15 |
+
if len(tokens)>limit+1:raise Oversize(f'{source}: {len(tokens)} IDs exceeds {limit+1}; no truncation')
|
| 16 |
+
weights=[0]+([1]*len(p) if supervise_all else [0]*len(p))+[1]*(len(a)+1)
|
| 17 |
+
return dict(ids=tokens,weights=weights,source=source,prompt_len=1+len(p),meta=meta or {})
|
| 18 |
+
|
| 19 |
+
def plain_chunks(text,source,limit=1024):
|
| 20 |
+
# Lossless bytes, including split UTF-8 sequences: decoder assembles the byte stream.
|
| 21 |
+
# No false EOS at chunk boundaries. One-token overlap predicts each byte once.
|
| 22 |
+
if not isinstance(text,str) or not text.strip():raise InvalidRecord('Empty/non-string text: '+source)
|
| 23 |
+
raw_bytes=text.encode('utf-8')
|
| 24 |
+
if len(raw_bytes)>1024*1024:raise Oversize('Document exceeds the 1 MiB bounded-buffer limit; rejected intact')
|
| 25 |
+
raw=[BOS]+list(raw_bytes)+[EOS];out=[]
|
| 26 |
+
for offset in range(0,len(raw)-1,limit):
|
| 27 |
+
chunk=raw[offset:offset+limit+1]
|
| 28 |
+
out.append(dict(ids=chunk,weights=[0]+[1]*(len(chunk)-1),source=source,prompt_len=1,meta={}))
|
| 29 |
+
return out
|
| 30 |
+
|
| 31 |
+
def validate(r,limit=1024):
|
| 32 |
+
assert 2<=len(r['ids'])<=limit+1
|
| 33 |
+
assert len(r['ids'])==len(r['weights'])
|
| 34 |
+
assert all(type(x)==int and 0<=x<259 for x in r['ids'])
|
| 35 |
+
assert all(x in (0,1) for x in r['weights']) and sum(r['weights'][1:])>0
|
| 36 |
+
assert r['weights'][0]==0
|
| 37 |
+
return r
|
unified/ablations/unified-v1-blt-none-s42/source/tokenization_bet.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from transformers import PreTrainedTokenizer
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
BYTE_PREFIX = "<0x"
|
| 7 |
+
PAD_TOKEN = "<pad>"
|
| 8 |
+
BOS_TOKEN = "<bos>"
|
| 9 |
+
EOS_TOKEN = "<eos>"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class BETByteTokenizer(PreTrainedTokenizer):
|
| 13 |
+
"""Lossless UTF-8 byte tokenizer used by BET.
|
| 14 |
+
|
| 15 |
+
IDs:
|
| 16 |
+
0..255 -> raw byte values
|
| 17 |
+
256 -> PAD
|
| 18 |
+
257 -> BOS
|
| 19 |
+
258 -> EOS
|
| 20 |
+
|
| 21 |
+
No UNK token is required because every UTF-8 string is representable as bytes.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
vocab_files_names = {"vocab_file": "byte_vocab.json"}
|
| 25 |
+
model_input_names = ["input_ids", "attention_mask"]
|
| 26 |
+
|
| 27 |
+
def __init__(
|
| 28 |
+
self,
|
| 29 |
+
vocab_file=None,
|
| 30 |
+
pad_token=PAD_TOKEN,
|
| 31 |
+
bos_token=BOS_TOKEN,
|
| 32 |
+
eos_token=EOS_TOKEN,
|
| 33 |
+
unk_token=None,
|
| 34 |
+
model_max_length=1024,
|
| 35 |
+
padding_side="left",
|
| 36 |
+
clean_up_tokenization_spaces=False,
|
| 37 |
+
**kwargs,
|
| 38 |
+
):
|
| 39 |
+
# Transformers v5 loads values from tokenizer_config.json into this
|
| 40 |
+
# constructor. Make every value that we also forward to PythonBackend
|
| 41 |
+
# an explicit argument so it is consumed exactly once instead of being
|
| 42 |
+
# duplicated inside **kwargs.
|
| 43 |
+
self.vocab_file = vocab_file
|
| 44 |
+
kwargs.setdefault("split_special_tokens",True)
|
| 45 |
+
super().__init__(
|
| 46 |
+
pad_token=pad_token,
|
| 47 |
+
bos_token=bos_token,
|
| 48 |
+
eos_token=eos_token,
|
| 49 |
+
unk_token=unk_token,
|
| 50 |
+
model_max_length=model_max_length,
|
| 51 |
+
padding_side=padding_side,
|
| 52 |
+
clean_up_tokenization_spaces=clean_up_tokenization_spaces,
|
| 53 |
+
**kwargs,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def vocab_size(self):
|
| 58 |
+
return 259
|
| 59 |
+
|
| 60 |
+
def get_vocab(self):
|
| 61 |
+
vocab = {f"<0x{i:02X}>": i for i in range(256)}
|
| 62 |
+
vocab[PAD_TOKEN] = 256
|
| 63 |
+
vocab[BOS_TOKEN] = 257
|
| 64 |
+
vocab[EOS_TOKEN] = 258
|
| 65 |
+
return vocab
|
| 66 |
+
|
| 67 |
+
def _tokenize(self, text, **kwargs):
|
| 68 |
+
return [f"<0x{b:02X}>" for b in text.encode("utf-8", errors="replace")]
|
| 69 |
+
|
| 70 |
+
def _convert_token_to_id(self, token):
|
| 71 |
+
if token == PAD_TOKEN:
|
| 72 |
+
return 256
|
| 73 |
+
if token == BOS_TOKEN:
|
| 74 |
+
return 257
|
| 75 |
+
if token == EOS_TOKEN:
|
| 76 |
+
return 258
|
| 77 |
+
if isinstance(token, str) and token.startswith(BYTE_PREFIX) and token.endswith(">"):
|
| 78 |
+
try:
|
| 79 |
+
value = int(token[3:-1], 16)
|
| 80 |
+
if 0 <= value <= 255:
|
| 81 |
+
return value
|
| 82 |
+
except ValueError:
|
| 83 |
+
pass
|
| 84 |
+
# This branch should be unreachable for text encoded by this tokenizer.
|
| 85 |
+
return 0
|
| 86 |
+
|
| 87 |
+
def _convert_id_to_token(self, index):
|
| 88 |
+
index = int(index)
|
| 89 |
+
if 0 <= index <= 255:
|
| 90 |
+
return f"<0x{index:02X}>"
|
| 91 |
+
if index == 256:
|
| 92 |
+
return PAD_TOKEN
|
| 93 |
+
if index == 257:
|
| 94 |
+
return BOS_TOKEN
|
| 95 |
+
if index == 258:
|
| 96 |
+
return EOS_TOKEN
|
| 97 |
+
return "<0x00>"
|
| 98 |
+
|
| 99 |
+
def convert_tokens_to_string(self, tokens):
|
| 100 |
+
out = []
|
| 101 |
+
buf = bytearray()
|
| 102 |
+
|
| 103 |
+
def flush():
|
| 104 |
+
nonlocal buf
|
| 105 |
+
if buf:
|
| 106 |
+
out.append(bytes(buf).decode("utf-8", errors="replace"))
|
| 107 |
+
buf = bytearray()
|
| 108 |
+
|
| 109 |
+
for token in tokens:
|
| 110 |
+
idx = self._convert_token_to_id(token)
|
| 111 |
+
if isinstance(token, str) and 0 <= idx <= 255 and token.startswith(BYTE_PREFIX):
|
| 112 |
+
buf.append(idx)
|
| 113 |
+
else:
|
| 114 |
+
flush()
|
| 115 |
+
out.append(str(token))
|
| 116 |
+
flush()
|
| 117 |
+
return "".join(out)
|
| 118 |
+
|
| 119 |
+
def build_inputs_with_special_tokens(self, token_ids_0, token_ids_1=None):
|
| 120 |
+
# BET pretraining did not automatically insert BOS/EOS around ordinary text.
|
| 121 |
+
if token_ids_1 is None:
|
| 122 |
+
return list(token_ids_0)
|
| 123 |
+
return list(token_ids_0) + list(token_ids_1)
|
| 124 |
+
|
| 125 |
+
def create_token_type_ids_from_sequences(self, token_ids_0, token_ids_1=None):
|
| 126 |
+
n = len(token_ids_0) + (len(token_ids_1) if token_ids_1 is not None else 0)
|
| 127 |
+
return [0] * n
|
| 128 |
+
|
| 129 |
+
def save_vocabulary(self, save_directory, filename_prefix=None):
|
| 130 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 131 |
+
name = "byte_vocab.json" if filename_prefix is None else f"{filename_prefix}-byte_vocab.json"
|
| 132 |
+
path = os.path.join(save_directory, name)
|
| 133 |
+
vocab = {f"<0x{i:02X}>": i for i in range(256)}
|
| 134 |
+
vocab.update({PAD_TOKEN: 256, BOS_TOKEN: 257, EOS_TOKEN: 258})
|
| 135 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 136 |
+
json.dump(vocab, f, indent=2, sort_keys=True)
|
| 137 |
+
return (path,)
|
unified/ablations/unified-v1-blt-none-s42/source/trainer.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Continuous SparkBET training on one GPU or torchrun DDP, always FP16 autocast."""
|
| 2 |
+
import os,json,time,math,random,signal,gc,uuid,shutil,fcntl,subprocess,sys,traceback
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from datetime import timedelta
|
| 5 |
+
from contextlib import nullcontext
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
from torch.nn.parallel import DistributedDataParallel as DDP
|
| 10 |
+
from bet_model import (
|
| 11 |
+
BETFog,BETConfig,uniform_steps,verify_architecture,
|
| 12 |
+
set_gradient_checkpointing,set_state_noise_sigma,
|
| 13 |
+
)
|
| 14 |
+
from records import decode
|
| 15 |
+
from data_stream import Mixer
|
| 16 |
+
from checkpoints import FORMAT,save,resume,legacy_import,pipeline_hash,atomic_json,Publisher
|
| 17 |
+
from dataset_registry import make_manifest
|
| 18 |
+
|
| 19 |
+
ROOT=Path(__file__).resolve().parent
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def lr_at(step,cfg):
|
| 23 |
+
"""Warm up once, then keep learning indefinitely; no terminal update exists."""
|
| 24 |
+
warm=cfg['warmup_steps']
|
| 25 |
+
return cfg['lr']*min(1.0,(step+1)/max(1,warm))
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def loss_sum(logits,target,mask):
|
| 29 |
+
# FP32 cross entropy even when matmuls use FP16.
|
| 30 |
+
losses=torch.nn.functional.cross_entropy(
|
| 31 |
+
logits.float().flatten(0,1),target.reshape(-1),reduction='none'
|
| 32 |
+
).view_as(mask)
|
| 33 |
+
return (losses*mask).sum()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def scaler_new(enabled=True):
|
| 37 |
+
# FP16 + GradScaler is deliberately the one CUDA precision contract on T4,
|
| 38 |
+
# A10/A10G and RTX PRO 6000 so hardware changes do not change numerics by policy.
|
| 39 |
+
return torch.amp.GradScaler('cuda',enabled=enabled and torch.cuda.is_available(),init_scale=1024,growth_interval=2000)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def optimizer_new(model,cfg):
|
| 43 |
+
return torch.optim.AdamW(model.parameters(),lr=cfg['lr'],betas=(.9,.95),eps=1e-8,weight_decay=.1)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def broadcast_object(value,rank,world):
|
| 47 |
+
if world==1:return value
|
| 48 |
+
box=[value if rank==0 else None];dist.broadcast_object_list(box,src=0);return box[0]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def coordinator_call(fn,rank,world):
|
| 52 |
+
"""Broadcast rank-zero errors before peers enter the next collective."""
|
| 53 |
+
result=None;error=None
|
| 54 |
+
if rank==0:
|
| 55 |
+
try:result=fn()
|
| 56 |
+
except Exception as exc:error=f'{type(exc).__name__}: {exc}'
|
| 57 |
+
error=broadcast_object(error,rank,world)
|
| 58 |
+
if error:raise RuntimeError('Coordinator operation failed: '+error)
|
| 59 |
+
return result
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def all_true(flag,device,world):
|
| 63 |
+
value=torch.tensor(int(flag),device=device,dtype=torch.int32)
|
| 64 |
+
if world>1:dist.all_reduce(value,op=dist.ReduceOp.MIN)
|
| 65 |
+
return bool(value.item())
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def amp(device):
|
| 69 |
+
return torch.autocast(device.type,dtype=torch.float16,enabled=device.type=='cuda')
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def rng_state():
|
| 73 |
+
return dict(
|
| 74 |
+
python=random.getstate(),torch=torch.get_rng_state(),
|
| 75 |
+
cuda=torch.cuda.get_rng_state().cpu() if torch.cuda.is_available() else None,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def restore_rng(s):
|
| 80 |
+
random.setstate(s['python']);torch.set_rng_state(s['torch'])
|
| 81 |
+
if s['cuda'] is not None and torch.cuda.is_available():torch.cuda.set_rng_state(s['cuda'])
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def auxiliary_stage(update_step,cfg):
|
| 85 |
+
"""Sustained fresh-data stages: L8+Lr for base_updates*r optimizer updates.
|
| 86 |
+
|
| 87 |
+
One optimizer update fetches exactly one *new batch draw* from the mixer.
|
| 88 |
+
Batches may revisit finite data across epochs, but do not repeat a cached
|
| 89 |
+
mini-batch merely to inflate the number of stage steps. The selected depth
|
| 90 |
+
is independent of microbatch, DDP partitions, and FP16 overflow retries.
|
| 91 |
+
The origin anchors the new curriculum at an explicitly migrated checkpoint.
|
| 92 |
+
"""
|
| 93 |
+
lo=int(cfg.get('aux_loop_min',1));hi=int(cfg.get('aux_loop_max',7))
|
| 94 |
+
if not (1<=lo<=hi<8):raise ValueError('auxiliary loop range must stay inside L1..L7')
|
| 95 |
+
if cfg.get('aux_loop_schedule','progressive_data_stages_v2')!='progressive_data_stages_v2':
|
| 96 |
+
raise ValueError('Unsupported auxiliary schedule: expected progressive_data_stages_v2')
|
| 97 |
+
base=int(cfg.get('aux_stage_base_updates',128))
|
| 98 |
+
if base<1:raise ValueError('aux_stage_base_updates must be positive')
|
| 99 |
+
step=int(update_step);origin=int(cfg.get('aux_curriculum_origin_step',0))
|
| 100 |
+
if step<0 or origin<0 or step<origin:
|
| 101 |
+
raise ValueError('Curriculum step must be >= its nonnegative origin')
|
| 102 |
+
cycle_updates=base*sum(range(lo,hi+1))
|
| 103 |
+
cycle,phase=divmod(step-origin,cycle_updates)
|
| 104 |
+
for depth in range(lo,hi+1):
|
| 105 |
+
stage_updates=base*depth
|
| 106 |
+
if phase<stage_updates:
|
| 107 |
+
return dict(depth=depth,cycle=cycle,cycle_updates=cycle_updates,
|
| 108 |
+
stage_updates=stage_updates,stage_update=phase,
|
| 109 |
+
stage_remaining=stage_updates-phase-1)
|
| 110 |
+
phase-=stage_updates
|
| 111 |
+
raise AssertionError('Invalid progressive curriculum phase')
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def auxiliary_loop(update_step,micro_index,cfg):
|
| 115 |
+
"""One exact auxiliary budget per committed update; micro_index unused."""
|
| 116 |
+
return auxiliary_stage(update_step,cfg)['depth']
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def prepare(cfg, keep_datasets=False):
|
| 120 |
+
"""Runs once in the coordinator before CUDA/DDP is launched."""
|
| 121 |
+
run=Path(cfg['run_dir']);run.mkdir(parents=True,exist_ok=True)
|
| 122 |
+
lock=open(run/'trainer.lock','w')
|
| 123 |
+
try:fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
| 124 |
+
except BlockingIOError:raise RuntimeError('A trainer is already running in this output directory')
|
| 125 |
+
publisher=Publisher(cfg['repo'],run,checkpoint_prefix=cfg.get('hub_checkpoint_prefix','checkpoints/sparkbet9m'))
|
| 126 |
+
if cfg['upload']:publisher.authenticate()
|
| 127 |
+
sha=pipeline_hash(ROOT)
|
| 128 |
+
if cfg.get('aux_loop_schedule')!='progressive_data_stages_v2':
|
| 129 |
+
raise ValueError('This notebook requires progressive_data_stages_v2 auxiliary scheduling')
|
| 130 |
+
if int(cfg.get('aux_stage_base_updates',0))<1:
|
| 131 |
+
raise ValueError('aux_stage_base_updates must be a positive integer')
|
| 132 |
+
# Allow only two precisely verified prior code fingerprints; fail closed otherwise.
|
| 133 |
+
previous=("273f8513fadd50aeb20907fce71d2352fbb12a965519e7810950caedf36bd207", "a306e734261104d381edf90097254fcb59b298deec64314f48fe85764e5159df", "78638dd200d2834060ffb746ee680feb15b95586b432d37e25606a2575b131aa") if cfg.get('allow_verified_schedule_migration',True) else ()
|
| 134 |
+
state=resume(
|
| 135 |
+
cfg['resume_roots'],cfg['repo'] if cfg['hub_resume'] else None,run,sha,
|
| 136 |
+
checkpoint_prefix=cfg.get('hub_checkpoint_prefix','checkpoints/sparkbet9m'),
|
| 137 |
+
allowed_previous_pipelines=previous,
|
| 138 |
+
migration_schedule='progressive_data_stages_v2',
|
| 139 |
+
aux_stage_base_updates=int(cfg['aux_stage_base_updates']),
|
| 140 |
+
)
|
| 141 |
+
old=None
|
| 142 |
+
if state is None:old=legacy_import(cfg['resume_roots'],cfg.get('legacy_checkpoint'))
|
| 143 |
+
if state:
|
| 144 |
+
# Optimization semantics follow the checkpoint. Hardware-only choices
|
| 145 |
+
# (microbatch and checkpointing) are re-probed on every launch.
|
| 146 |
+
for k in ['global_batch','context','lr','warmup_steps','primary_loops','aux_loop_weight','aux_loop_min','aux_loop_max','aux_loop_schedule','aux_stage_base_updates','aux_curriculum_origin_step','mix_weights']:
|
| 147 |
+
if k in state['config'] and cfg.get(k)!=state['config'][k]:
|
| 148 |
+
print(f'Resuming saved {k}={state["config"][k]}',flush=True);cfg[k]=state['config'][k]
|
| 149 |
+
# When crossing into the rewrite-plus live mix, keep the learned model,
|
| 150 |
+
# optimizer, scaler and Cortex generator position but intentionally reset
|
| 151 |
+
# language source cursors. The source set itself changed, so reusing the old
|
| 152 |
+
# manifest/cursors would silently keep training on the previous corpus.
|
| 153 |
+
migration=state.get('schedule_migrations',[])
|
| 154 |
+
just_migrated=bool(migration and migration[-1].get('new_pipeline')==sha and migration[-1].get('step')==state['step'])
|
| 155 |
+
if just_migrated and cfg.get('migrate_to_rewrite_plus_mix',True):
|
| 156 |
+
manifest=make_manifest(cfg.get('data_file_overrides'),cfg.get('disabled_sources',[]))
|
| 157 |
+
old_mix=state.get('mixer') or {}
|
| 158 |
+
mix_state=dict(cortex=old_mix.get('cortex'),rng=old_mix.get('rng'),stats=old_mix.get('stats',{}))
|
| 159 |
+
mixer=Mixer(manifest,cfg['context'],mix_state,mix_weights=cfg.get('mix_weights')).initialize()
|
| 160 |
+
state.setdefault('data_migrations',[]).append(dict(step=state['step'],kind='rewrite_plus_live_mix',language_cursors_reset=True))
|
| 161 |
+
print('Migrated to live rewrite-plus mix: model/optimizer/scaler/Cortex preserved; language streams restart under the new fair interleaver.',flush=True)
|
| 162 |
+
else:
|
| 163 |
+
manifest=state['manifest'];mixer=Mixer(manifest,cfg['context'],state['mixer'],mix_weights=cfg.get('mix_weights')).initialize()
|
| 164 |
+
else:
|
| 165 |
+
manifest=make_manifest(cfg.get('data_file_overrides'),cfg.get('disabled_sources',[]));mixer=Mixer(manifest,cfg['context'],mix_weights=cfg.get('mix_weights')).initialize()
|
| 166 |
+
state=dict(
|
| 167 |
+
format=FORMAT,step=int(old['completed_steps']) if old else 0,lineage=uuid.uuid4().hex,
|
| 168 |
+
pipeline=sha,config=cfg,manifest=manifest,mixer=mixer.state_dict(),rng_by_rank=None,
|
| 169 |
+
tokens=0,targets=0,model=old['model'] if old else None,optimizer=old['optimizer'] if old else None,
|
| 170 |
+
scaler=old['scaler'] if old else None,migration=old.get('migration') if old else None,
|
| 171 |
+
)
|
| 172 |
+
# Keep the checkpoint's stage origin and budget; never restart a stage
|
| 173 |
+
# accidentally when the notebook reruns after a checkpoint recovery.
|
| 174 |
+
auxiliary_stage(state['step'],cfg)
|
| 175 |
+
panel=mixer.validation();state['manifest']=manifest;state['mixer']=mixer.state_dict();state['config']=cfg
|
| 176 |
+
staging=run/'launch_state.pt';tmp=run/'launch_state.tmp.pt';torch.save(state,tmp);os.replace(tmp,staging)
|
| 177 |
+
torch.save(panel,run/'validation.pt');atomic_json(run/'config.json',cfg);atomic_json(run/'dataset_manifest.json',manifest)
|
| 178 |
+
print(f'Preflight: {len(manifest)} language sources + Cortex; {len(panel)} fixed validation records. Target {cfg["repo"]}.',flush=True)
|
| 179 |
+
print('CPU data validation complete. GPU memory/gradient preflight runs in the training process.',flush=True)
|
| 180 |
+
if keep_datasets:return lock,mixer
|
| 181 |
+
mixer.close();return lock
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _preflight_candidate(model,opt,sc,device,cfg,mb,use_checkpointing):
|
| 185 |
+
set_gradient_checkpointing(use_checkpointing)
|
| 186 |
+
torch.cuda.empty_cache();torch.cuda.reset_peak_memory_stats();opt.zero_grad(set_to_none=True)
|
| 187 |
+
# Use full 1024 input length. Training slices a 1025-token record to 1024 inputs.
|
| 188 |
+
x=torch.randint(0,256,(mb,cfg['context']),device=device)
|
| 189 |
+
y=torch.roll(x,-1,1);mask=torch.ones_like(x,dtype=torch.float32)
|
| 190 |
+
paths=[(int(cfg.get('primary_loops',8)),1.0),(int(cfg.get('aux_loop_max',7)),float(cfg.get('aux_loop_weight',.20)))]
|
| 191 |
+
for loops,weight in paths:
|
| 192 |
+
with amp(device):
|
| 193 |
+
logits=model(x,uniform_steps(loops));loss=loss_sum(logits,y,mask)*weight/mask.sum()
|
| 194 |
+
sc.scale(loss).backward();del logits,loss
|
| 195 |
+
sc.unscale_(opt)
|
| 196 |
+
finite=all(p.grad is None or bool(torch.isfinite(p.grad).all()) for p in model.parameters())
|
| 197 |
+
if not finite:raise RuntimeError('Non-finite FP16 preflight gradients')
|
| 198 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(),1.)
|
| 199 |
+
sc.step(opt);sc.update();torch.cuda.synchronize()
|
| 200 |
+
used=torch.cuda.max_memory_allocated()
|
| 201 |
+
del x,y,mask;opt.zero_grad(set_to_none=True)
|
| 202 |
+
return used
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def memory_preflight(device,cfg):
|
| 206 |
+
"""Full-context L8+L7 probe. Prefer physical batch before checkpointing."""
|
| 207 |
+
saved=rng_state();torch.manual_seed(42)
|
| 208 |
+
model=BETFog(BETConfig()).to(device);verify_architecture(model,1)
|
| 209 |
+
memory=torch.cuda.get_device_properties(device).total_memory
|
| 210 |
+
local_batch=cfg['global_batch']//cfg['world_size']
|
| 211 |
+
requested=[local_batch,32,16,12,8,6,4,3,2,1]+list(cfg.get('microbatch_candidates') or [])
|
| 212 |
+
candidates=[]
|
| 213 |
+
for v in requested:
|
| 214 |
+
v=int(v)
|
| 215 |
+
if v>=1 and v<=local_batch and local_batch%v==0 and v not in candidates:candidates.append(v)
|
| 216 |
+
if 1 not in candidates:candidates.append(1)
|
| 217 |
+
candidates=sorted(candidates,reverse=True)
|
| 218 |
+
report=[];selected=None;selected_checkpointing=None
|
| 219 |
+
# First spend available memory on real batch. Checkpoint only if no uncheckpointed
|
| 220 |
+
# candidate has enough safety headroom.
|
| 221 |
+
for use_checkpointing in (False,True):
|
| 222 |
+
for mb in candidates:
|
| 223 |
+
opt=optimizer_new(model,cfg);sc=scaler_new()
|
| 224 |
+
try:
|
| 225 |
+
used=_preflight_candidate(model,opt,sc,device,cfg,mb,use_checkpointing)
|
| 226 |
+
headroom=used<=memory*float(cfg.get('preflight_memory_fraction',.86))
|
| 227 |
+
report.append(dict(microbatch=mb,gradient_checkpointing=use_checkpointing,peak_gib=used/2**30,headroom=headroom))
|
| 228 |
+
if headroom:
|
| 229 |
+
selected=mb;selected_checkpointing=use_checkpointing
|
| 230 |
+
del opt,sc;break
|
| 231 |
+
except torch.cuda.OutOfMemoryError:
|
| 232 |
+
report.append(dict(microbatch=mb,gradient_checkpointing=use_checkpointing,oom=True))
|
| 233 |
+
torch.cuda.empty_cache()
|
| 234 |
+
finally:
|
| 235 |
+
if 'opt' in locals():
|
| 236 |
+
try:opt.zero_grad(set_to_none=True)
|
| 237 |
+
except Exception:pass
|
| 238 |
+
del opt
|
| 239 |
+
if 'sc' in locals():del sc
|
| 240 |
+
gc.collect();torch.cuda.empty_cache()
|
| 241 |
+
if selected is not None:break
|
| 242 |
+
del model;gc.collect();torch.cuda.empty_cache();restore_rng(saved)
|
| 243 |
+
if selected is None:
|
| 244 |
+
raise RuntimeError('No full-context L8+L7 microbatch fits with memory headroom. Context/architecture were not silently reduced.')
|
| 245 |
+
set_gradient_checkpointing(selected_checkpointing)
|
| 246 |
+
print('GPU preflight:',json.dumps(report),'selected',selected,'checkpointing',selected_checkpointing,flush=True)
|
| 247 |
+
return selected,selected_checkpointing,report
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def train_update(engine,model,opt,sc,batch,cfg,device,rank=0,world=1,update_step=0):
|
| 251 |
+
"""L8 + sustained fresh-data stage auxiliary; overflow retries are schedule-stable."""
|
| 252 |
+
all_ids,all_w=batch;denom=all_w[:,1:].sum()
|
| 253 |
+
if denom<=0:raise RuntimeError('No supervised targets')
|
| 254 |
+
ids=all_ids.chunk(world,0)[rank];weights=all_w.chunk(world,0)[rank]
|
| 255 |
+
mb=cfg['microbatch'];total=ids.shape[0];aux_weight=float(cfg.get('aux_loop_weight',.20))
|
| 256 |
+
# One exact auxiliary budget per optimizer update, independent of physical
|
| 257 |
+
# microbatch partitioning. Every successful update consumes another global
|
| 258 |
+
# batch; longer Lr stages therefore truly receive more data and updates.
|
| 259 |
+
aux_loops_for_update=auxiliary_loop(update_step,0,cfg)
|
| 260 |
+
for attempt in range(9):
|
| 261 |
+
opt.zero_grad(set_to_none=True);losses=torch.zeros(2,device=device);aux_stats=torch.zeros(2,device=device)
|
| 262 |
+
for micro_index,offset in enumerate(range(0,total,mb)):
|
| 263 |
+
x=ids[offset:offset+mb,:-1];y=ids[offset:offset+mb,1:];m=weights[offset:offset+mb,1:]
|
| 264 |
+
aux_loops=aux_loops_for_update
|
| 265 |
+
for j,(loops,weight) in enumerate([(int(cfg.get('primary_loops',8)),1.0),(aux_loops,aux_weight)]):
|
| 266 |
+
sync=world>1 and offset+mb>=total and j==1
|
| 267 |
+
ctx=engine.no_sync() if world>1 and not sync else nullcontext()
|
| 268 |
+
with ctx:
|
| 269 |
+
with amp(device):
|
| 270 |
+
logits=engine(x,uniform_steps(loops));raw=loss_sum(logits,y,m);loss=raw*(world*weight)/denom
|
| 271 |
+
if not all_true(torch.isfinite(raw).item(),device,world):
|
| 272 |
+
raise RuntimeError('Non-finite forward loss; checkpoint remains at last successful update')
|
| 273 |
+
sc.scale(loss).backward();losses[j]+=raw.detach();del logits,raw,loss
|
| 274 |
+
aux_stats[0]+=float(aux_loops);aux_stats[1]+=1
|
| 275 |
+
sc.unscale_(opt)
|
| 276 |
+
finite=all(p.grad is None or bool(torch.isfinite(p.grad).all()) for p in model.parameters());finite=all_true(finite,device,world)
|
| 277 |
+
if finite:
|
| 278 |
+
norm=torch.nn.utils.clip_grad_norm_(model.parameters(),1.);finite=all_true(torch.isfinite(norm).item(),device,world)
|
| 279 |
+
if finite:
|
| 280 |
+
sc.step(opt);sc.update()
|
| 281 |
+
if world>1:dist.all_reduce(losses);dist.all_reduce(aux_stats)
|
| 282 |
+
return (losses/denom).tolist(),float(norm),attempt,int(denom.item()),float(aux_stats[0]/aux_stats[1])
|
| 283 |
+
sc.update(new_scale=sc.get_scale()*.5)
|
| 284 |
+
if rank==0:print(f'FP16 overflow: retrying the same update at scale {sc.get_scale()}',flush=True)
|
| 285 |
+
raise RuntimeError('FP16 overflow persists after eight retries; no data/step advancement')
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
@torch.inference_mode()
|
| 289 |
+
def evaluate(model,panel,step,cfg,device):
|
| 290 |
+
model.eval();metrics={}
|
| 291 |
+
full=step%cfg['full_eval_every']==0
|
| 292 |
+
rows=panel if full else [panel[(step//cfg['eval_every']*8+i)%len(panel)] for i in range(min(8,len(panel)))]
|
| 293 |
+
groups={};budgets=range(1,9) if full else (3,6,8)
|
| 294 |
+
for loops in budgets:
|
| 295 |
+
for r in rows:
|
| 296 |
+
x=torch.tensor([r['ids'][:-1]],device=device);y=torch.tensor([r['ids'][1:]],device=device);w=torch.tensor([r['weights'][1:]],device=device,dtype=torch.float32)
|
| 297 |
+
with amp(device):logits=model(x,uniform_steps(loops));loss=loss_sum(logits,y,w)
|
| 298 |
+
name=r['source'];tag=f'eval/{name}/L{loops}'
|
| 299 |
+
if name=='cortex':tag+=f'/{r["meta"]["surface"]}/{r["meta"].get("split","train")}'
|
| 300 |
+
acc=((logits.argmax(-1)==y)*w).sum();bucket=groups.setdefault(tag,[0.,0.,0.]);bucket[0]+=float(loss);bucket[1]+=float(acc);bucket[2]+=float(w.sum())
|
| 301 |
+
chosen=[r for r in panel if r['source']=='cortex' and len(r['ids'])-r['prompt_len']<=128][:4]
|
| 302 |
+
correct=0
|
| 303 |
+
for r in chosen:
|
| 304 |
+
context=list(r['ids'][:r['prompt_len']]);expected=r['ids'][r['prompt_len']:];ok=True
|
| 305 |
+
for target in expected:
|
| 306 |
+
# Keep only the last context window on generated sequences.
|
| 307 |
+
current=context[-cfg['context']:]
|
| 308 |
+
with amp(device):next_id=int(model(torch.tensor([current],device=device),uniform_steps(loops))[0,-1].argmax())
|
| 309 |
+
if next_id!=target:ok=False;break
|
| 310 |
+
context.append(next_id)
|
| 311 |
+
correct+=ok
|
| 312 |
+
if chosen:metrics[f'eval/cortex/greedy_exact_L{loops}']=correct/len(chosen)
|
| 313 |
+
for tag,(loss,correct,total) in groups.items():
|
| 314 |
+
metrics[tag+'/loss']=loss/total;metrics[tag+'/byte_accuracy']=correct/total
|
| 315 |
+
model.train();return metrics
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def main(cfg):
|
| 319 |
+
rank=int(os.environ.get('RANK','0'));local=int(os.environ.get('LOCAL_RANK','0'));world=int(os.environ.get('WORLD_SIZE','1'))
|
| 320 |
+
if not torch.cuda.is_available():raise RuntimeError('Training requires a CUDA GPU. CPU mode is only used by packaged tests.')
|
| 321 |
+
torch.cuda.set_device(local);device=torch.device('cuda',local)
|
| 322 |
+
if world>1:dist.init_process_group('nccl',timeout=timedelta(minutes=90))
|
| 323 |
+
cfg['world_size']=world;run=Path(cfg['run_dir']);lock=None
|
| 324 |
+
def acquire_lock():
|
| 325 |
+
nonlocal lock
|
| 326 |
+
lock=open(run/'trainer.lock','w')
|
| 327 |
+
try:fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
| 328 |
+
except BlockingIOError:raise RuntimeError('Another trainer is already running here')
|
| 329 |
+
coordinator_call(acquire_lock,rank,world)
|
| 330 |
+
state=torch.load(run/'launch_state.pt',map_location='cpu',weights_only=True)
|
| 331 |
+
random.seed(42);np.random.seed(42);torch.manual_seed(42);torch.cuda.manual_seed(42)
|
| 332 |
+
torch.backends.cuda.matmul.allow_tf32=False;torch.backends.cudnn.allow_tf32=False
|
| 333 |
+
# Keep the mathematically equivalent memory-efficient kernels available. Precision
|
| 334 |
+
# remains FP16 regardless of accelerator family.
|
| 335 |
+
print(f'Rank {rank}: measuring full {cfg["context"]}-byte FP16 L8+L7 backward/Adam memory on {torch.cuda.get_device_name(device)}...',flush=True)
|
| 336 |
+
mb,use_checkpointing,report=memory_preflight(device,cfg)
|
| 337 |
+
m=torch.tensor(mb,device=device,dtype=torch.int32);cp=torch.tensor(int(use_checkpointing),device=device,dtype=torch.int32)
|
| 338 |
+
if world>1:
|
| 339 |
+
dist.all_reduce(m,op=dist.ReduceOp.MIN);dist.all_reduce(cp,op=dist.ReduceOp.MAX)
|
| 340 |
+
cfg['microbatch']=int(m.item());cfg['gradient_checkpointing']=bool(cp.item());set_gradient_checkpointing(cfg['gradient_checkpointing'])
|
| 341 |
+
set_state_noise_sigma(float(cfg.get('state_noise_sigma',0.0)))
|
| 342 |
+
model=BETFog(BETConfig()).to(device);verify_architecture(model,rank)
|
| 343 |
+
opt=optimizer_new(model,cfg);sc=scaler_new()
|
| 344 |
+
if state['model'] is not None:model.load_state_dict(state['model'],strict=True)
|
| 345 |
+
if state['optimizer'] is not None:opt.load_state_dict(state['optimizer'])
|
| 346 |
+
if state['scaler'] is not None:sc.load_state_dict(state['scaler'])
|
| 347 |
+
for values in opt.state.values():
|
| 348 |
+
for k,v in values.items():
|
| 349 |
+
if torch.is_tensor(v) and k!='step':values[k]=v.to(device)
|
| 350 |
+
if state.get('rng_by_rank'):restore_rng(state['rng_by_rank'][rank%len(state['rng_by_rank'])])
|
| 351 |
+
step=state['step'];token_count=state['tokens'];target_count=state['targets'];lineage=state['lineage'];manifest=state['manifest'];committed_mixer=state['mixer'];sha=state['pipeline'];migration=state.get('migration');schedule_migrations=state.get('schedule_migrations',[])
|
| 352 |
+
del state;gc.collect()
|
| 353 |
+
if cfg.get('compile')=='auto':do_compile=torch.cuda.get_device_capability(device)[0]>=8 and not cfg['gradient_checkpointing']
|
| 354 |
+
else:do_compile=bool(cfg.get('compile'))
|
| 355 |
+
if do_compile:
|
| 356 |
+
torch._dynamo.config.suppress_errors=True
|
| 357 |
+
try:
|
| 358 |
+
modules=[*model.prelude,*model.body,*model.coda]
|
| 359 |
+
for layer in modules:layer.compile(dynamic=True)
|
| 360 |
+
print(f'Rank {rank}: physical-block compilation enabled (FP16 unchanged).',flush=True)
|
| 361 |
+
except Exception as e:print('Compile unavailable; eager blocks retained:',type(e).__name__,flush=True)
|
| 362 |
+
engine=DDP(model,device_ids=[local],broadcast_buffers=False,gradient_as_bucket_view=True) if world>1 else model
|
| 363 |
+
mixer=Mixer(manifest,cfg['context'],committed_mixer,mix_weights=cfg.get('mix_weights')).initialize() if rank==0 else None
|
| 364 |
+
panel=torch.load(run/'validation.pt',weights_only=True) if rank==0 else None
|
| 365 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 366 |
+
publisher=Publisher(cfg['repo'],run,checkpoint_prefix=cfg.get('hub_checkpoint_prefix','checkpoints/sparkbet9m'));writer=None;segment=None
|
| 367 |
+
def new_writer():
|
| 368 |
+
nonlocal writer,segment
|
| 369 |
+
segment=run/'publish_logs'/lineage;segment.mkdir(parents=True,exist_ok=True)
|
| 370 |
+
writer=SummaryWriter(str(segment),purge_step=step+1,max_queue=10,flush_secs=30,filename_suffix=f'.step-{step:012d}')
|
| 371 |
+
if rank==0:
|
| 372 |
+
new_writer();atomic_json(run/'gpu_preflight.json',dict(report=report,world=world,microbatch=cfg['microbatch'],gradient_checkpointing=cfg['gradient_checkpointing'],precision='float16'))
|
| 373 |
+
stop=False
|
| 374 |
+
def stop_handler(*_):
|
| 375 |
+
nonlocal stop;stop=True
|
| 376 |
+
signal.signal(signal.SIGINT,stop_handler);signal.signal(signal.SIGTERM,stop_handler)
|
| 377 |
+
start=time.monotonic();last_save=start;last_upload=start;last_logs=start;last_export=start;saved_step=-1
|
| 378 |
+
def checkpoint_now(upload=False):
|
| 379 |
+
nonlocal saved_step,last_save,last_upload
|
| 380 |
+
rng=rng_state();rngs=[None]*world if rank==0 else None
|
| 381 |
+
if world>1:dist.gather_object(rng,rngs,dst=0)
|
| 382 |
+
else:rngs=[rng]
|
| 383 |
+
def do_save():
|
| 384 |
+
nonlocal saved_step,last_save,last_upload
|
| 385 |
+
payload=dict(format=FORMAT,step=step,lineage=lineage,pipeline=sha,config=cfg,manifest=manifest,mixer=committed_mixer,
|
| 386 |
+
rng_by_rank=rngs,tokens=token_count,targets=target_count,model=model.state_dict(),optimizer=opt.state_dict(),scaler=sc.state_dict(),migration=migration,schedule_migrations=schedule_migrations)
|
| 387 |
+
path=save(run,payload,cfg['keep_local']);saved_step=step;last_save=time.monotonic();print(f'Saved full checkpoint: update {step:,}',flush=True)
|
| 388 |
+
if cfg.get('modal_volume_name'):
|
| 389 |
+
try:
|
| 390 |
+
import modal;modal.Volume.from_name(cfg['modal_volume_name']).commit()
|
| 391 |
+
except Exception as e:print('Volume commit failed; Hub upload still attempted:',type(e).__name__,flush=True)
|
| 392 |
+
if upload and cfg['upload']:
|
| 393 |
+
try:publisher.checkpoint(path);last_upload=time.monotonic();print('Full-state Hub upload complete.',flush=True)
|
| 394 |
+
except Exception as e:print('Hub upload failed; local checkpoint is intact:',type(e).__name__,str(e)[:200],flush=True)
|
| 395 |
+
coordinator_call(do_save,rank,world)
|
| 396 |
+
try:
|
| 397 |
+
while True:
|
| 398 |
+
stop_flag=stop or (run/'STOP').exists();should_stop=not all_true(not stop_flag,device,world)
|
| 399 |
+
if should_stop:break
|
| 400 |
+
stage=auxiliary_stage(step,cfg)
|
| 401 |
+
if rank==0 and stage['stage_update']==0:
|
| 402 |
+
examples=int(cfg['global_batch'])*stage['stage_updates']
|
| 403 |
+
print(f'Curriculum cycle {stage["cycle"]}: L8 + L{stage["depth"]} | '
|
| 404 |
+
f'{stage["stage_updates"]:,} fresh batch draws, '
|
| 405 |
+
f'{examples:,} example presentations allocated.',flush=True)
|
| 406 |
+
began=time.monotonic();data=coordinator_call(lambda:mixer.batch(cfg['global_batch']),rank,world)
|
| 407 |
+
shape=broadcast_object(data['ids'].shape if rank==0 else None,rank,world)
|
| 408 |
+
x=torch.as_tensor(data['ids'],device=device) if rank==0 else torch.empty(shape,dtype=torch.long,device=device)
|
| 409 |
+
w=torch.as_tensor(data['weights'],device=device) if rank==0 else torch.empty(shape,dtype=torch.float32,device=device)
|
| 410 |
+
if world>1:dist.broadcast(x,0);dist.broadcast(w,0)
|
| 411 |
+
current_lr=lr_at(step,cfg)
|
| 412 |
+
for pg in opt.param_groups:pg['lr']=current_lr
|
| 413 |
+
losses,norm,overflows,targets,aux_mean=train_update(engine,model,opt,sc,(x,w),cfg,device,rank,world,step)
|
| 414 |
+
step+=1;token_count+=int((x[:,1:]!=256).sum());target_count+=targets
|
| 415 |
+
if rank==0:committed_mixer=mixer.state_dict()
|
| 416 |
+
del x,w,data
|
| 417 |
+
if rank==0:
|
| 418 |
+
elapsed=time.monotonic()-began;aux_weight=float(cfg.get('aux_loop_weight',.20))
|
| 419 |
+
values={'train/loss_L8':losses[0],'train/loss_aux':losses[1],'train/aux_loop_mean':aux_mean,
|
| 420 |
+
'train/objective':losses[0]+aux_weight*losses[1],'train/lr':current_lr,'train/grad_norm':norm,
|
| 421 |
+
'train/scaler':sc.get_scale(),'train/overflow_retries':overflows,'train/supervised_tokens':target_count,
|
| 422 |
+
'train/nonpad_tokens':token_count,'train/update_seconds':elapsed,'train/target_bytes_per_second':targets/elapsed,
|
| 423 |
+
'train/aux_stage_depth':stage['depth'], 'train/aux_stage_cycle':stage['cycle'],
|
| 424 |
+
'train/aux_stage_updates_completed':stage['stage_update']+1,
|
| 425 |
+
'train/aux_stage_updates_budget':stage['stage_updates'],
|
| 426 |
+
'train/aux_stage_examples_presented':(stage['stage_update']+1)*int(cfg['global_batch']),
|
| 427 |
+
'train/aux_stage_updates_remaining':stage['stage_remaining']}
|
| 428 |
+
for k,v in values.items():writer.add_scalar(k,v,step)
|
| 429 |
+
if step%cfg['log_every']==0:
|
| 430 |
+
print(f'update {step:,} | L8 {losses[0]:.4f} | aux {losses[1]:.4f} @ L{stage["depth"]} '
|
| 431 |
+
f'({stage["stage_update"]+1}/{stage["stage_updates"]} stage updates) | '
|
| 432 |
+
f'lr {current_lr:.2e} | {elapsed:.1f}s | targets {target_count:,}',flush=True)
|
| 433 |
+
for n,s in mixer.sources.items():writer.add_scalar('data/'+n+'/epochs',s.s['epoch'],step);writer.add_scalar('data/'+n+'/rejected_rows',s.s['rejected'],step)
|
| 434 |
+
atomic_json(run/'metrics_latest.json',dict(step=step,**values))
|
| 435 |
+
if step%cfg['eval_every']==0:
|
| 436 |
+
def evaluate_and_log():
|
| 437 |
+
metrics=evaluate(model,panel,step,cfg,device)
|
| 438 |
+
for k,v in metrics.items():writer.add_scalar(k,v,step)
|
| 439 |
+
atomic_json(run/'eval_latest.json',dict(step=step,**metrics));print('Validation complete:',json.dumps(metrics),flush=True)
|
| 440 |
+
coordinator_call(evaluate_and_log,rank,world)
|
| 441 |
+
actions=None
|
| 442 |
+
if rank==0:
|
| 443 |
+
now=time.monotonic();actions=dict(
|
| 444 |
+
save=step%cfg['save_every']==0 or now-last_save>=cfg['save_seconds'],
|
| 445 |
+
upload=step%cfg['hub_checkpoint_steps']==0,
|
| 446 |
+
logs=now-last_logs>=cfg['hub_log_seconds'],export=now-last_export>=cfg['hub_export_seconds'])
|
| 447 |
+
actions=broadcast_object(actions,rank,world)
|
| 448 |
+
if actions['save'] or actions['upload']:checkpoint_now(actions['upload'])
|
| 449 |
+
if rank==0 and actions['logs']:
|
| 450 |
+
writer.flush();writer.close()
|
| 451 |
+
if cfg['upload']:
|
| 452 |
+
try:publisher.logs()
|
| 453 |
+
except Exception as e:print('Metric upload pending; retry next interval:',type(e).__name__,flush=True)
|
| 454 |
+
new_writer();last_logs=time.monotonic()
|
| 455 |
+
if rank==0 and actions['export'] and cfg['upload']:
|
| 456 |
+
try:publisher.export(model,step,manifest,ROOT);last_export=time.monotonic()
|
| 457 |
+
except Exception as e:print('Inference export pending:',type(e).__name__,str(e)[:200],flush=True)
|
| 458 |
+
except BaseException:
|
| 459 |
+
if rank==0:
|
| 460 |
+
print('Training failed. Recover from the last completed checkpoint; no partial update is published.',flush=True)
|
| 461 |
+
if writer:writer.flush();writer.close()
|
| 462 |
+
if mixer:mixer.close()
|
| 463 |
+
raise
|
| 464 |
+
else:
|
| 465 |
+
if rank==0:print('Final checkpoint and metric flush...',flush=True)
|
| 466 |
+
checkpoint_now(upload=True)
|
| 467 |
+
if rank==0:
|
| 468 |
+
writer.flush();writer.close()
|
| 469 |
+
if cfg['upload']:
|
| 470 |
+
try:publisher.logs();publisher.export(model,step,manifest,ROOT)
|
| 471 |
+
except Exception as e:print('Final publication incomplete; local state preserved:',type(e).__name__,str(e)[:200],flush=True)
|
| 472 |
+
mixer.close()
|
| 473 |
+
if world>1:dist.destroy_process_group()
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
if __name__=='__main__':
|
| 477 |
+
import argparse
|
| 478 |
+
parser=argparse.ArgumentParser();parser.add_argument('--config',required=True);parser.add_argument('--prepare',action='store_true');args=parser.parse_args()
|
| 479 |
+
cfg=json.loads(Path(args.config).read_text())
|
| 480 |
+
if args.prepare:prepare(cfg)
|
| 481 |
+
else:main(cfg)
|
unified/ablations/unified-v1-blt-none-s42/source/unified_data.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Coverage-preserving four-family data stream. No permanent train exclusions."""
|
| 2 |
+
import copy,fnmatch,hashlib,json,math
|
| 3 |
+
from collections import Counter
|
| 4 |
+
from functools import lru_cache
|
| 5 |
+
import numpy as np
|
| 6 |
+
from dataset_registry import SOURCES,retry
|
| 7 |
+
from data_stream import Source,Mixer,row_key
|
| 8 |
+
from cortex_source import CortexSource,episode_pair,FAMILIES
|
| 9 |
+
from records import BOS,EOS,PAD,validate,record,plain_chunks,InvalidRecord
|
| 10 |
+
|
| 11 |
+
GROUPS=('climbmix','ultrafine','cortex','rewrite6')
|
| 12 |
+
DEFAULT_WEIGHTS=dict(climbmix=50,ultrafine=30,cortex=15,rewrite6=5)
|
| 13 |
+
|
| 14 |
+
def group_of(name): return 'ultrafine' if name.startswith('ultra_') else name
|
| 15 |
+
|
| 16 |
+
def complete_manifest(old=None,api=None):
|
| 17 |
+
"""Retain old pinned revisions/cursors; add missing partitions at that revision."""
|
| 18 |
+
from huggingface_hub import HfApi
|
| 19 |
+
from dataclasses import asdict
|
| 20 |
+
api=api or HfApi();old={k:v for k,v in (old or {}).items() if k in SOURCES};revisions={s['repo']:s['revision'] for s in old.values()}
|
| 21 |
+
info={};result={}
|
| 22 |
+
for name,spec in SOURCES.items():
|
| 23 |
+
if spec.repo not in info:
|
| 24 |
+
info[spec.repo]=retry(lambda:api.dataset_info(spec.repo,revision=revisions.get(spec.repo)))
|
| 25 |
+
entry=info[spec.repo]
|
| 26 |
+
files=sorted(f.rfilename for f in entry.siblings if any(fnmatch.fnmatch(f.rfilename,p) for p in spec.patterns))
|
| 27 |
+
if not files:raise RuntimeError(f'{name}: no matching shards at pinned revision {entry.sha}')
|
| 28 |
+
if name in old:
|
| 29 |
+
if set(old[name]['files'])-set(files):raise RuntimeError(f'{name}: saved shards missing at pinned revision')
|
| 30 |
+
result[name]=copy.deepcopy(old[name])
|
| 31 |
+
# Preserve the old shard-index meaning. Newly discovered shards append.
|
| 32 |
+
result[name]['files']=list(old[name]['files'])+[f for f in files if f not in old[name]['files']]
|
| 33 |
+
else: result[name]={**asdict(spec),'revision':entry.sha,'files':files}
|
| 34 |
+
return result
|
| 35 |
+
|
| 36 |
+
@lru_cache(maxsize=64)
|
| 37 |
+
def quota_tuple(count,weights):
|
| 38 |
+
"""Closest integer allocation with strict priority and every family present."""
|
| 39 |
+
if count<10:raise ValueError('global_batch must be >=10 for strict climbmix > ultrafine > cortex > rewrite6 quotas')
|
| 40 |
+
target=np.array(weights,float)*count/sum(weights);best=None;score=float('inf')
|
| 41 |
+
for rewrite in range(1,count//4+1):
|
| 42 |
+
for cortex in range(rewrite+1,count//3+1):
|
| 43 |
+
for ultra in range(cortex+1,count//2+1):
|
| 44 |
+
climb=count-rewrite-cortex-ultra
|
| 45 |
+
if climb<=ultra:continue
|
| 46 |
+
q=(climb,ultra,cortex,rewrite);loss=float(((np.array(q)-target)**2).sum())
|
| 47 |
+
if loss<score:score=loss;best=q
|
| 48 |
+
if best is None:raise ValueError('Cannot satisfy per-batch source priority')
|
| 49 |
+
return best
|
| 50 |
+
|
| 51 |
+
class CoverageSource(Source):
|
| 52 |
+
def initialize(self):
|
| 53 |
+
# Legacy held-out rows rejoin the stream when their shard is visited again.
|
| 54 |
+
# Never reset a partially consumed shard merely to change the mixture.
|
| 55 |
+
self.s['initialized']=True
|
| 56 |
+
self.s.setdefault('pending_document',None)
|
| 57 |
+
self.s.setdefault('documents_emitted',0)
|
| 58 |
+
def _chunk(self):
|
| 59 |
+
doc=self.s['pending_document'];raw=doc['bytes'];offset=doc['offset'];total=len(raw)+2
|
| 60 |
+
stop=min(total,offset+self.limit+1)
|
| 61 |
+
ids=([BOS] if offset==0 else [])+list(raw[max(0,offset-1):min(len(raw),stop-1)])+([EOS] if stop==total else [])
|
| 62 |
+
r=dict(ids=ids,weights=[0]+[1]*(len(ids)-1),source=self.name,prompt_len=1,
|
| 63 |
+
meta={'row_key':doc['key'],'chunk_offset':offset,'coverage':'lossless'})
|
| 64 |
+
doc['offset']+=self.limit
|
| 65 |
+
if doc['offset']>=total-1:
|
| 66 |
+
self.s['pending_document']=None;self.s['documents_emitted']+=1
|
| 67 |
+
return validate(r,self.limit)
|
| 68 |
+
def next_record(self,excluded=()):
|
| 69 |
+
if self.s['pending']:return self.s['pending'].pop(0) # Preserve legacy buffered records.
|
| 70 |
+
if self.s.get('pending_document') is not None:return self._chunk()
|
| 71 |
+
row=self.raw();key=row_key(self.spec,row)
|
| 72 |
+
try:
|
| 73 |
+
if self.spec['kind']=='rewrite':
|
| 74 |
+
values=[row.get(k) for k in ('instruction','text','output')]
|
| 75 |
+
if not all(isinstance(v,str) for v in values):raise InvalidRecord('instruction/text/output must be strings')
|
| 76 |
+
text='\n\n'.join(values)
|
| 77 |
+
else:
|
| 78 |
+
text=row.get(self.spec['column'])
|
| 79 |
+
if not isinstance(text,str):raise InvalidRecord('missing string column '+self.spec['column'])
|
| 80 |
+
raw=text.encode('utf-8') # Empty/whitespace rows still contribute their EOS.
|
| 81 |
+
except (ValueError,TypeError,UnicodeError) as e:
|
| 82 |
+
raise RuntimeError(f'{self.name}: invalid row at shard={self.s["shard"]}, cursor={self.s["cursor"]}; no sample was silently skipped. Fix the schema, then resume the last complete checkpoint.') from e
|
| 83 |
+
self.s['accepted']+=1
|
| 84 |
+
self.s['pending_document']={'bytes':raw,'offset':0,'key':key}
|
| 85 |
+
return self._chunk()
|
| 86 |
+
|
| 87 |
+
class CodecCortex:
|
| 88 |
+
"""Add deterministic graphics within Cortex's share; never skip a task index."""
|
| 89 |
+
def __init__(self,state=None,limit=1024,codec='none'):
|
| 90 |
+
state=state or {};self.codec=codec;self.limit=limit
|
| 91 |
+
self.base=CortexSource(state.get('kernel',state if 'index' in state else None),limit)
|
| 92 |
+
self.visual_index=state.get('visual_index',0);self.until_visual=state.get('until_visual',3)
|
| 93 |
+
def record(self):
|
| 94 |
+
if self.codec=='none' or self.until_visual:
|
| 95 |
+
self.until_visual=max(0,self.until_visual-1);return self.base.record()
|
| 96 |
+
from canvas_flow import draw_picture,FAMILIES as GRAPHICS,pack_braille
|
| 97 |
+
rng=np.random.default_rng(92000+self.visual_index);family=GRAPHICS[self.visual_index%len(GRAPHICS)]
|
| 98 |
+
bitmap=draw_picture(rng,family,48)
|
| 99 |
+
if self.codec=='braille':
|
| 100 |
+
cells=pack_braille(bitmap);art='\n'.join(''.join(chr(0x2800+int(v)) for v in row) for row in cells)
|
| 101 |
+
elif self.codec=='ascii':
|
| 102 |
+
density=bitmap.reshape(12,4,24,2).mean((1,3));ramp=' .:-=+*#%@'
|
| 103 |
+
art='\n'.join(''.join(ramp[int(round(v*9))] for v in row) for row in density)
|
| 104 |
+
else:raise ValueError(self.codec)
|
| 105 |
+
r=record(f'Image encoding={self.codec}; name the drawing.\n{art}\nAnswer: ',family,'cortex',self.limit,
|
| 106 |
+
meta={'family':'visual_classification','codec':self.codec,'bitmap_seed':self.visual_index})
|
| 107 |
+
self.visual_index+=1;self.until_visual=3;return r
|
| 108 |
+
def state_dict(self):return dict(kernel=self.base.state_dict(),visual_index=self.visual_index,until_visual=self.until_visual)
|
| 109 |
+
def validation(self):
|
| 110 |
+
rows=self.base.validation()
|
| 111 |
+
if self.codec!='none':
|
| 112 |
+
other=CodecCortex({'visual_index':100000,'until_visual':0},self.limit,self.codec)
|
| 113 |
+
for _ in range(10):other.until_visual=0;rows.append(other.record())
|
| 114 |
+
return rows
|
| 115 |
+
|
| 116 |
+
class CoverageMixer(Mixer):
|
| 117 |
+
def __init__(self,manifest,limit=1024,state=None,loaders=None,group_weights=None,codec='none'):
|
| 118 |
+
super().__init__(manifest,limit,state,loaders,cortex=False)
|
| 119 |
+
state=state or {}
|
| 120 |
+
self.sources={n:CoverageSource(n,s,state.get('sources',{}).get(n),limit,(loaders or {}).get(n)) for n,s in manifest.items()}
|
| 121 |
+
self.cortex=CodecCortex(state.get('cortex'),limit,codec)
|
| 122 |
+
self.group_weights=dict(group_weights or state.get('group_weights') or DEFAULT_WEIGHTS)
|
| 123 |
+
if set(self.group_weights)!=set(GROUPS) or not all(v>0 for v in self.group_weights.values()):raise ValueError('Exactly four positive source weights are required')
|
| 124 |
+
self.groups={g:[n for n in self.sources if group_of(n)==g] for g in GROUPS};self.groups['cortex']=['cortex']
|
| 125 |
+
if any(not n for n in self.groups.values()):raise ValueError('A required dataset family is missing')
|
| 126 |
+
self.group_cursor=dict(state.get('group_cursor',{}));self.codec=codec;self.excluded=set()
|
| 127 |
+
self.pending={n:r for n,r in self.pending.items() if n in manifest or n=='cortex'}
|
| 128 |
+
def initialize(self):
|
| 129 |
+
for s in self.sources.values():s.initialize()
|
| 130 |
+
# Do not consume a row merely to build a validation set.
|
| 131 |
+
return self
|
| 132 |
+
def batch(self,count):
|
| 133 |
+
quotas=quota_tuple(count,tuple(self.group_weights[g] for g in GROUPS))
|
| 134 |
+
schedule=sorted((i/q,g) for g,q in zip(GROUPS,quotas) for i in range(q))
|
| 135 |
+
blocks=[]
|
| 136 |
+
for _,group in schedule:
|
| 137 |
+
members=self.groups[group];cursor=self.group_cursor.get(group,0)
|
| 138 |
+
name=members[cursor%len(members)];self.group_cursor[group]=cursor+1
|
| 139 |
+
# One requested source per block; old buffered records stay attached to it.
|
| 140 |
+
self.mix_queue=[name];blocks.append(self.block())
|
| 141 |
+
width=min(self.limit+1,1+math.ceil(max(b[3]-1 for b in blocks)/128)*128)
|
| 142 |
+
return dict(ids=np.asarray([b[0][:width] for b in blocks],np.int64),
|
| 143 |
+
weights=np.asarray([b[1][:width] for b in blocks],np.float32),sources=[b[2] for b in blocks])
|
| 144 |
+
def state_dict(self):
|
| 145 |
+
s=super().state_dict();s.update(group_weights=self.group_weights,group_cursor=self.group_cursor,
|
| 146 |
+
coverage_version=1,codec=self.codec,excluded=[])
|
| 147 |
+
return copy.deepcopy(s)
|
| 148 |
+
def validation(self):
|
| 149 |
+
# Language probes are illustrative, external to the streamed corpora;
|
| 150 |
+
# do not call them a representative held-out web benchmark.
|
| 151 |
+
probes=[record('','The river flows past a quiet village.','language_probe',self.limit,supervise_all=True),
|
| 152 |
+
record('','A careful experiment changes one condition at a time.','language_probe',self.limit,supervise_all=True)]
|
| 153 |
+
return probes+self.cortex.validation()
|
| 154 |
+
def coverage(self):
|
| 155 |
+
return {n:dict(epoch=s.s['epoch'],shard=s.s['shard'],shards=len(s.spec['files']),rows_read=s.s['rows'],
|
| 156 |
+
documents_emitted=s.s.get('documents_emitted',0),pending_document=s.s.get('pending_document') is not None,
|
| 157 |
+
buffered_records=len(s.s['pending'])+int(n in self.pending)) for n,s in self.sources.items()}
|
unified/ablations/unified-v1-blt-none-s42/source/unified_models.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit experiment registry; no recurrent Ouro candidate."""
|
| 2 |
+
from dataclasses import asdict
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from torch import nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from bet_model import BETFog,BETConfig,uniform_steps,verify_architecture
|
| 8 |
+
from causal_arch import ModelCfg,build_model,forward_model,targets_and_mask
|
| 9 |
+
|
| 10 |
+
CAUSAL=('sparkbet','ouro_1pass','evabyte','blt','byteflow_causal')
|
| 11 |
+
FLOW=('canvas_full','canvas_no_flow','canvas_raster','canvas_patch8')
|
| 12 |
+
VARIANTS=CAUSAL+FLOW
|
| 13 |
+
|
| 14 |
+
def build(variant,context=1024,seed=42):
|
| 15 |
+
if variant not in VARIANTS:raise ValueError(f'Unsupported variant {variant}; recurrent ouro is removed')
|
| 16 |
+
torch.manual_seed(seed)
|
| 17 |
+
if variant=='sparkbet':
|
| 18 |
+
m=BETFog(BETConfig());verify_architecture(m,1)
|
| 19 |
+
return m,dict(variant=variant,config=asdict(m.c),fidelity='checkpoint-compatible SparkBET, one physical pass')
|
| 20 |
+
if variant in FLOW:
|
| 21 |
+
from canvas_flow import Config,CanvasModel
|
| 22 |
+
v=variant.removeprefix('canvas_');v='full' if v=='full' else v
|
| 23 |
+
c=Config(text_len=context+2,variant=v,patch=8 if v=='patch8' else 4)
|
| 24 |
+
m=nn.ModuleDict({'content':CanvasModel(c),'planner':CanvasModel(Config(**{**asdict(c),'dim':128,'depth':3,'heads':4}),True)})
|
| 25 |
+
return m,dict(variant=variant,config=asdict(c),fidelity='custom absorbing discrete flow; separate planner and content')
|
| 26 |
+
c=ModelCfg(dim=320,ff=864,heads=5,context=context,body_layers=6,loops=1,
|
| 27 |
+
local_dim=128,local_ff=384,local_heads=2,encoder_layers=1,decoder_layers=1,
|
| 28 |
+
local_window=128,eva_window=256,eva_chunk=16,hash_buckets=1024,
|
| 29 |
+
entropy_dim=64,entropy_ff=192,entropy_heads=1,entropy_layers=2,grad_checkpoint=False)
|
| 30 |
+
m,meta=build_model(variant,c,seed)
|
| 31 |
+
return m,dict(variant=variant,config=asdict(c),**meta)
|
| 32 |
+
|
| 33 |
+
def denominators(ids,weights,variant):
|
| 34 |
+
n=8 if variant=='evabyte' else 1
|
| 35 |
+
return torch.stack([(targets_and_mask(ids,j+1)[1]*weights[:,j+1:]).sum().clamp_min(1) for j in range(n)])
|
| 36 |
+
|
| 37 |
+
def causal_loss(model,variant,ids,weights,denom):
|
| 38 |
+
out={'logits':model(ids[:,:-1],uniform_steps(1))} if variant=='sparkbet' else forward_model(model,ids[:,:-1],variant)
|
| 39 |
+
heads=out.get('heads') or [out['logits']];loss=out['logits'].float().sum()*0
|
| 40 |
+
primary=None
|
| 41 |
+
for j,logits in enumerate(heads):
|
| 42 |
+
target,valid=targets_and_mask(ids,j+1);mask=valid*weights[:,j+1:]
|
| 43 |
+
ce=F.cross_entropy(logits[:,:target.shape[1]].float().flatten(0,1),target.flatten(),reduction='none').view_as(target)
|
| 44 |
+
raw=(ce*mask).sum();loss=loss+raw/denom[j]/len(heads)
|
| 45 |
+
if j==0:primary=raw.detach()/denom[0]
|
| 46 |
+
return loss,primary
|
| 47 |
+
|
| 48 |
+
def canvas_batch(data,step,context,codec='braille',seed=42,variant='canvas_full'):
|
| 49 |
+
"""Every real input byte is retained; only Cortex contributes generated images.
|
| 50 |
+
|
| 51 |
+
Bytes (including UTF-8 continuation bytes) have individual flow positions.
|
| 52 |
+
Display code must UTF-8-decode the reconstructed linguistic lane. Spatial
|
| 53 |
+
byte slots are not Unicode glyph cells. Do not claim browser-grade shaping.
|
| 54 |
+
"""
|
| 55 |
+
from canvas_flow import make_example,capacity_map,ARRAY_KEYS,TEXT,IMAGE,RESERVED,FREE,EOS,PAD,POLICIES
|
| 56 |
+
rows=[]
|
| 57 |
+
for j,(ids,source) in enumerate(zip(data['ids'],data['sources'])):
|
| 58 |
+
index=step*len(data['ids'])+j
|
| 59 |
+
# Non-Cortex content is text-only; preserve all corpus bytes, no filtering.
|
| 60 |
+
e=make_example(index,'train',128,context+2,192,seed,encoding_override=(codec=='ascii'))
|
| 61 |
+
# Preserve packed-document separation without introducing an early flow EOS.
|
| 62 |
+
lane=[int(x) if x<256 else 10 for x in ids if 0<=x<256 or x==258]
|
| 63 |
+
raw=np.asarray(lane,np.int16)
|
| 64 |
+
visual=source=='cortex' and index%3!=0
|
| 65 |
+
if not visual:e['image'].fill(0);e['layout'].fill(TEXT)
|
| 66 |
+
else:
|
| 67 |
+
e['layout'][e['layout']==FREE]=TEXT
|
| 68 |
+
# Image-only pages are additional Cortex presentations, never replace a
|
| 69 |
+
# corpus/Cortex task's bytes. The paired task bytes stay on this page.
|
| 70 |
+
if (e['layout']==TEXT).sum()<len(raw)+1:raise RuntimeError('Canvas capacity exhausted; no bytes truncated')
|
| 71 |
+
text=np.full(context+2,PAD,np.int16);text[:len(raw)]=raw;text[len(raw)]=EOS;e['text']=text
|
| 72 |
+
if variant=='canvas_raster':e['order']=np.int64(POLICIES.index('row'))
|
| 73 |
+
e['coords'],e['regions'],e['capacity']=capacity_map(e['layout'],context+2,POLICIES[int(e['order'])])
|
| 74 |
+
geometry='; '.join(f'{o["family"]} at {o["x"]},{o["y"]}' for o in e['meta']['objects']) if visual else ''
|
| 75 |
+
prompt=f'{source}; {"mixed" if visual else "text"}; {"ASCII" if e["encoding"] else "Braille"}; order {POLICIES[int(e["order"])]}; margin {e["meta"]["margin"]}; {geometry}'.encode()[:192]
|
| 76 |
+
e['prompt'].fill(PAD);e['prompt'][:len(prompt)]=list(prompt)
|
| 77 |
+
rows.append(e)
|
| 78 |
+
if visual and index%3==1:
|
| 79 |
+
# Additional image-only page: the original task bytes above are kept.
|
| 80 |
+
import copy
|
| 81 |
+
picture=copy.deepcopy(e);picture['layout'][picture['layout']==TEXT]=FREE
|
| 82 |
+
picture['text'].fill(PAD);picture['text'][0]=EOS
|
| 83 |
+
picture['coords'].fill(0);picture['regions'].fill(0);picture['capacity'].fill(False)
|
| 84 |
+
rows.append(picture)
|
| 85 |
+
return {k:torch.from_numpy(np.stack([e[k] for e in rows])).long() for k in ARRAY_KEYS}
|
| 86 |
+
|
| 87 |
+
def flow_loss(model,batch):
|
| 88 |
+
from canvas_flow import make_loss
|
| 89 |
+
content=make_loss(model['content'],batch)
|
| 90 |
+
planner=make_loss(model['planner'],batch,planner=True)
|
| 91 |
+
return content+planner,content.detach()
|
unified/ablations/unified-v1-blt-none-s42/source/unified_runner.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single-GPU Run All entry point: bounded screens or continuous main training."""
|
| 2 |
+
import contextlib,copy,csv,fcntl,json,math,os,random,signal,time,uuid
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from checkpoints import pipeline_hash,atomic_json
|
| 7 |
+
from unified_data import CoverageMixer,complete_manifest,DEFAULT_WEIGHTS
|
| 8 |
+
from unified_models import build,CAUSAL,FLOW,denominators,causal_loss,canvas_batch,flow_loss
|
| 9 |
+
from unified_state import Store,save,ledger,fingerprint,FORMAT,LEGACY_FORMAT
|
| 10 |
+
from trainer import rng_state,restore_rng
|
| 11 |
+
|
| 12 |
+
ROOT=Path(__file__).parent
|
| 13 |
+
|
| 14 |
+
def lr_at(step,cfg,plan):
|
| 15 |
+
value=cfg['lr']*min(1.,(step+1)/max(1,cfg['warmup_steps']))
|
| 16 |
+
end=plan['end_step'];cool=plan['cooldown_steps']
|
| 17 |
+
if end is not None and cool and step>=end-cool:
|
| 18 |
+
# Last optimizer update has the requested floor; no off-by-one zero step.
|
| 19 |
+
fraction=(end-1-step)/max(1,cool-1)
|
| 20 |
+
value*=cfg['min_lr_ratio']+(1-cfg['min_lr_ratio'])*max(0.,fraction)
|
| 21 |
+
return value
|
| 22 |
+
|
| 23 |
+
def plan_for(start,steps,cooldown):
|
| 24 |
+
if cooldown<0:raise ValueError('Cooldown must be nonnegative')
|
| 25 |
+
if steps is not None and steps<1:raise ValueError('Finite steps must be positive; use None for infinite')
|
| 26 |
+
return dict(start_step=start,end_step=None if steps is None else start+steps,
|
| 27 |
+
cooldown_steps=0 if steps is None else min(steps,int(cooldown)))
|
| 28 |
+
|
| 29 |
+
def finite_optimizer(opt):
|
| 30 |
+
return all(not torch.is_tensor(v) or bool(torch.isfinite(v).all()) for s in opt.state.values() for v in s.values())
|
| 31 |
+
|
| 32 |
+
def update(model,opt,data,variant,cfg,device,step,codec):
|
| 33 |
+
"""Retry SAME batch + RNG: BF16/FP16 scales -> FP32, OOM -> smaller microbatch.
|
| 34 |
+
|
| 35 |
+
No optimizer mutation on a failed backward. Invalid Adam updates restore the
|
| 36 |
+
pre-update parameters and moments, then retry at a lower learning rate.
|
| 37 |
+
"""
|
| 38 |
+
saved_rng=rng_state();micro=min(cfg['microbatch'],len(data['ids']))
|
| 39 |
+
ids=torch.as_tensor(data['ids'],device=device);weights=torch.as_tensor(data['weights'],device=device)
|
| 40 |
+
flow=variant in FLOW
|
| 41 |
+
mapped=(data.get('canvas') or canvas_batch(data,step,cfg['context'],codec,cfg['seed'],variant)) if flow else None
|
| 42 |
+
batch_count=len(mapped['image']) if flow else len(ids)
|
| 43 |
+
denom=None if flow else denominators(ids,weights,variant)
|
| 44 |
+
modes=[('bf16',1.)] if device.type=='cuda' and torch.cuda.is_bf16_supported() else ([('fp16',s) for s in cfg['fallback_scales']] if device.type=='cuda' else [])
|
| 45 |
+
modes += [('fp32',1.),('fp32',1.)]
|
| 46 |
+
mode_index=0;attempt=0;lr_backoff=1.
|
| 47 |
+
initial_lrs=[p['lr'] for p in opt.param_groups]
|
| 48 |
+
while mode_index<len(modes):
|
| 49 |
+
precision,scale=modes[mode_index];restore_rng(saved_rng);opt.zero_grad(set_to_none=True)
|
| 50 |
+
total=0.;primary=0.;failed=False;oom=False
|
| 51 |
+
try:
|
| 52 |
+
for start in range(0,batch_count,micro):
|
| 53 |
+
end=min(batch_count,start+micro)
|
| 54 |
+
with torch.autocast(device.type,dtype=torch.bfloat16 if precision=='bf16' else torch.float16,enabled=precision!='fp32'):
|
| 55 |
+
if flow:
|
| 56 |
+
batch={k:v[start:end].to(device) for k,v in mapped.items()}
|
| 57 |
+
loss,one=flow_loss(model,batch);factor=(end-start)/batch_count;loss=loss*factor;one=one*factor
|
| 58 |
+
else:loss,one=causal_loss(model,variant,ids[start:end],weights[start:end],denom)
|
| 59 |
+
if not bool(torch.isfinite(loss)):failed=True;break
|
| 60 |
+
(loss*scale).backward();total+=float(loss.detach());primary+=float(one)
|
| 61 |
+
if not failed:
|
| 62 |
+
for p in model.parameters():
|
| 63 |
+
if p.grad is not None:p.grad.div_(scale)
|
| 64 |
+
failed=not all(p.grad is None or bool(torch.isfinite(p.grad).all()) for p in model.parameters())
|
| 65 |
+
if not failed:
|
| 66 |
+
norm=torch.nn.utils.clip_grad_norm_(model.parameters(),cfg['clip_grad'])
|
| 67 |
+
failed=not bool(torch.isfinite(norm))
|
| 68 |
+
except torch.cuda.OutOfMemoryError:
|
| 69 |
+
oom=True;failed=True
|
| 70 |
+
if not failed:
|
| 71 |
+
# ~9M parameter screen: CPU rollback copies are cheap relative to a
|
| 72 |
+
# corrupt long-running optimizer. Disable only with an explicit choice.
|
| 73 |
+
snapshot={k:v.detach().cpu().clone() for k,v in model.state_dict().items()}
|
| 74 |
+
moments=copy.deepcopy(opt.state_dict())
|
| 75 |
+
for group,lr in zip(opt.param_groups,initial_lrs):group['lr']=lr*lr_backoff
|
| 76 |
+
opt.step()
|
| 77 |
+
good=all(bool(torch.isfinite(p).all()) for p in model.parameters()) and finite_optimizer(opt)
|
| 78 |
+
if good:return dict(loss=primary,objective=total,grad_norm=float(norm),retry_count=attempt,precision=precision,
|
| 79 |
+
targets=int(weights[:,1:].sum()),microbatch=micro,lr_backoff=lr_backoff)
|
| 80 |
+
model.load_state_dict(snapshot);opt.load_state_dict(moments);lr_backoff*=.25
|
| 81 |
+
attempt+=1;opt.zero_grad(set_to_none=True)
|
| 82 |
+
if oom:
|
| 83 |
+
if device.type=='cuda':torch.cuda.empty_cache()
|
| 84 |
+
if micro==1:raise RuntimeError('OOM at microbatch 1; checkpoint is intact. Use canvas_patch8 or a smaller model.')
|
| 85 |
+
micro=max(1,micro//2)
|
| 86 |
+
else:mode_index+=1
|
| 87 |
+
print(f'Retrying unchanged batch: attempt={attempt}, microbatch={micro}, next precision={modes[min(mode_index,len(modes)-1)][0]}, lr multiplier={lr_backoff}',flush=True)
|
| 88 |
+
restore_rng(saved_rng)
|
| 89 |
+
raise RuntimeError('Numerical fallbacks exhausted; no data cursor committed. Resume the last complete checkpoint.')
|
| 90 |
+
|
| 91 |
+
@torch.no_grad()
|
| 92 |
+
def evaluate(model,variant,panel,cfg,device,step,codec):
|
| 93 |
+
"""Fixed external Cortex/probe panel; not held-out web perplexity."""
|
| 94 |
+
was=model.training;model.eval();saved=rng_state();torch.manual_seed(97031)
|
| 95 |
+
losses=[]
|
| 96 |
+
try:
|
| 97 |
+
for r in panel:
|
| 98 |
+
data=dict(ids=np.asarray([r['ids']],np.int64),weights=np.asarray([r['weights']],np.float32),sources=[r['source']])
|
| 99 |
+
if variant in FLOW:
|
| 100 |
+
batch={k:v.to(device) for k,v in canvas_batch(data,0,cfg['context'],codec,seed=97031,variant=variant).items()}
|
| 101 |
+
loss,_=flow_loss(model,batch)
|
| 102 |
+
else:
|
| 103 |
+
x=torch.as_tensor(data['ids'],device=device);w=torch.as_tensor(data['weights'],device=device)
|
| 104 |
+
_,loss=causal_loss(model,variant,x,w,denominators(x,w,variant))
|
| 105 |
+
losses.append(float(loss))
|
| 106 |
+
return sum(losses)/len(losses)
|
| 107 |
+
finally:restore_rng(saved);model.train(was)
|
| 108 |
+
|
| 109 |
+
def patcher_stage(model,mixer,cfg,device,store):
|
| 110 |
+
"""Train an independent entropy model; persist progress and train-only threshold.
|
| 111 |
+
|
| 112 |
+
Its extra data/compute are reported separately. Main ablations restart the
|
| 113 |
+
same data anchor afterward, so patcher preparation does not shift BLT's data.
|
| 114 |
+
"""
|
| 115 |
+
stage_began=time.monotonic();previous_seconds=0.
|
| 116 |
+
path=store.run/'patcher.pt';patcher=model.patcher;patcher.requires_grad_(True)
|
| 117 |
+
opt=torch.optim.AdamW(patcher.parameters(),lr=cfg['lr'],betas=(.9,.95));start=0
|
| 118 |
+
if not path.exists() and store.cfg['hub_resume'] and store.prefix+'/patcher.pt' in store.remote_files:
|
| 119 |
+
from huggingface_hub import hf_hub_download
|
| 120 |
+
import shutil
|
| 121 |
+
f=hf_hub_download(store.repo,store.prefix+'/patcher.pt',revision=store.head)
|
| 122 |
+
shutil.copy2(f,path)
|
| 123 |
+
if path.exists():
|
| 124 |
+
state=torch.load(path,map_location='cpu',weights_only=True)
|
| 125 |
+
if state['budget']!=cfg['patcher_steps']:raise ValueError('Patcher budget differs')
|
| 126 |
+
if state.get('signature')!=store.signature or state.get('code_sha')!=pipeline_hash(ROOT):raise ValueError('Patcher configuration/code differs')
|
| 127 |
+
if state.get('manifest')!=mixer.manifest:raise ValueError('Patcher dataset revision differs; use its original data anchor')
|
| 128 |
+
patcher.load_state_dict(state['model']);opt.load_state_dict(state['optimizer']);start=state['step'];previous_seconds=state.get('patcher_seconds',0.)
|
| 129 |
+
if hasattr(mixer,'archive'):
|
| 130 |
+
from prepared_data import PreparedMixer
|
| 131 |
+
mixer=PreparedMixer(mixer.manifest,cfg['context'],state['mixer'],group_weights=cfg['group_weights'],archive=mixer.archive).initialize()
|
| 132 |
+
else:mixer=CoverageMixer(mixer.manifest,cfg['context'],state['mixer'],group_weights=cfg['group_weights']).initialize()
|
| 133 |
+
restore_rng(state['rng'])
|
| 134 |
+
if hasattr(mixer,'archive'):
|
| 135 |
+
from prepared_data import ShardPrefetcher
|
| 136 |
+
mixer=ShardPrefetcher(mixer,cfg,store.run_id+'-patcher',start,cfg['patcher_steps']-start+4,observations=state.get('prepared_observations') if path.exists() else None)
|
| 137 |
+
mixer.prewarm()
|
| 138 |
+
for i in range(start,cfg['patcher_steps']):
|
| 139 |
+
data=mixer.batch(cfg['global_batch']);update(patcher,opt,data,'entropy',cfg,device,i,'none')
|
| 140 |
+
if hasattr(mixer,'commit_batch'):mixer.commit_batch()
|
| 141 |
+
if (i+1)%cfg['save_every']==0 or i+1==cfg['patcher_steps']:
|
| 142 |
+
temp=path.with_suffix('.tmp');torch.save(dict(step=i+1,budget=cfg['patcher_steps'],patcher_seconds=previous_seconds+time.monotonic()-stage_began,signature=store.signature,code_sha=pipeline_hash(ROOT),manifest=mixer.manifest,model=patcher.state_dict(),optimizer=opt.state_dict(),mixer=mixer.state_dict(),prepared_observations=copy.deepcopy(getattr(mixer,'observations',{})),rng=rng_state()),temp);os.replace(temp,path)
|
| 143 |
+
if hasattr(mixer,'flush_observed'):mixer.flush_observed(store.run_id+'-patcher','entropy-pretraining',i+1,path)
|
| 144 |
+
if store.cfg['upload']:
|
| 145 |
+
from huggingface_hub import CommitOperationAdd
|
| 146 |
+
operations=[CommitOperationAdd(path_in_repo=store.prefix+'/patcher.pt',path_or_fileobj=str(path))]
|
| 147 |
+
anchor=store.root/(cfg['experiment_id']+'-anchor.pt')
|
| 148 |
+
if anchor.exists():operations.append(CommitOperationAdd(path_in_repo='unified/experiments/'+cfg['experiment_id']+'/anchor.pt',path_or_fileobj=str(anchor)))
|
| 149 |
+
result=store.api.create_commit(repo_id=store.repo,parent_commit=store.head,operations=operations,commit_message=f'{store.run_id}: entropy patcher {i+1}')
|
| 150 |
+
store.head=result.oid
|
| 151 |
+
patcher.eval();values=[]
|
| 152 |
+
for _ in range(4):
|
| 153 |
+
data=mixer.batch(cfg['global_batch']);x=torch.as_tensor(data['ids'],device=device)[:,:-1]
|
| 154 |
+
if hasattr(mixer,'commit_batch'):
|
| 155 |
+
mixer.pending_event['phase']='entropy_calibration';mixer.commit_batch()
|
| 156 |
+
for chunk in x.split(cfg['microbatch']):
|
| 157 |
+
ent=patcher.entropy(chunk);values.append(ent[chunk<256].cpu())
|
| 158 |
+
values=torch.cat(values)
|
| 159 |
+
if not len(values):raise RuntimeError('Empty entropy calibration')
|
| 160 |
+
patcher.threshold.fill_(float(torch.quantile(values,1-1/model.c.patch_size)))
|
| 161 |
+
patcher.requires_grad_(False)
|
| 162 |
+
if hasattr(mixer,'flush_observed'):mixer.flush_observed(store.run_id+'-calibration','entropy-calibration',cfg['patcher_steps'],path)
|
| 163 |
+
mixer.close()
|
| 164 |
+
return previous_seconds+time.monotonic()-stage_began
|
| 165 |
+
|
| 166 |
+
def run_one(cfg,variant,codec,seed,main=False,anchor=None):
|
| 167 |
+
if variant in FLOW and codec not in ('braille','ascii'):raise ValueError('Canvas codec must be braille or ascii')
|
| 168 |
+
if codec not in ('none','braille','ascii'):raise ValueError('Unknown codec')
|
| 169 |
+
cfg={**cfg,'seed':seed};run_id=f'main-{variant}-{codec}' if main else f'{cfg["experiment_id"]}-{variant}-{codec}-s{seed}'
|
| 170 |
+
# Hyperparameters that change a trial's meaning are immutable on resume.
|
| 171 |
+
semantic={k:cfg[k] for k in ('context','global_batch','lr','warmup_steps','group_weights','clip_grad','patcher_steps')}
|
| 172 |
+
signature=fingerprint(dict(variant=variant,codec=codec,seed=seed,semantic=semantic,version=1))
|
| 173 |
+
store=Store(cfg,run_id,signature,main);state=store.latest();store.restore_ledger()
|
| 174 |
+
promoted=False
|
| 175 |
+
if main and state is None and cfg.get('train_from_run_id'):
|
| 176 |
+
origin=Store(cfg,cfg['train_from_run_id'],signature,False);state=origin.latest()
|
| 177 |
+
if state is None:raise RuntimeError('Requested ablation checkpoint was not found')
|
| 178 |
+
state=copy.deepcopy(state);state['initialization']='promoted:'+cfg['train_from_run_id'];state['lineage']=uuid.uuid4().hex;promoted=True
|
| 179 |
+
random.seed(seed);np.random.seed(seed);torch.manual_seed(seed)
|
| 180 |
+
device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 181 |
+
if device.type!='cuda' and not cfg.get('allow_cpu_test'):raise RuntimeError('Select a Molab RTX PRO 6000 GPU runtime before Run All')
|
| 182 |
+
if device.type=='cuda':
|
| 183 |
+
print(torch.cuda.get_device_name(),flush=True)
|
| 184 |
+
torch.backends.cuda.matmul.allow_tf32=True;torch.backends.cudnn.allow_tf32=True
|
| 185 |
+
model,meta=build(variant,cfg['context'],seed);model=model.to(device)
|
| 186 |
+
legacy=state is not None and state['format']==LEGACY_FORMAT
|
| 187 |
+
if state and not legacy and state['code_sha']!=pipeline_hash(ROOT):raise RuntimeError('Code fingerprint differs; use a new experiment ID or explicitly migrate the implementation')
|
| 188 |
+
manifest=complete_manifest(state['manifest'] if state else (anchor or {}).get('manifest'))
|
| 189 |
+
mix_state=state['mixer'] if state else (anchor or {}).get('mixer')
|
| 190 |
+
if state and state.get('prepared_dataset') and not cfg.get('prepared_shards',False):raise ValueError('This checkpoint uses prepared record cursors; keep PREPARED_SHARDS enabled')
|
| 191 |
+
archive=None
|
| 192 |
+
if cfg.get('prepared_shards',False):
|
| 193 |
+
from prepared_data import Archive,PreparedMixer,ShardPrefetcher
|
| 194 |
+
archive=Archive(cfg)
|
| 195 |
+
mixer=PreparedMixer(manifest,cfg['context'],mix_state,group_weights=cfg['group_weights'],codec=codec,archive=archive).initialize()
|
| 196 |
+
else:mixer=CoverageMixer(manifest,cfg['context'],mix_state,group_weights=cfg['group_weights'],codec=codec).initialize()
|
| 197 |
+
start=state['step'] if state else 0;step=start
|
| 198 |
+
if state and state['config']['context']!=cfg['context']:raise ValueError('Resume context differs; buffered stream records must keep their original context')
|
| 199 |
+
if state:model.load_state_dict(state['model'],strict=True)
|
| 200 |
+
initialization=state.get('initialization','migrated_attached_sparkbet') if state else 'fresh_matched_seed'
|
| 201 |
+
print(f'{run_id}: {initialization}; {sum(p.numel() for p in model.parameters()):,} parameters; one pass',flush=True)
|
| 202 |
+
patcher_seconds=state.get('patcher_seconds',0.) if state else 0.
|
| 203 |
+
if variant=='blt' and not state:
|
| 204 |
+
if cfg['patcher_steps']<1:raise ValueError('BLT requires a positive entropy-patcher training budget')
|
| 205 |
+
pm=PreparedMixer(manifest,cfg['context'],mix_state,group_weights=cfg['group_weights'],archive=archive).initialize() if archive else CoverageMixer(manifest,cfg['context'],mix_state,group_weights=cfg['group_weights']).initialize()
|
| 206 |
+
patcher_seconds=patcher_stage(model,pm,cfg,device,store)
|
| 207 |
+
# Restore the trial's initialization RNG after preparation (patcher saved).
|
| 208 |
+
random.seed(seed);torch.manual_seed(seed)
|
| 209 |
+
opt=torch.optim.AdamW([p for p in model.parameters() if p.requires_grad],lr=cfg['lr'],betas=(.9,.95),eps=1e-8,weight_decay=.1,
|
| 210 |
+
fused=(device.type=='cuda'))
|
| 211 |
+
if state:opt.load_state_dict(state['optimizer'])
|
| 212 |
+
if state:
|
| 213 |
+
rng=state.get('rng') or (state.get('rng_by_rank') or [None])[0]
|
| 214 |
+
if rng:restore_rng(rng)
|
| 215 |
+
steps=cfg['train_steps'] if main else cfg['ablation_steps']
|
| 216 |
+
plan=state['plan'] if state and not legacy and not promoted else plan_for(start,steps,cfg['cooldown_steps'])
|
| 217 |
+
if state and not legacy and not promoted:
|
| 218 |
+
if main and ((steps is None)!=(plan['end_step'] is None)):
|
| 219 |
+
print('Explicit main schedule change: keeping weights, optimizer and all data cursors',flush=True)
|
| 220 |
+
plan=plan_for(start,steps,cfg['cooldown_steps'])
|
| 221 |
+
expected=None if steps is None else steps
|
| 222 |
+
saved_budget=None if plan['end_step'] is None else plan['end_step']-plan['start_step']
|
| 223 |
+
if expected!=saved_budget:raise ValueError('Saved run budget differs. Resume with its original budget; use a new experiment ID for a new trial.')
|
| 224 |
+
lineage=state['lineage'] if state else uuid.uuid4().hex
|
| 225 |
+
targets=state.get('targets',0) if state else 0;seconds=state.get('seconds',0.) if state else 0.
|
| 226 |
+
measured_targets=state.get('measured_targets',0) if state else 0
|
| 227 |
+
if archive and (plan['end_step'] is None or step<plan['end_step']):
|
| 228 |
+
mapper=(lambda data,i:canvas_batch(data,i,cfg['context'],codec,seed,variant)) if variant in FLOW else None
|
| 229 |
+
mixer=ShardPrefetcher(mixer,cfg,run_id,step,None if plan['end_step'] is None else plan['end_step']-step,variant,codec,mapper,
|
| 230 |
+
observations=state.get('prepared_observations') if state else None)
|
| 231 |
+
mixer.prewarm()
|
| 232 |
+
preparation_seconds=(state.get('preparation_seconds',0.) if state else 0.)+getattr(mixer,'preparation_seconds',0.)
|
| 233 |
+
code_sha=pipeline_hash(ROOT);committed=mixer.state_dict();stop=False
|
| 234 |
+
panel=mixer.validation();last={};probe='';csvpath=Path(cfg['work_dir'])/'experiments.csv'
|
| 235 |
+
def handler(*_):
|
| 236 |
+
nonlocal stop;stop=True
|
| 237 |
+
old_handlers={s:signal.getsignal(s) for s in (signal.SIGINT,signal.SIGTERM)}
|
| 238 |
+
for s in old_handlers:signal.signal(s,handler)
|
| 239 |
+
def checkpoint(status,publish=True):
|
| 240 |
+
payload=dict(format=FORMAT,step=step,lineage=lineage,signature=signature,code_sha=code_sha,config=cfg,meta=meta,
|
| 241 |
+
manifest=manifest,mixer=committed,model=model.state_dict(),optimizer=opt.state_dict(),rng=rng_state(),
|
| 242 |
+
plan=plan,targets=targets,measured_targets=measured_targets,seconds=seconds,preparation_seconds=preparation_seconds,patcher_seconds=patcher_seconds,initialization=initialization)
|
| 243 |
+
if archive:
|
| 244 |
+
payload['prepared_dataset']=dict(repo=archive.repo,revision=archive.head,recipe=archive.recipe)
|
| 245 |
+
payload['prepared_observations']=copy.deepcopy(getattr(mixer,'observations',{}))
|
| 246 |
+
path=save(store.run,payload,cfg['keep_local'])
|
| 247 |
+
if hasattr(mixer,'flush_observed'):mixer.flush_observed(run_id,lineage,step,path)
|
| 248 |
+
row=dict(run_id=run_id,mode='train' if main else 'ablation',cohort='canvas_flow' if variant in FLOW else ('codec' if codec!='none' else 'causal'),
|
| 249 |
+
variant=variant,codec=codec,seed=seed,step=step,start_step=plan['start_step'],status=status,initialization=initialization,
|
| 250 |
+
params=sum(p.numel() for p in model.parameters()),context=cfg['context'],loss=last.get('loss',''),probe_loss=probe,
|
| 251 |
+
targets=targets,seconds=seconds,targets_per_second=measured_targets/max(1,seconds),grad_norm=last.get('grad_norm',''),retry_count=last.get('retry_count',0),
|
| 252 |
+
precision=last.get('precision',''),patcher_steps=cfg['patcher_steps'] if variant=='blt' else 0,
|
| 253 |
+
dataset_sha=fingerprint(manifest),code_sha=code_sha,coverage_json=json.dumps(mixer.coverage(),sort_keys=True))
|
| 254 |
+
observations=getattr(mixer,'observations',{})
|
| 255 |
+
row.update(sample_presentations=observations.get('presentations',''),unique_samples_seen=observations.get('unique_samples',''),preparation_seconds=preparation_seconds,patcher_seconds=patcher_seconds)
|
| 256 |
+
ledger(csvpath,row)
|
| 257 |
+
atomic_json(store.run/'coverage.json',mixer.coverage())
|
| 258 |
+
progress=dict(step=step,prepared_dataset=payload.get('prepared_dataset'),observations=observations,coverage=mixer.coverage())
|
| 259 |
+
if publish:store.publish(path,model,manifest,{**meta,'data_progress':progress})
|
| 260 |
+
try:
|
| 261 |
+
if not state or promoted:checkpoint('initialized')
|
| 262 |
+
# Completed finite trials are immutable; Run All proceeds to the next one.
|
| 263 |
+
if plan['end_step'] is not None and step>=plan['end_step']:
|
| 264 |
+
print(f'{run_id}: already complete',flush=True);return True
|
| 265 |
+
while not stop and not (Path(cfg['work_dir'])/'STOP').exists():
|
| 266 |
+
if plan['end_step'] is not None and step>=plan['end_step']:break
|
| 267 |
+
began=time.monotonic();data=mixer.batch(cfg['global_batch'])
|
| 268 |
+
for g in opt.param_groups:g['lr']=lr_at(step,cfg,plan)
|
| 269 |
+
last=update(model,opt,data,variant,cfg,device,step,codec)
|
| 270 |
+
cfg['microbatch']=last['microbatch'];step+=1;targets+=last['targets'];measured_targets+=last['targets'];seconds+=time.monotonic()-began
|
| 271 |
+
if hasattr(mixer,'commit_batch'):mixer.commit_batch()
|
| 272 |
+
committed=mixer.state_dict()
|
| 273 |
+
if step%cfg['log_every']==0:print(f'{run_id}: update={step} loss={last["loss"]:.4f} targets/s={measured_targets/max(1,seconds):.0f}',flush=True)
|
| 274 |
+
if step%cfg['eval_every']==0:probe=evaluate(model,variant,panel,cfg,device,step,codec)
|
| 275 |
+
if step%cfg['save_every']==0:checkpoint('running',publish=step%cfg['hub_every']==0)
|
| 276 |
+
elif step%cfg['hub_every']==0:checkpoint('running')
|
| 277 |
+
complete=plan['end_step'] is not None and step>=plan['end_step']
|
| 278 |
+
probe=evaluate(model,variant,panel,cfg,device,step,codec)
|
| 279 |
+
checkpoint('complete' if complete else 'paused')
|
| 280 |
+
return complete
|
| 281 |
+
except BaseException:
|
| 282 |
+
print('Run failed: no partial update is published. Resume the last complete local/Hub checkpoint.',flush=True)
|
| 283 |
+
raise
|
| 284 |
+
finally:
|
| 285 |
+
mixer.close()
|
| 286 |
+
for s,h in old_handlers.items():signal.signal(s,h)
|
| 287 |
+
del model,opt
|
| 288 |
+
if device.type=='cuda':torch.cuda.empty_cache()
|
| 289 |
+
|
| 290 |
+
def main(cfg):
|
| 291 |
+
if cfg['mode'] not in ('train','ablation'):raise ValueError('MODE must be train or ablation')
|
| 292 |
+
for key in ('context','global_batch','microbatch','ablation_steps','patcher_steps','hub_every','save_every','eval_every','log_every'):
|
| 293 |
+
if not isinstance(cfg[key],int) or cfg[key]<1:raise ValueError(key+' must be a positive integer')
|
| 294 |
+
if cfg.get('prepared_shards'):
|
| 295 |
+
for key in ('prepared_records_per_shard','cortex_records_per_shard','prefetch_shards','dispatch_batches_per_shard','prepared_memory_shards','prepared_workers'):
|
| 296 |
+
if cfg.get(key,1)<1:raise ValueError(key+' must be positive')
|
| 297 |
+
if cfg['global_batch']<10:raise ValueError('GLOBAL_BATCH must be at least 10')
|
| 298 |
+
if not cfg['seeds'] or len(set(cfg['seeds']))!=len(cfg['seeds']):raise ValueError('Supply distinct seeds')
|
| 299 |
+
if cfg['lr']<=0 or cfg['clip_grad']<=0 or not 0<=cfg['min_lr_ratio']<=1:raise ValueError('Invalid optimizer settings')
|
| 300 |
+
root=Path(cfg['work_dir']);root.mkdir(parents=True,exist_ok=True)
|
| 301 |
+
lock=(root/'runner.lock').open('w')
|
| 302 |
+
try:fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
| 303 |
+
except BlockingIOError:raise RuntimeError('This experiment already has an active trainer')
|
| 304 |
+
if (root/'STOP').exists():
|
| 305 |
+
print('Removing the previous clean-stop request; Run All explicitly resumes training.',flush=True);(root/'STOP').unlink()
|
| 306 |
+
if cfg['mode']=='train':run_one(cfg,cfg['train_variant'],cfg['train_codec'],cfg['seeds'][0],True)
|
| 307 |
+
else:
|
| 308 |
+
# One immutable starting stream for all fresh architecture/codec trials.
|
| 309 |
+
anchor_path=root/(cfg['experiment_id']+'-anchor.pt')
|
| 310 |
+
if anchor_path.exists():anchor=torch.load(anchor_path,map_location='cpu',weights_only=True)
|
| 311 |
+
else:
|
| 312 |
+
from huggingface_hub import HfApi,hf_hub_download
|
| 313 |
+
from dataset_registry import retry
|
| 314 |
+
remote_anchor='unified/experiments/'+cfg['experiment_id']+'/anchor.pt'
|
| 315 |
+
info=retry(lambda:HfApi().model_info(cfg['repo'])) if cfg['hub_resume'] else None
|
| 316 |
+
if info and remote_anchor in [f.rfilename for f in info.siblings]:
|
| 317 |
+
import shutil
|
| 318 |
+
f=retry(lambda:hf_hub_download(cfg['repo'],remote_anchor,revision=info.sha))
|
| 319 |
+
shutil.copy2(f,anchor_path)
|
| 320 |
+
manifest=complete_manifest();m=CoverageMixer(manifest,cfg['context'],group_weights=cfg['group_weights']).initialize()
|
| 321 |
+
anchor=torch.load(anchor_path,map_location='cpu',weights_only=True) if anchor_path.exists() else dict(manifest=manifest,mixer=m.state_dict());m.close();tmp=anchor_path.with_suffix('.tmp');torch.save(anchor,tmp);os.replace(tmp,anchor_path)
|
| 322 |
+
for seed in cfg['seeds']:
|
| 323 |
+
for variant,codec in cfg['trials']:
|
| 324 |
+
if not run_one(cfg,variant,codec,seed,anchor=anchor):return
|
| 325 |
+
lock.close()
|
| 326 |
+
|
| 327 |
+
if __name__=='__main__':
|
| 328 |
+
import argparse
|
| 329 |
+
parser=argparse.ArgumentParser();parser.add_argument('--config',required=True);args=parser.parse_args()
|
| 330 |
+
main(json.loads(Path(args.config).read_text()))
|
unified/ablations/unified-v1-blt-none-s42/source/unified_state.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verified resume and single-commit state + inference + experiment ledger."""
|
| 2 |
+
import csv,hashlib,json,os,shutil,time,uuid,copy
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import torch
|
| 5 |
+
from huggingface_hub import HfApi,hf_hub_download,CommitOperationAdd,CommitOperationDelete
|
| 6 |
+
from checkpoints import digest,atomic_json,verified_load,FORMAT as LEGACY_FORMAT,Publisher
|
| 7 |
+
from dataset_registry import retry
|
| 8 |
+
|
| 9 |
+
FORMAT='cortex-unified-v1'
|
| 10 |
+
LEGACY_PIPELINE='d99360d8f48a2cc017360a4d48b462b9fbee20902d3b58bcc936f054c46fc07c'
|
| 11 |
+
FIELDS=['run_id','mode','cohort','variant','codec','seed','step','start_step','status','initialization',
|
| 12 |
+
'sample_presentations','unique_samples_seen','preparation_seconds','patcher_seconds','params','context','loss','probe_loss','targets','seconds','targets_per_second','grad_norm',
|
| 13 |
+
'retry_count','precision','patcher_steps','dataset_sha','code_sha','coverage_json']
|
| 14 |
+
|
| 15 |
+
def fingerprint(value):return hashlib.sha256(json.dumps(value,sort_keys=True,default=str).encode()).hexdigest()
|
| 16 |
+
|
| 17 |
+
def ledger(path,row):
|
| 18 |
+
"""One ledger; idempotent (run_id, step) upsert across retries/resumes."""
|
| 19 |
+
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True)
|
| 20 |
+
rows=list(csv.DictReader(path.open())) if path.exists() else []
|
| 21 |
+
key=(str(row['run_id']),str(row['step']))
|
| 22 |
+
rows=[r for r in rows if (r['run_id'],r['step'])!=key];rows.append({k:row.get(k,'') for k in FIELDS})
|
| 23 |
+
tmp=path.with_suffix('.tmp')
|
| 24 |
+
with tmp.open('w',newline='') as f:
|
| 25 |
+
w=csv.DictWriter(f,fieldnames=FIELDS);w.writeheader();w.writerows(rows);f.flush();os.fsync(f.fileno())
|
| 26 |
+
os.replace(tmp,path)
|
| 27 |
+
|
| 28 |
+
def save(run,state,keep=3):
|
| 29 |
+
root=Path(run)/'checkpoints';root.mkdir(parents=True,exist_ok=True)
|
| 30 |
+
dest=root/f'checkpoint-{state["step"]:012d}'
|
| 31 |
+
temp=root/('.partial-'+uuid.uuid4().hex);temp.mkdir()
|
| 32 |
+
try:
|
| 33 |
+
with (temp/'training.pt').open('wb') as f:torch.save(state,f);f.flush();os.fsync(f.fileno())
|
| 34 |
+
meta=dict(format=FORMAT,step=state['step'],lineage=state['lineage'],signature=state['signature'],
|
| 35 |
+
sha256=digest(temp/'training.pt'),bytes=(temp/'training.pt').stat().st_size,saved_at=time.time())
|
| 36 |
+
atomic_json(temp/'metadata.json',meta);(temp/'COMPLETE').write_text('complete\n')
|
| 37 |
+
if dest.exists():
|
| 38 |
+
# Completed update is immutable; status/ledger can still be republished.
|
| 39 |
+
shutil.rmtree(temp);return dest
|
| 40 |
+
os.replace(temp,dest)
|
| 41 |
+
for p in sorted(root.glob('checkpoint-*'))[:-keep]:shutil.rmtree(p)
|
| 42 |
+
return dest
|
| 43 |
+
except BaseException:
|
| 44 |
+
shutil.rmtree(temp,ignore_errors=True);raise
|
| 45 |
+
|
| 46 |
+
def load(path):
|
| 47 |
+
path=Path(path);meta=json.loads((path/'metadata.json').read_text())
|
| 48 |
+
if meta['format']==LEGACY_FORMAT:return verified_load(path)
|
| 49 |
+
f=path/'training.pt'
|
| 50 |
+
if meta['format']!=FORMAT or not (path/'COMPLETE').exists():raise ValueError('Unrecognized/incomplete checkpoint')
|
| 51 |
+
if f.stat().st_size!=meta['bytes'] or digest(f)!=meta['sha256']:raise ValueError('Checkpoint checksum mismatch')
|
| 52 |
+
s=torch.load(f,map_location='cpu',weights_only=True)
|
| 53 |
+
if any(s[k]!=meta[k] for k in ('format','step','lineage','signature')):raise ValueError('State/metadata mismatch')
|
| 54 |
+
return s
|
| 55 |
+
|
| 56 |
+
class Store:
|
| 57 |
+
def __init__(self,cfg,run_id,signature,main=False,api=None):
|
| 58 |
+
self.cfg=cfg;self.run_id=run_id;self.signature=signature;self.main=main
|
| 59 |
+
self.repo=cfg['repo'];self.root=Path(cfg['work_dir']);self.run=self.root/run_id
|
| 60 |
+
self.run.mkdir(parents=True,exist_ok=True);self.api=api or HfApi()
|
| 61 |
+
self.prefix='unified/main/'+run_id if main else 'unified/ablations/'+run_id
|
| 62 |
+
self.head=None;self.remote_files=[]
|
| 63 |
+
def inventory(self):
|
| 64 |
+
if not self.cfg['hub_resume'] and not self.cfg['upload']:return
|
| 65 |
+
# Authentication/repository errors must fail, never silently start fresh.
|
| 66 |
+
info=retry(lambda:self.api.model_info(self.repo));self.head=info.sha
|
| 67 |
+
self.remote_files=[f.rfilename for f in info.siblings]
|
| 68 |
+
def latest(self):
|
| 69 |
+
self.inventory();items=[]
|
| 70 |
+
for p in (self.run/'checkpoints').glob('checkpoint-*/metadata.json'):
|
| 71 |
+
if (p.parent/'COMPLETE').exists():items.append((json.loads(p.read_text()),p.parent,None))
|
| 72 |
+
if self.cfg['hub_resume']:
|
| 73 |
+
for f in self.remote_files:
|
| 74 |
+
eligible=f.startswith(self.prefix+'/checkpoint-') or (self.main and self.cfg.get('train_variant','sparkbet')=='sparkbet' and not self.cfg.get('train_from_run_id') and f.startswith('checkpoints/sparkbet9m/checkpoint-'))
|
| 75 |
+
if eligible and f.endswith('/metadata.json') and f.rsplit('/',1)[0]+'/COMPLETE' in self.remote_files:
|
| 76 |
+
local=retry(lambda f=f:hf_hub_download(self.repo,f,revision=self.head,cache_dir=str(self.root/'hub_cache')))
|
| 77 |
+
items.append((json.loads(Path(local).read_text()),None,f.rsplit('/',1)[0]))
|
| 78 |
+
unified=[x for x in items if x[0]['format']==FORMAT]
|
| 79 |
+
# The unified lineage succeeds the old pipeline. Never prefer a legacy
|
| 80 |
+
# writer's higher step over a deliberately migrated unified lineage.
|
| 81 |
+
if unified:items=unified
|
| 82 |
+
if len({x[0]['lineage'] for x in items})>1:raise RuntimeError('Conflicting lineages; select a distinct work directory/repository')
|
| 83 |
+
for meta,path,remote in sorted(items,key=lambda x:(x[0]['step'],x[0]['saved_at']),reverse=True):
|
| 84 |
+
if meta['format']==FORMAT and meta['signature']!=self.signature:raise RuntimeError('Saved experiment configuration differs; change EXPERIMENT_ID for a new trial')
|
| 85 |
+
try:
|
| 86 |
+
if remote:
|
| 87 |
+
path=self.run/'download'/remote.replace('/','_');path.mkdir(parents=True,exist_ok=True)
|
| 88 |
+
for n in ('training.pt','metadata.json','COMPLETE'):
|
| 89 |
+
f=retry(lambda n=n:hf_hub_download(self.repo,remote+'/'+n,revision=self.head,cache_dir=str(self.root/'hub_cache')))
|
| 90 |
+
shutil.copy2(f,path/n)
|
| 91 |
+
s=load(path)
|
| 92 |
+
except (ValueError,EOFError,OSError) as e:
|
| 93 |
+
print(f'Invalid checkpoint at {meta["step"]}: {type(e).__name__}; trying older complete state',flush=True);continue
|
| 94 |
+
if s['format']==LEGACY_FORMAT:
|
| 95 |
+
if not self.main or s['pipeline']!=LEGACY_PIPELINE:raise RuntimeError('Legacy pipeline was not the audited attachment; refusing implicit migration')
|
| 96 |
+
print('Migrating attached SparkBET weights, optimizer and data cursors. New objective: one pass, no auxiliary recurrence. Missing dataset partitions append at the saved revision.',flush=True)
|
| 97 |
+
print(f'Resuming {self.run_id}, update {s["step"]}, {"Hub" if remote else "local"}',flush=True)
|
| 98 |
+
return s
|
| 99 |
+
if items:raise RuntimeError('No checkpoint passed verification; fresh initialization refused')
|
| 100 |
+
if self.main and self.cfg['hub_resume'] and 'model.safetensors' in self.remote_files and not self.cfg.get('train_from_run_id'):
|
| 101 |
+
raise RuntimeError('Root weights exist but no compatible full-state checkpoint was found. Select TRAIN_FROM_RUN_ID to promote a compatible ablation; a silent fresh main run is refused.')
|
| 102 |
+
return None
|
| 103 |
+
def restore_ledger(self):
|
| 104 |
+
path=self.root/'experiments.csv'
|
| 105 |
+
if self.cfg['hub_resume'] and 'experiments.csv' in self.remote_files:
|
| 106 |
+
f=retry(lambda:hf_hub_download(self.repo,'experiments.csv',revision=self.head,cache_dir=str(self.root/'hub_cache')))
|
| 107 |
+
remote=list(csv.DictReader(open(f)));local=list(csv.DictReader(path.open())) if path.exists() else []
|
| 108 |
+
for row in remote+local:ledger(path,row)
|
| 109 |
+
def publish(self,path,model,manifest,meta):
|
| 110 |
+
if not self.cfg['upload']:return
|
| 111 |
+
# Fetch a head before building an optimistic atomic commit. A concurrent
|
| 112 |
+
# writer causes a clear conflict; never overwrite its root or ledger.
|
| 113 |
+
previous_head=self.head
|
| 114 |
+
self.inventory()
|
| 115 |
+
if previous_head is not None and self.head!=previous_head:
|
| 116 |
+
self.head=previous_head
|
| 117 |
+
raise RuntimeError('Hub changed since this run last read/published it; resume to merge state instead of overwriting another writer')
|
| 118 |
+
dest=self.run/'export';dest.mkdir(exist_ok=True)
|
| 119 |
+
if self.main and meta['variant']=='sparkbet':
|
| 120 |
+
export=Publisher(self.repo,self.run)
|
| 121 |
+
class LocalOnly:
|
| 122 |
+
def upload_folder(self,**kwargs):pass
|
| 123 |
+
export.api=LocalOnly();export.export(model,int(Path(path).name.split('-')[-1]),manifest,Path(__file__).parent)
|
| 124 |
+
config=json.loads((dest/'config.json').read_text());config.update(refinement_cycles=1,precision='bf16 autocast where supported; fp32 master')
|
| 125 |
+
atomic_json(dest/'config.json',config)
|
| 126 |
+
shutil.copy2(Path(__file__).parent/'UNIFIED_README.md',dest/'README.md')
|
| 127 |
+
else:
|
| 128 |
+
from safetensors.torch import save_file
|
| 129 |
+
save_file({k:v.detach().cpu().contiguous().clone() for k,v in model.state_dict().items()},str(dest/'model.safetensors'))
|
| 130 |
+
atomic_json(dest/'experiment_config.json',meta)
|
| 131 |
+
if self.main:
|
| 132 |
+
atomic_json(dest/'config.json',dict(architecture=meta['variant'],custom_loader='unified_models.build',config=meta['config'],step=int(Path(path).name.split('-')[-1])))
|
| 133 |
+
(dest/'README.md').write_text('# Unified canvas / byte experiment\n\nCustom scaled architecture. Fetch the matching `unified/main` source modules and use `unified_models.build(variant, context, seed)`, then load `model.safetensors` strictly. This export does not advertise Transformers AutoModel compatibility. Full optimizer/data/RNG resume is in the same commit.\n')
|
| 134 |
+
if 'data_progress' in meta:atomic_json(dest/'data_progress.json',meta['data_progress'])
|
| 135 |
+
operations=[];remote_prefix=self.prefix+'/'+Path(path).name
|
| 136 |
+
for p in Path(path).iterdir():
|
| 137 |
+
if p.is_file():operations.append(CommitOperationAdd(path_in_repo=remote_prefix+'/'+p.name,path_or_fileobj=str(p)))
|
| 138 |
+
for p in dest.iterdir():
|
| 139 |
+
if p.is_file():operations.append(CommitOperationAdd(path_in_repo=p.name if self.main else self.prefix+'/inference/'+p.name,path_or_fileobj=str(p)))
|
| 140 |
+
# Save the exact implementation beside every exported configuration.
|
| 141 |
+
for p in Path(__file__).parent.glob('*.py'):
|
| 142 |
+
operations.append(CommitOperationAdd(path_in_repo=self.prefix+'/source/'+p.name,path_or_fileobj=str(p)))
|
| 143 |
+
anchor=self.root/(self.cfg['experiment_id']+'-anchor.pt')
|
| 144 |
+
if anchor.exists() and not self.main:operations.append(CommitOperationAdd(path_in_repo='unified/experiments/'+self.cfg['experiment_id']+'/anchor.pt',path_or_fileobj=str(anchor)))
|
| 145 |
+
csvpath=self.root/'experiments.csv'
|
| 146 |
+
if csvpath.exists():operations.append(CommitOperationAdd(path_in_repo='experiments.csv',path_or_fileobj=str(csvpath)))
|
| 147 |
+
old=sorted({f.rsplit('/',1)[0] for f in self.remote_files if f.startswith(self.prefix+'/checkpoint-') and f.endswith('/COMPLETE')})
|
| 148 |
+
keep=set(sorted(set(old+[remote_prefix]))[-self.cfg.get('keep_hub',2):])
|
| 149 |
+
for f in self.remote_files:
|
| 150 |
+
if f.startswith(self.prefix+'/checkpoint-') and f.rsplit('/',1)[0] not in keep:operations.append(CommitOperationDelete(path_in_repo=f))
|
| 151 |
+
result=self.api.create_commit(repo_id=self.repo,operations=operations,parent_commit=self.head,
|
| 152 |
+
commit_message=f'{self.run_id}: state and inference synchronized at {Path(path).name}')
|
| 153 |
+
self.head=result.oid
|