Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +274 -0
- RUNBOOK.md +694 -0
- gpu-sft/.gitignore +6 -0
- gpu-sft/EVAL_RESULTS.md +50 -0
- gpu-sft/README.md +47 -0
- gpu-sft/claude/compile_results_local.py +1220 -0
- gpu-sft/claude/evals.md +38 -0
- gpu-sft/claude/make_fixture_eval_tree.py +183 -0
- gpu-sft/experiments/qwen3_chat_template.py +108 -0
- gpu-sft/requirements-gpu.txt +19 -0
- gpu-sft/scripts/gpu_eval/bootstrap_gpu_eval.sh +194 -0
- gpu-sft/scripts/gpu_eval/gpu_eval_driver.py +996 -0
- gpu-sft/scripts/gpu_eval/gpu_eval_monitor.py +486 -0
- gpu-sft/scripts/gpu_eval/patch_evalchemy_gpu.py +274 -0
- gpu-sft/scripts/gpu_eval/run_all_evals.sh +60 -0
- gpu-sft/scripts/gpu_sft/accelerate_fsdp_qwen3.yaml +41 -0
- gpu-sft/scripts/gpu_sft/deepspeed_zero3_qwen3.json +40 -0
- gpu-sft/scripts/gpu_sft/launch_sft_8b.sh +108 -0
- gpu-sft/scripts/gpu_sft/prepare_sft_data.py +525 -0
- gpu-sft/scripts/gpu_sft/push_exports.py +98 -0
- gpu-sft/scripts/gpu_sft/run_sft.sh +131 -0
- gpu-sft/scripts/gpu_sft/train_sft_qwen3.py +820 -0
- scripts/combine_and_publish.py +171 -0
- scripts/launch_shards.sh +36 -0
- scripts/monitor.sh +44 -0
- scripts/upload_my_shards.py +66 -0
- sdg/__init__.py +1 -0
- sdg/__main__.py +4 -0
- sdg/config.py +94 -0
- sdg/configs/qwen3_0.6b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml +41 -0
- sdg/configs/qwen3_0.6b_openthoughts4_code9K_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_0.6b_openthoughts4_science26K_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_0.6b_rstarcoder_seedsft_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_1.7b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml +41 -0
- sdg/configs/qwen3_1.7b_openthoughts4_code9K_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_1.7b_openthoughts4_science26K_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_32b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml +41 -0
- sdg/configs/qwen3_32b_openthoughts4_code9K_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_32b_openthoughts4_science26K_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_32b_rstarcoder_seedsft_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_4b_mot_math_instill_n4_valredundancy3_round1.yaml +42 -0
- sdg/configs/qwen3_4b_nemotron_cascade2_science100k_instill_n8_valredundancy5_round1.yaml +42 -0
- sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy1_round1.yaml +41 -0
- sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy3_round1.yaml +41 -0
- sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy5_round1.yaml +41 -0
- sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy1_round1.yaml +41 -0
- sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy3_round1.yaml +41 -0
- sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy5_round1.yaml +41 -0
- sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_no_filter_round1.yaml +41 -0
- sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_simple_valredundancy5_round1.yaml +41 -0
README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SDG — Distributed Sharded Pipeline
|
| 2 |
+
|
| 3 |
+
Generate synthetic data with vLLM + uncertainty-quantification validation, distributed across multiple GPUs via SLURM sharding with a shared MongoDB inference cache.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pip install -r sdg/requirements.txt
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
Install MongoDB (download binary):
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-8.0.4.tgz
|
| 15 |
+
tar xzf mongodb-linux-x86_64-ubuntu2204-8.0.4.tgz
|
| 16 |
+
cp mongodb-linux-x86_64-ubuntu2204-8.0.4/bin/mongod /u/nlp/anaconda/main/anaconda3/envs/tonyreasoningtraces/bin/
|
| 17 |
+
mongod --version
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
## Quick Start (Single GPU, No Sharding)
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
python -m sdg.generate --config sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml --limit 100
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
This requires `mongo_uri` set in the config YAML (see below).
|
| 27 |
+
|
| 28 |
+
## Distributed Sharded Run
|
| 29 |
+
|
| 30 |
+
### 1. Start MongoDB
|
| 31 |
+
|
| 32 |
+
Launch a MongoDB server on a cluster node (no GPU needed) with max TTL of 21 days:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
nlprun -a tonyreasoningtraces --time 21-0 -m john3 --exclude john17 -c 4 -g 0 --memory 64g \
|
| 36 |
+
-w /nlp/scr4/nlp/crfm/text2image/text2image-rlhf/reasoning/virtual-world-data \
|
| 37 |
+
--job-name sdg-mongodb "python -m sdg.launch_mongodb 2>&1 | tee mongo.log"
|
| 38 |
+
|
| 39 |
+
nlprun -a tonyreasoningtraces --time 21-0 -q jag -p high -m jagupard39 --exclude john17 -c 4 -g 0 --memory 64g \
|
| 40 |
+
-w /nlp/scr4/nlp/crfm/text2image/text2image-rlhf/reasoning/virtual-world-data \
|
| 41 |
+
--job-name sdg-serve "python -m sdg.launch_mongodb 2>&1 | tee mongo.log"
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
Or locally:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
python -m sdg.launch_mongodb --port 27017 --dbpath /tmp/sdg_mongo
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
It will print `mongo-uri: mongodb://<hostname>:27017` to stdout (and `mongo.log`).
|
| 51 |
+
|
| 52 |
+
### 2. Set `mongo_uri` and `num_shards` in your config
|
| 53 |
+
|
| 54 |
+
Grab the hostname from `mongo.log`, then edit your YAML config (e.g. `sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml`):
|
| 55 |
+
|
| 56 |
+
```yaml
|
| 57 |
+
mongo_uri: "mongodb://<hostname>:27017"
|
| 58 |
+
num_shards: 32
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### 3. Launch shards
|
| 62 |
+
|
| 63 |
+
```commandline
|
| 64 |
+
conda activate tonyreasoningtraces && cd /nlp/scr4/nlp/crfm/text2image/text2image-rlhf/reasoning/virtual-world-data
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Dry run first to verify commands:
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml launch --dry-run
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
Then launch for real:
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml launch
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
This reads `num_shards` from the config and submits that many nlprun jobs (1 GPU each). Each job processes `ceil(total_seeds / num_shards)` examples and writes to `output/{experiment_name}/shards/shard_NNN/`. You can override with `--num-shards N` on the CLI.
|
| 80 |
+
|
| 81 |
+
### 4. Check status
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml check
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Prints a table like:
|
| 88 |
+
|
| 89 |
+
```
|
| 90 |
+
Shard Status Seeds Passed Failed
|
| 91 |
+
000 completed 2930 2100 830
|
| 92 |
+
001 running - - -
|
| 93 |
+
002 failed - - -
|
| 94 |
+
...
|
| 95 |
+
Overall: 20/32 completed, 8/32 running, 4/32 failed
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
When all shards are completed, `check` automatically:
|
| 99 |
+
- Combines all shard `output.jsonl` files into `output/{experiment_name}/output.jsonl`
|
| 100 |
+
- Aggregates all `stats.json` into a combined `output/{experiment_name}/stats.json`
|
| 101 |
+
- Uploads to HuggingFace at `teetone/{experiment_name}`
|
| 102 |
+
|
| 103 |
+
### 5. Rerun failed shards
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml rerun
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
This re-launches only `failed` shards (skips `completed`, `running`, and `not_started`).
|
| 110 |
+
|
| 111 |
+
## Output Directory Structure
|
| 112 |
+
|
| 113 |
+
```
|
| 114 |
+
output/{experiment_name}/
|
| 115 |
+
launch_meta.json # Shard metadata
|
| 116 |
+
config.yaml # Config copy
|
| 117 |
+
output.jsonl # Combined output (after all shards done)
|
| 118 |
+
stats.json # Aggregated stats (after all shards done)
|
| 119 |
+
logs/
|
| 120 |
+
shard_000.log
|
| 121 |
+
shard_001.log
|
| 122 |
+
...
|
| 123 |
+
shards/
|
| 124 |
+
shard_000/
|
| 125 |
+
output.jsonl
|
| 126 |
+
stats.json
|
| 127 |
+
shard_001/
|
| 128 |
+
output.jsonl
|
| 129 |
+
stats.json
|
| 130 |
+
...
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
## Example: OpenThoughts4 with 32 shards
|
| 134 |
+
|
| 135 |
+
```bash
|
| 136 |
+
# 1. Start MongoDB on john (no GPU, 16 GB RAM)
|
| 137 |
+
nlprun -a tonyreasoningtraces -q john --exclude john17 -c 4 -g 0 --memory 64g \
|
| 138 |
+
-w /nlp/scr4/nlp/crfm/text2image/text2image-rlhf/reasoning/virtual-world-data \
|
| 139 |
+
--job-name sdg-mongodb "python -m sdg.launch_mongodb 2>&1 | tee mongo.log"
|
| 140 |
+
|
| 141 |
+
# 2. Grab the hostname from the job output, then set mongo_uri in your config
|
| 142 |
+
# mongo_uri: "mongodb://<hostname>:27017"
|
| 143 |
+
|
| 144 |
+
# 3. Launch
|
| 145 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml launch
|
| 146 |
+
|
| 147 |
+
# 4. Monitor
|
| 148 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml check
|
| 149 |
+
|
| 150 |
+
# 5. Rerun any failures
|
| 151 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_math30K_instill_n4_valredundancy3_round1.yaml rerun
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
## CLI Reference
|
| 155 |
+
|
| 156 |
+
| Command | Description |
|
| 157 |
+
|---------|-------------|
|
| 158 |
+
| `python -m sdg.generate --config CONFIG` | Run pipeline on a single GPU |
|
| 159 |
+
| `python -m sdg.generate --config CONFIG --shard-id I --num-shards N` | Run a single shard |
|
| 160 |
+
| `python -m sdg.launch_mongodb` | Start MongoDB server |
|
| 161 |
+
| `python -m sdg.launcher --config CONFIG launch` | Launch all shards via nlprun |
|
| 162 |
+
| `python -m sdg.launcher --config CONFIG launch --dry-run` | Print commands without executing |
|
| 163 |
+
| `python -m sdg.launcher --config CONFIG check` | Check shard status, combine if done |
|
| 164 |
+
| `python -m sdg.launcher --config CONFIG rerun` | Rerun failed shards only |
|
| 165 |
+
| `python -m sdg.launcher --config CONFIG rerun --dry-run` | Show which failed shards would be relaunched |
|
| 166 |
+
|
| 167 |
+
## Launch Jobs
|
| 168 |
+
|
| 169 |
+
### Qwen3 4B
|
| 170 |
+
|
| 171 |
+
#### Launch OpenThoughts3 Math53K
|
| 172 |
+
|
| 173 |
+
```bash
|
| 174 |
+
# n=1, val_redundancy=3 - DONE
|
| 175 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy3_round1.yaml launch
|
| 176 |
+
|
| 177 |
+
# n=1, val_redundancy=1 - DONE
|
| 178 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy1_round1.yaml launch
|
| 179 |
+
|
| 180 |
+
# n=8, val_redundancy=1 - DONE
|
| 181 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy1_round1.yaml launch
|
| 182 |
+
|
| 183 |
+
# n=8, val_redundancy=3 - DONE
|
| 184 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy3_round1.yaml launch
|
| 185 |
+
|
| 186 |
+
# n=1, val_redundancy=5
|
| 187 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy5_round1.yaml launch
|
| 188 |
+
|
| 189 |
+
# n=4, val_redundancy=5
|
| 190 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy5_round1.yaml launch
|
| 191 |
+
|
| 192 |
+
# n=8, val_redundancy=5 - RUNNING
|
| 193 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml launch
|
| 194 |
+
|
| 195 |
+
# n=8, no_filter (no UQ validation)
|
| 196 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_no_filter_round1.yaml launch
|
| 197 |
+
|
| 198 |
+
# n=8, val_redundancy=5, all_valid (keep all samples that pass UQ)
|
| 199 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_allvalid_round1.yaml launch
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
#### Launch OpenThoughts4 Science26K
|
| 203 |
+
|
| 204 |
+
```bash
|
| 205 |
+
# n=4, val_redundancy=3 - DONE
|
| 206 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_science26K_instill_n4_valredundancy3_round1.yaml launch
|
| 207 |
+
|
| 208 |
+
# n=1, val_redundancy=1 - DONE
|
| 209 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_science26K_instill_n1_valredundancy1_round1.yaml launch
|
| 210 |
+
|
| 211 |
+
# n=8, val_redundancy=3 - DONE
|
| 212 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_science26K_instill_n8_valredundancy3_round1.yaml launch
|
| 213 |
+
|
| 214 |
+
# n=8, val_redundancy=5 - DONE
|
| 215 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_science26K_instill_n8_valredundancy5_round1.yaml launch
|
| 216 |
+
|
| 217 |
+
# n=8, no_filter (no UQ validation) - RUNNING
|
| 218 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_science26K_instill_n8_no_filter_round1.yaml launch
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
#### Launch OpenThoughts4 Code9K
|
| 222 |
+
|
| 223 |
+
```bash
|
| 224 |
+
# n=4, val_redundancy=3 - DONE
|
| 225 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_code9K_instill_n4_valredundancy3_round1.yaml launch
|
| 226 |
+
|
| 227 |
+
# n=1, val_redundancy=1 - DONE
|
| 228 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_code9K_instill_n1_valredundancy1_round1.yaml launch
|
| 229 |
+
|
| 230 |
+
# n=8, val_redundancy=3 - DONE
|
| 231 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_code9K_instill_n8_valredundancy3_round1.yaml launch
|
| 232 |
+
|
| 233 |
+
# n=8, val_redundancy=5 - DONE
|
| 234 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_code9K_instill_n8_valredundancy5_round1.yaml launch
|
| 235 |
+
|
| 236 |
+
# n=8, no_filter (no UQ validation) - RUNNING
|
| 237 |
+
python -m sdg.launcher --config sdg/configs/qwen3_4b_openthoughts4_code9K_instill_n8_no_filter_round1.yaml launch
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
### Qwen3 8B
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
#### Launch OpenThoughts4 Science26K (Qwen3-8B)
|
| 244 |
+
|
| 245 |
+
```bash
|
| 246 |
+
# n=8, val_redundancy=5 - DONE
|
| 247 |
+
python -m sdg.launcher --config sdg/configs/qwen3_8b_openthoughts4_science26K_instill_n8_valredundancy5_round1.yaml launch
|
| 248 |
+
|
| 249 |
+
# n=4, val_redundancy=5 - RUNNING
|
| 250 |
+
python -m sdg.launcher --config sdg/configs/qwen3_8b_openthoughts4_science26K_instill_n4_valredundancy5_round1.yaml launch
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
#### Launch OpenThoughts4 Code9K (Qwen3-8B)
|
| 254 |
+
|
| 255 |
+
```bash
|
| 256 |
+
# n=8, val_redundancy=5 - DONE
|
| 257 |
+
python -m sdg.launcher --config sdg/configs/qwen3_8b_openthoughts4_code9K_instill_n8_valredundancy5_round1.yaml launch
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
#### Launch OpenThoughts4 Math219K (Qwen3-8B)
|
| 261 |
+
|
| 262 |
+
```bash
|
| 263 |
+
# n=8, val_redundancy=5 - RUNNING
|
| 264 |
+
python -m sdg.launcher --config sdg/configs/qwen3_8b_openthoughts4_math219K_instill_n8_valredundancy5_round1.yaml launch
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
# Other
|
| 268 |
+
|
| 269 |
+
## Check logs
|
| 270 |
+
|
| 271 |
+
```bash
|
| 272 |
+
scp -r tonyhlee@scdt.stanford.edu:/nlp/scr4/nlp/crfm/text2image/text2image-rlhf/reasoning/virtual-world-data/output/qwen3_0.6b_openthoughts3_math53K_instill_n8_valredundancy5_round1 /Users/tonyhlee/Dev/virtual-world-data/
|
| 273 |
+
```
|
| 274 |
+
|
RUNBOOK.md
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-Node SDG Pipeline Runbook
|
| 2 |
+
|
| 3 |
+
End-to-end recipe for running the SDG pipeline distributed across **4 worker
|
| 4 |
+
nodes × 8 H100 GPUs = 32 shards**, with cross-node MongoDB caching and HuggingFace
|
| 5 |
+
output gathering. Tested on ByteDance Arnold (Kubernetes-backed Azure ND96isr_H100_v5).
|
| 6 |
+
|
| 7 |
+
- Target config: `sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml`
|
| 8 |
+
- Final dataset: published to `<your-hf-username>/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1`
|
| 9 |
+
- Wall time: **~40–60 hours** end-to-end
|
| 10 |
+
- HF username placeholder used below: `fzzhang` — replace with yours
|
| 11 |
+
|
| 12 |
+
If the cluster dies, follow this runbook top-to-bottom on a fresh allocation.
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## 0. Cluster prerequisites
|
| 17 |
+
|
| 18 |
+
- 4 worker nodes, each with 8× H100 (80 GB), shared no-filesystem (per-pod overlay).
|
| 19 |
+
- NVIDIA driver supporting at least CUDA 12.4 (driver 535+ is fine).
|
| 20 |
+
- HF account with write access to the target dataset namespace.
|
| 21 |
+
- Each worker has these Arnold-provided env vars: `ARNOLD_WORKER_0_HOST`,
|
| 22 |
+
`ARNOLD_WORKER_0_PORT`, `MY_POD_NAME` (ends with `worker-N`).
|
| 23 |
+
- Cross-worker IPv6 traffic on the `ARNOLD_WORKER_*_PORT` ports is open by default —
|
| 24 |
+
verify in §3 below.
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## 1. Per-worker bootstrap (run on EVERY worker, in parallel)
|
| 29 |
+
|
| 30 |
+
### 1.1 Clone the repo
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
cd ~/lmc_muon # or your preferred dir
|
| 34 |
+
# git clone or rsync this repo into ./self-verified-distillation
|
| 35 |
+
cd self-verified-distillation
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### 1.2 Create the conda env and install pinned dependencies
|
| 39 |
+
|
| 40 |
+
The driver caps at CUDA 12.4, so vLLM 0.8.5 + torch 2.6.0+cu124 is the highest
|
| 41 |
+
stack that runs. Newer vLLM (≥0.9) ships cu126/cu130 wheels which **will not
|
| 42 |
+
run on driver 535**.
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
conda create -n sdg python=3.12 -y
|
| 46 |
+
conda activate sdg
|
| 47 |
+
|
| 48 |
+
pip install -U uv
|
| 49 |
+
uv pip install -r sdg/requirements.txt
|
| 50 |
+
|
| 51 |
+
# Downgrade to cu12.4-compatible stack
|
| 52 |
+
uv pip uninstall vllm torch torchvision torchaudio flashinfer-python triton
|
| 53 |
+
uv pip install torch==2.6.0 torchvision==0.21.0 \
|
| 54 |
+
--index-url https://download.pytorch.org/whl/cu124
|
| 55 |
+
uv pip install "vllm==0.8.5" "transformers>=4.51.3,<5.0"
|
| 56 |
+
|
| 57 |
+
# Sanity check — MUST print "OK 1.0"
|
| 58 |
+
python -c "import torch; x=torch.zeros(1).cuda(); print('OK', (x+1).item())"
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### 1.3 HuggingFace login
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
huggingface-cli login # paste token; or set HF_TOKEN env var
|
| 65 |
+
python -c "from huggingface_hub import HfApi; print(HfApi().whoami()['name'])"
|
| 66 |
+
# should print your HF username (e.g. fzzhang)
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## 2. MongoDB on worker_0 (ONLY worker_0)
|
| 72 |
+
|
| 73 |
+
### 2.1 Install mongod
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
cd ~/lmc_muon/self-verified-distillation
|
| 77 |
+
curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-8.0.4.tgz
|
| 78 |
+
tar xzf mongodb-linux-x86_64-ubuntu2204-8.0.4.tgz
|
| 79 |
+
export PATH="$PWD/mongodb-linux-x86_64-ubuntu2204-8.0.4/bin:$PATH"
|
| 80 |
+
which mongod # must print a path
|
| 81 |
+
|
| 82 |
+
# Persist for new shells
|
| 83 |
+
echo "export PATH=\"$PWD/mongodb-linux-x86_64-ubuntu2204-8.0.4/bin:\$PATH\"" >> ~/.bashrc
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### 2.2 Start mongod bound to all interfaces with IPv6
|
| 87 |
+
|
| 88 |
+
The `--ipv6` flag is mandatory; without it `--bind_ip_all` is IPv4-only and
|
| 89 |
+
cross-worker traffic (IPv6 fabric) cannot reach mongod.
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
mkdir -p /tmp/sdg_mongo
|
| 93 |
+
nohup mongod --port $ARNOLD_WORKER_0_PORT --dbpath /tmp/sdg_mongo \
|
| 94 |
+
--bind_ip_all --ipv6 > mongo.log 2>&1 &
|
| 95 |
+
|
| 96 |
+
sleep 5
|
| 97 |
+
grep -i "Listening on" mongo.log
|
| 98 |
+
# Expected: BOTH 0.0.0.0:<port> AND [::]:<port>
|
| 99 |
+
|
| 100 |
+
# Local sanity
|
| 101 |
+
python3 -c "from pymongo import MongoClient; \
|
| 102 |
+
MongoClient(f'mongodb://[::1]:$ARNOLD_WORKER_0_PORT', serverSelectionTimeoutMS=5000).admin.command('ping'); \
|
| 103 |
+
print('mongo OK locally')"
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
## 3. Verify cross-worker MongoDB reachability
|
| 109 |
+
|
| 110 |
+
On workers 1, 2, 3:
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
python3 -c "
|
| 114 |
+
import os
|
| 115 |
+
from pymongo import MongoClient
|
| 116 |
+
host = os.environ['ARNOLD_WORKER_0_HOST']
|
| 117 |
+
port = os.environ['ARNOLD_WORKER_0_PORT']
|
| 118 |
+
uri = f'mongodb://[{host}]:{port}'
|
| 119 |
+
MongoClient(uri, serverSelectionTimeoutMS=5000).admin.command('ping')
|
| 120 |
+
print(f'mongo OK from {os.uname().nodename} → {uri}')
|
| 121 |
+
"
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
You should see `mongo OK from ...` on each. If any worker times out, the
|
| 125 |
+
platform is blocking inter-worker traffic on that port — do not proceed.
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 4. Patch the config on each worker
|
| 130 |
+
|
| 131 |
+
Point `mongo_uri` at the IPv6 MongoDB and set `num_shards=32` to use all GPUs.
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
# Run on EACH worker
|
| 135 |
+
python3 <<'EOF'
|
| 136 |
+
import os, yaml
|
| 137 |
+
host = os.environ['ARNOLD_WORKER_0_HOST']
|
| 138 |
+
port = os.environ['ARNOLD_WORKER_0_PORT']
|
| 139 |
+
path = 'sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml'
|
| 140 |
+
with open(path) as f: cfg = yaml.safe_load(f)
|
| 141 |
+
cfg['mongo_uri'] = f'mongodb://[{host}]:{port}'
|
| 142 |
+
cfg['num_shards'] = 32
|
| 143 |
+
with open(path, 'w') as f: yaml.dump(cfg, f, sort_keys=False)
|
| 144 |
+
print('updated:', cfg['mongo_uri'])
|
| 145 |
+
EOF
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
---
|
| 149 |
+
|
| 150 |
+
## 5. Helper scripts
|
| 151 |
+
|
| 152 |
+
All orchestration scripts live in `scripts/` (committed to the repo, so each
|
| 153 |
+
worker has them after `git clone`). They are designed to be **runnable as-is**;
|
| 154 |
+
override behavior with env vars rather than editing the files:
|
| 155 |
+
|
| 156 |
+
| Script | Purpose | Run on |
|
| 157 |
+
|---|---|---|
|
| 158 |
+
| `scripts/launch_shards.sh` | Launch 8 shards on this worker, one per GPU. Honors `CONFIG`, `LIMIT`, `NUM_SHARDS`. | every worker |
|
| 159 |
+
| `scripts/upload_my_shards.py` | Poll for this worker's 8 stats.json, then upload to staging HF repo. Honors `HF_USER`, `RUN_NAME`. | every worker |
|
| 160 |
+
| `scripts/combine_and_publish.py` | Poll for 4 worker-done markers, gather, combine, publish to HF (full launcher.py stats schema). Honors `HF_USER`, `RUN_NAME`, `NUM_WORKERS`, `NUM_SHARDS`. | worker_0 only |
|
| 161 |
+
| `scripts/monitor.sh` | Per-worker live status (GPUs, current shard progress, completion counts, mongo cache size via cross-worker IPv6). | any worker |
|
| 162 |
+
|
| 163 |
+
Defaults target the math53K config + `HF_USER=fzzhang` + 4 workers × 32 shards.
|
| 164 |
+
**Why scripts/, not inline heredocs:** during the original run, terminal
|
| 165 |
+
autocomplete corrupted multi-line heredoc pastes (Python files came out
|
| 166 |
+
unparseable). Committed files bypass paste entirely — you just `git pull` and
|
| 167 |
+
run them.
|
| 168 |
+
|
| 169 |
+
**Verify they're present and parse on each worker:**
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
ls scripts/
|
| 173 |
+
python -c "import ast; ast.parse(open('scripts/upload_my_shards.py').read()); ast.parse(open('scripts/combine_and_publish.py').read()); print('OK')"
|
| 174 |
+
bash -n scripts/launch_shards.sh && bash -n scripts/monitor.sh && echo "shell OK"
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
If you need a different HF account or a different config, export the env vars
|
| 178 |
+
before running (no file edits needed):
|
| 179 |
+
|
| 180 |
+
```bash
|
| 181 |
+
export HF_USER=youraccount
|
| 182 |
+
export RUN_NAME=your_run_name_matching_your_config
|
| 183 |
+
export CONFIG=sdg/configs/your_config.yaml
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
---
|
| 187 |
+
|
| 188 |
+
## 6. Per-worker uploader
|
| 189 |
+
|
| 190 |
+
`scripts/upload_my_shards.py` runs on **every worker** — sleep-polls every 2
|
| 191 |
+
minutes for that worker's 8 `stats.json` to appear, then uploads each
|
| 192 |
+
`shards/shard_NNN/` dir to `<HF_USER>/<RUN_NAME>-staging` and writes a
|
| 193 |
+
`workers/worker_N.done` marker so the combiner knows that worker is finished.
|
| 194 |
+
|
| 195 |
+
Consumes near-zero resources while waiting — safe to launch any time (before,
|
| 196 |
+
during, or after the shards run). No file edits needed; override `HF_USER` /
|
| 197 |
+
`RUN_NAME` via env if you're not using defaults (see §5).
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
## 7. Combiner (worker_0 only)
|
| 202 |
+
|
| 203 |
+
`scripts/combine_and_publish.py` runs on **worker_0 only** — sleep-polls for all
|
| 204 |
+
4 worker-done markers, downloads the staging repo, concatenates the 32
|
| 205 |
+
`output.jsonl` files, aggregates stats with the **full launcher.py schema**
|
| 206 |
+
(`summary`, `checks`, `failure_breakdown`, `failure_breakdown_examples`,
|
| 207 |
+
`num_shards`), enforces a row-count vs `total_passed` integrity check, and
|
| 208 |
+
publishes to `<HF_USER>/<RUN_NAME>` with a HuggingFace dataset-card README.
|
| 209 |
+
|
| 210 |
+
Set `HF_USER` / `RUN_NAME` / `NUM_WORKERS` / `NUM_SHARDS` via env if defaults
|
| 211 |
+
don't match.
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## 8. Optional: smoke test before committing 40+ hours
|
| 216 |
+
|
| 217 |
+
Tiny end-to-end validation (~15 min on all 32 GPUs). Confirms multi-worker
|
| 218 |
+
plumbing without burning real time. Run **on each worker**:
|
| 219 |
+
|
| 220 |
+
```bash
|
| 221 |
+
LIMIT=128 ./scripts/launch_shards.sh
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
Wait ~15 min, then check completion (on each worker):
|
| 225 |
+
|
| 226 |
+
```bash
|
| 227 |
+
grep -l "PIPELINE COMPLETE" logs/worker_*/shard_*.log | wc -l # should be 8
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
If all four workers report 8, plumbing is verified. Clear the smoke outputs
|
| 231 |
+
before the real run:
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
# On each worker
|
| 235 |
+
rm -rf output/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1
|
| 236 |
+
rm -rf logs/worker_*
|
| 237 |
+
# Optionally wipe the cache too (on worker_0)
|
| 238 |
+
pkill mongod && sleep 2
|
| 239 |
+
rm -rf /tmp/sdg_mongo/*
|
| 240 |
+
# Restart mongod (see §2.2)
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
---
|
| 244 |
+
|
| 245 |
+
## 9. Production launch
|
| 246 |
+
|
| 247 |
+
### 9.1 Launch shards on each worker
|
| 248 |
+
|
| 249 |
+
```bash
|
| 250 |
+
./scripts/launch_shards.sh
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
### 9.2 Start the per-worker uploader on each worker
|
| 254 |
+
|
| 255 |
+
It will sleep-poll until its 8 shards complete, then upload.
|
| 256 |
+
|
| 257 |
+
```bash
|
| 258 |
+
nohup python scripts/upload_my_shards.py > upload.log 2>&1 &
|
| 259 |
+
disown
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
### 9.3 Start the combiner on worker_0 ONLY
|
| 263 |
+
|
| 264 |
+
It will sleep-poll until all 4 worker-done markers exist on HF, then combine
|
| 265 |
+
and publish.
|
| 266 |
+
|
| 267 |
+
```bash
|
| 268 |
+
nohup python scripts/combine_and_publish.py > combine.log 2>&1 &
|
| 269 |
+
disown
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
After this, you can disconnect all terminals. Work continues under `nohup`.
|
| 273 |
+
|
| 274 |
+
### 9.4 (Recommended) Verify integrity once shards complete
|
| 275 |
+
|
| 276 |
+
Before the uploader kicks off uploads, you can sanity-check each shard locally.
|
| 277 |
+
For `selection_policy: first_valid` (the default), `output.jsonl` line count
|
| 278 |
+
must equal `total_passed` in `stats.json`. Run **on each worker**:
|
| 279 |
+
|
| 280 |
+
```bash
|
| 281 |
+
RUN=output/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1/shards
|
| 282 |
+
for d in $RUN/shard_*; do
|
| 283 |
+
lines=$(wc -l < $d/output.jsonl 2>/dev/null)
|
| 284 |
+
passed=$(python3 -c "import json; print(json.load(open('$d/stats.json'))['summary']['total_passed'])" 2>/dev/null)
|
| 285 |
+
echo "$(basename $d): $lines rows | total_passed=$passed"
|
| 286 |
+
done
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
Lines must match `total_passed` for every shard. The combiner re-enforces this
|
| 290 |
+
check globally and will `raise RuntimeError` if anything diverges — catching it
|
| 291 |
+
locally first is faster.
|
| 292 |
+
|
| 293 |
+
---
|
| 294 |
+
|
| 295 |
+
## 10. Monitoring
|
| 296 |
+
|
| 297 |
+
### 10.1 Per-worker live view: `scripts/monitor.sh`
|
| 298 |
+
|
| 299 |
+
```bash
|
| 300 |
+
watch -n 30 ./scripts/monitor.sh
|
| 301 |
+
# Or if `watch` isn't installed in the container:
|
| 302 |
+
while true; do clear; ./scripts/monitor.sh; sleep 30; done
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
Each refresh shows: per-GPU util, the most-recent tqdm state for each shard
|
| 306 |
+
(decoded from `\r`-overwritten progress bars), per-worker completion counts,
|
| 307 |
+
and the cluster-wide MongoDB cache size (queried over IPv6 so it works from any
|
| 308 |
+
worker, not just worker_0).
|
| 309 |
+
|
| 310 |
+
⚠ **Do not use `tail -1 | cut -cN`** to read shard progress — tqdm writes one
|
| 311 |
+
physical line with `\r` overwrites, so `cut -cN` shows the line's *beginning*
|
| 312 |
+
(which never changes) while progress advances at the *end*. `monitor.sh`
|
| 313 |
+
correctly does `tail -c 500 | tr '\r' '\n' | tail -1`.
|
| 314 |
+
|
| 315 |
+
Expected sequence in the per-shard line over the run:
|
| 316 |
+
1. `Loading model:` / `Capturing CUDA graphs` (~2 min vLLM warmup)
|
| 317 |
+
2. `Processed prompts: N/512 [...]` (Stage 3 generation, batch size 64 × n=8 = 512)
|
| 318 |
+
3. `[Generation] N/1656 examples (...%)` (between-batch progress lines)
|
| 319 |
+
4. `Round 0: M pending rows` (Stage 4 UQ starts; tqdm batches now sized by pending rows)
|
| 320 |
+
5. `Cycle: ... Factual: ... Correctness: ...` (inside a UQ round)
|
| 321 |
+
6. `PIPELINE COMPLETE` (shard finished)
|
| 322 |
+
|
| 323 |
+
To definitively confirm a shard is in UQ rather than generation, grep the log:
|
| 324 |
+
|
| 325 |
+
```bash
|
| 326 |
+
grep -E "Round [0-9]+:|Cycle:|Factual:|Correctness:|\[Generation\]|PIPELINE" \
|
| 327 |
+
logs/worker_0/shard_0.log | tail -20
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
You'll see `[Generation] 1656/1656 examples (100.0%)` followed by `Round 0:` if
|
| 331 |
+
generation is done.
|
| 332 |
+
|
| 333 |
+
### 10.2 Aggregate signal: MongoDB cache size
|
| 334 |
+
|
| 335 |
+
`monitor.sh` already prints this. Standalone:
|
| 336 |
+
|
| 337 |
+
```bash
|
| 338 |
+
python3 -c "
|
| 339 |
+
from pymongo import MongoClient
|
| 340 |
+
import os
|
| 341 |
+
host = os.environ['ARNOLD_WORKER_0_HOST']
|
| 342 |
+
port = os.environ['ARNOLD_WORKER_0_PORT']
|
| 343 |
+
print(MongoClient(f'mongodb://[{host}]:{port}').sdg_cache.inference_cache.count_documents({}))
|
| 344 |
+
"
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
Grows continuously. Expect hundreds of thousands by mid-run, well over a
|
| 348 |
+
million during UQ. **Use the IPv6 host, not `127.0.0.1`** — only worker_0 can
|
| 349 |
+
reach loopback mongod.
|
| 350 |
+
|
| 351 |
+
### 10.3 Uploader / combiner status
|
| 352 |
+
|
| 353 |
+
```bash
|
| 354 |
+
tail -f upload.log # on any worker (file lives in working dir)
|
| 355 |
+
tail -f combine.log # on worker_0
|
| 356 |
+
```
|
| 357 |
+
|
| 358 |
+
The combiner prints `DONE: https://huggingface.co/datasets/<HF_USER>/<RUN_NAME>`
|
| 359 |
+
when finished — that's the success signal for the entire pipeline.
|
| 360 |
+
|
| 361 |
+
---
|
| 362 |
+
|
| 363 |
+
## 11. Troubleshooting
|
| 364 |
+
|
| 365 |
+
### Shards exit immediately
|
| 366 |
+
- `Exit 127`: `mongod` (or `python`) not on PATH. `which mongod`, re-source `~/.bashrc`.
|
| 367 |
+
- `Qwen2Tokenizer has no attribute all_special_tokens_extended`: transformers
|
| 368 |
+
too new for vLLM 0.8.5. `uv pip install "transformers>=4.51.3,<5.0"`.
|
| 369 |
+
- `CUDA driver version is insufficient`: torch+CUDA wheel doesn't match driver.
|
| 370 |
+
Reinstall via §1.2 exactly.
|
| 371 |
+
|
| 372 |
+
### MongoDB connection refused from other workers
|
| 373 |
+
- mongod log shows only `0.0.0.0:PORT` (no `[::]:PORT`): missing `--ipv6` flag.
|
| 374 |
+
`pkill mongod`, restart with `--bind_ip_all --ipv6`.
|
| 375 |
+
|
| 376 |
+
### `monitor.sh` shows shards stuck at `0%` for hours
|
| 377 |
+
- Almost always a display lie, not real stuckness. `tail -1 | cut -cN` would
|
| 378 |
+
show the beginning of a long `\r`-overwritten line forever; the actual
|
| 379 |
+
progress is at the *end*. Use `scripts/monitor.sh` (uses `tail -c 500 | tr '\r' '\n' | tail -1`).
|
| 380 |
+
- Confirm work is happening: GPU util ≥ 80%, log file `mtime` updates,
|
| 381 |
+
MongoDB cache count growing.
|
| 382 |
+
|
| 383 |
+
### `monitor.sh` shows `mongo cache: <unreachable>` from a non-zero worker
|
| 384 |
+
- Workers other than worker_0 cannot reach `127.0.0.1:PORT`. The script uses
|
| 385 |
+
`ARNOLD_WORKER_0_HOST` over IPv6, but if that env var isn't set in the
|
| 386 |
+
current shell, the query fails. Re-source whatever sets Arnold env vars or
|
| 387 |
+
run from a fresh login shell.
|
| 388 |
+
|
| 389 |
+
### Inline Python heredoc files won't parse
|
| 390 |
+
- Terminal autocomplete corrupts `cat > file <<'EOF' ... EOF`. Symptoms:
|
| 391 |
+
`unterminated string literal`, `import os, re, time Path` (extra word
|
| 392 |
+
appended), or scripts cut off mid-line.
|
| 393 |
+
- **Fix**: don't paste — use the committed files in `scripts/`. They survive
|
| 394 |
+
`git pull`. If you must hand-write, use `nano <file>` (editor paste bypasses
|
| 395 |
+
shell autocomplete) or base64-encode locally and `echo '...' | base64 -d > file`.
|
| 396 |
+
|
| 397 |
+
### Trial gets reclaimed mid-run
|
| 398 |
+
- Shard outputs on local disk are lost (no shared FS) and the MongoDB cache at
|
| 399 |
+
`/tmp/sdg_mongo` dies with worker_0.
|
| 400 |
+
- **Mitigation**: start the uploaders in §9.2 *at launch time*, not at the
|
| 401 |
+
end. They sleep-poll for completion and push completed work to HF
|
| 402 |
+
incrementally. If a worker dies after some shards finish but before its 8
|
| 403 |
+
are all done, you lose only the unfinished ones.
|
| 404 |
+
- Once a worker has written its `worker_N.done` marker on HF, that worker's
|
| 405 |
+
data is durable. The combiner can run from any machine afterward, including
|
| 406 |
+
a fresh trial.
|
| 407 |
+
|
| 408 |
+
### Some shards fail mid-run
|
| 409 |
+
- Each shard is independent; just relaunch the failed ones.
|
| 410 |
+
- Find failures: `grep -L "PIPELINE COMPLETE" logs/worker_*/shard_*.log`
|
| 411 |
+
- Relaunch one: `CUDA_VISIBLE_DEVICES=I nohup python -m sdg.generate --config <CONFIG> --shard-id N --num-shards 32 > logs/worker_W/shard_N.log 2>&1 &`
|
| 412 |
+
- MongoDB cache means most work is replayed for free on the rerun.
|
| 413 |
+
|
| 414 |
+
### Published `stats.json` is missing `failure_breakdown` fields
|
| 415 |
+
- Old `combine_and_publish.py` versions wrote a simplified stats schema.
|
| 416 |
+
Current `scripts/combine_and_publish.py` includes the full schema.
|
| 417 |
+
- If you have a published dataset with the old stats, you can re-aggregate
|
| 418 |
+
from `gathered/shards/*/stats.json` (still present on worker_0 after the
|
| 419 |
+
combine) and re-upload `stats.json` — same logic as the combiner's stats
|
| 420 |
+
section, just runs standalone.
|
| 421 |
+
|
| 422 |
+
---
|
| 423 |
+
|
| 424 |
+
## 12. After publish: verify and clean up
|
| 425 |
+
|
| 426 |
+
### 12.1 Verify the published dataset loads
|
| 427 |
+
|
| 428 |
+
```bash
|
| 429 |
+
python3 -c "
|
| 430 |
+
from datasets import load_dataset
|
| 431 |
+
ds = load_dataset('fzzhang/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1', split='train')
|
| 432 |
+
print('rows:', len(ds))
|
| 433 |
+
print('keys:', list(ds[0].keys()))
|
| 434 |
+
print('roles:', [m['role'] for m in ds[0]['messages']])
|
| 435 |
+
"
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
Should print the row count (~21k), `keys: ['messages']`, and the roles
|
| 439 |
+
(`['user', 'assistant']`). Confirms the HF dataset-card README correctly
|
| 440 |
+
auto-loads `output.jsonl`.
|
| 441 |
+
|
| 442 |
+
### 12.2 Clean up local + intermediate state
|
| 443 |
+
|
| 444 |
+
```bash
|
| 445 |
+
# On each worker — free disk if you plan to reuse the trial
|
| 446 |
+
rm -rf output/ logs/ gathered/ combined_output.jsonl combined_stats.json upload.log
|
| 447 |
+
|
| 448 |
+
# On worker_0
|
| 449 |
+
pkill mongod
|
| 450 |
+
rm -rf /tmp/sdg_mongo combine.log
|
| 451 |
+
|
| 452 |
+
# Optionally delete the intermediate staging HF repo
|
| 453 |
+
HF_USER=fzzhang RUN_NAME=qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1 python -c "
|
| 454 |
+
import os
|
| 455 |
+
from huggingface_hub import HfApi
|
| 456 |
+
repo = f\"{os.environ['HF_USER']}/{os.environ['RUN_NAME']}-staging\"
|
| 457 |
+
HfApi().delete_repo(repo_id=repo, repo_type='dataset')
|
| 458 |
+
print('deleted', repo)
|
| 459 |
+
"
|
| 460 |
+
```
|
| 461 |
+
|
| 462 |
+
The final HF dataset is independent of the staging repo and the MongoDB
|
| 463 |
+
cache — deleting them after the run is purely cosmetic.
|
| 464 |
+
|
| 465 |
+
---
|
| 466 |
+
|
| 467 |
+
## Step-by-step playbook
|
| 468 |
+
|
| 469 |
+
Every command in execution order. The "Where" column tells you which worker to
|
| 470 |
+
run the command on. Commands marked "every worker" should be run on workers 0,
|
| 471 |
+
1, 2, AND 3 (in parallel terminals to save wall time). Steps numbered with a
|
| 472 |
+
letter (e.g. 5a) are part of the same logical step but run on different
|
| 473 |
+
workers.
|
| 474 |
+
|
| 475 |
+
### Step 1 — get the code (every worker)
|
| 476 |
+
|
| 477 |
+
```bash
|
| 478 |
+
cd ~/lmc_muon # or wherever you keep the repo
|
| 479 |
+
git clone <your-repo-url> self-verified-distillation
|
| 480 |
+
cd self-verified-distillation
|
| 481 |
+
```
|
| 482 |
+
|
| 483 |
+
### Step 2 — Python env + pinned deps (every worker)
|
| 484 |
+
|
| 485 |
+
```bash
|
| 486 |
+
conda create -n sdg python=3.12 -y
|
| 487 |
+
conda activate sdg
|
| 488 |
+
pip install -U uv
|
| 489 |
+
uv pip install -r sdg/requirements.txt
|
| 490 |
+
uv pip uninstall vllm torch torchvision torchaudio flashinfer-python triton
|
| 491 |
+
uv pip install torch==2.6.0 torchvision==0.21.0 \
|
| 492 |
+
--index-url https://download.pytorch.org/whl/cu124
|
| 493 |
+
uv pip install "vllm==0.8.5" "transformers>=4.51.3,<5.0"
|
| 494 |
+
python -c "import torch; x=torch.zeros(1).cuda(); print('OK', (x+1).item())"
|
| 495 |
+
# Must print: OK 1.0
|
| 496 |
+
```
|
| 497 |
+
|
| 498 |
+
### Step 3 — HuggingFace login (every worker)
|
| 499 |
+
|
| 500 |
+
```bash
|
| 501 |
+
huggingface-cli login # paste your token
|
| 502 |
+
python -c "from huggingface_hub import HfApi; print(HfApi().whoami()['name'])"
|
| 503 |
+
# Must print your HF username
|
| 504 |
+
```
|
| 505 |
+
|
| 506 |
+
### Step 4 — install mongod (worker_0 ONLY)
|
| 507 |
+
|
| 508 |
+
```bash
|
| 509 |
+
cd ~/lmc_muon/self-verified-distillation
|
| 510 |
+
curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-8.0.4.tgz
|
| 511 |
+
tar xzf mongodb-linux-x86_64-ubuntu2204-8.0.4.tgz
|
| 512 |
+
export PATH="$PWD/mongodb-linux-x86_64-ubuntu2204-8.0.4/bin:$PATH"
|
| 513 |
+
echo "export PATH=\"$PWD/mongodb-linux-x86_64-ubuntu2204-8.0.4/bin:\$PATH\"" >> ~/.bashrc
|
| 514 |
+
which mongod # must print a path
|
| 515 |
+
```
|
| 516 |
+
|
| 517 |
+
### Step 5 — start mongod (worker_0 ONLY)
|
| 518 |
+
|
| 519 |
+
```bash
|
| 520 |
+
mkdir -p /tmp/sdg_mongo
|
| 521 |
+
nohup mongod --port $ARNOLD_WORKER_0_PORT --dbpath /tmp/sdg_mongo \
|
| 522 |
+
--bind_ip_all --ipv6 > mongo.log 2>&1 &
|
| 523 |
+
sleep 5
|
| 524 |
+
grep -i "Listening on" mongo.log
|
| 525 |
+
# Must show BOTH 0.0.0.0:<port> AND [::]:<port>
|
| 526 |
+
|
| 527 |
+
python3 -c "from pymongo import MongoClient; \
|
| 528 |
+
MongoClient(f'mongodb://[::1]:$ARNOLD_WORKER_0_PORT', serverSelectionTimeoutMS=5000).admin.command('ping'); \
|
| 529 |
+
print('mongo OK locally')"
|
| 530 |
+
```
|
| 531 |
+
|
| 532 |
+
### Step 6 — verify cross-worker mongo reachability (workers 1, 2, 3)
|
| 533 |
+
|
| 534 |
+
```bash
|
| 535 |
+
python3 -c "
|
| 536 |
+
import os
|
| 537 |
+
from pymongo import MongoClient
|
| 538 |
+
host = os.environ['ARNOLD_WORKER_0_HOST']
|
| 539 |
+
port = os.environ['ARNOLD_WORKER_0_PORT']
|
| 540 |
+
uri = f'mongodb://[{host}]:{port}'
|
| 541 |
+
MongoClient(uri, serverSelectionTimeoutMS=5000).admin.command('ping')
|
| 542 |
+
print(f'mongo OK from {os.uname().nodename} → {uri}')
|
| 543 |
+
"
|
| 544 |
+
```
|
| 545 |
+
|
| 546 |
+
All three must print `mongo OK from ...`. If any times out, the platform is
|
| 547 |
+
blocking inter-worker traffic — stop here.
|
| 548 |
+
|
| 549 |
+
### Step 7 — patch the config (every worker)
|
| 550 |
+
|
| 551 |
+
```bash
|
| 552 |
+
python3 <<'EOF'
|
| 553 |
+
import os, yaml
|
| 554 |
+
host = os.environ['ARNOLD_WORKER_0_HOST']
|
| 555 |
+
port = os.environ['ARNOLD_WORKER_0_PORT']
|
| 556 |
+
path = 'sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml'
|
| 557 |
+
with open(path) as f: cfg = yaml.safe_load(f)
|
| 558 |
+
cfg['mongo_uri'] = f'mongodb://[{host}]:{port}'
|
| 559 |
+
cfg['num_shards'] = 32
|
| 560 |
+
with open(path, 'w') as f: yaml.dump(cfg, f, sort_keys=False)
|
| 561 |
+
print('updated:', cfg['mongo_uri'])
|
| 562 |
+
EOF
|
| 563 |
+
```
|
| 564 |
+
|
| 565 |
+
### Step 8 ��� verify scripts/ are intact (every worker)
|
| 566 |
+
|
| 567 |
+
```bash
|
| 568 |
+
python -c "import ast; ast.parse(open('scripts/upload_my_shards.py').read()); ast.parse(open('scripts/combine_and_publish.py').read()); print('OK')"
|
| 569 |
+
bash -n scripts/launch_shards.sh && bash -n scripts/monitor.sh && echo "shell OK"
|
| 570 |
+
```
|
| 571 |
+
|
| 572 |
+
### Step 9 — (optional) smoke test (every worker)
|
| 573 |
+
|
| 574 |
+
```bash
|
| 575 |
+
LIMIT=128 ./scripts/launch_shards.sh
|
| 576 |
+
# Wait ~15-20 min, then check:
|
| 577 |
+
grep -l "PIPELINE COMPLETE" logs/worker_*/shard_*.log | wc -l # must be 8
|
| 578 |
+
```
|
| 579 |
+
|
| 580 |
+
If smoke passes, clear outputs before the real run:
|
| 581 |
+
|
| 582 |
+
```bash
|
| 583 |
+
# Every worker:
|
| 584 |
+
rm -rf output/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1
|
| 585 |
+
rm -rf logs/worker_*
|
| 586 |
+
# Worker_0 only — wipe cache and restart mongod:
|
| 587 |
+
pkill mongod && sleep 2 && rm -rf /tmp/sdg_mongo/*
|
| 588 |
+
# Then redo Step 5 on worker_0
|
| 589 |
+
```
|
| 590 |
+
|
| 591 |
+
### Step 10 — launch the real run (every worker)
|
| 592 |
+
|
| 593 |
+
```bash
|
| 594 |
+
./scripts/launch_shards.sh
|
| 595 |
+
# Expected: 8 PIDs printed. ~5 min later, all 8 GPUs should be at ~95% util.
|
| 596 |
+
```
|
| 597 |
+
|
| 598 |
+
### Step 11 — start the per-worker uploader (every worker)
|
| 599 |
+
|
| 600 |
+
Safe to launch immediately — it sleep-polls until the worker's shards finish.
|
| 601 |
+
|
| 602 |
+
```bash
|
| 603 |
+
nohup python scripts/upload_my_shards.py > upload.log 2>&1 &
|
| 604 |
+
disown
|
| 605 |
+
```
|
| 606 |
+
|
| 607 |
+
### Step 12 — start the combiner (worker_0 ONLY)
|
| 608 |
+
|
| 609 |
+
```bash
|
| 610 |
+
nohup python scripts/combine_and_publish.py > combine.log 2>&1 &
|
| 611 |
+
disown
|
| 612 |
+
```
|
| 613 |
+
|
| 614 |
+
### Step 13 — monitor (any worker, anytime)
|
| 615 |
+
|
| 616 |
+
```bash
|
| 617 |
+
watch -n 30 ./scripts/monitor.sh
|
| 618 |
+
# Or, if `watch` isn't installed:
|
| 619 |
+
while true; do clear; ./scripts/monitor.sh; sleep 30; done
|
| 620 |
+
```
|
| 621 |
+
|
| 622 |
+
After this you can disconnect all terminals. The shards, uploaders, and
|
| 623 |
+
combiner all run under `nohup`.
|
| 624 |
+
|
| 625 |
+
### Step 14 — (recommended) per-shard integrity check after shards finish (every worker)
|
| 626 |
+
|
| 627 |
+
When `monitor.sh` shows `stats.json: 8/8` and `PIPELINE COMPLETE: 8/8` on a
|
| 628 |
+
worker, sanity-check its outputs:
|
| 629 |
+
|
| 630 |
+
```bash
|
| 631 |
+
RUN=output/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1/shards
|
| 632 |
+
for d in $RUN/shard_*; do
|
| 633 |
+
lines=$(wc -l < $d/output.jsonl 2>/dev/null)
|
| 634 |
+
passed=$(python3 -c "import json; print(json.load(open('$d/stats.json'))['summary']['total_passed'])" 2>/dev/null)
|
| 635 |
+
echo "$(basename $d): $lines rows | total_passed=$passed"
|
| 636 |
+
done
|
| 637 |
+
```
|
| 638 |
+
|
| 639 |
+
`lines` must equal `total_passed` for every shard. If they diverge, the
|
| 640 |
+
combiner will refuse to publish — fix the broken shard before its uploader
|
| 641 |
+
starts pushing.
|
| 642 |
+
|
| 643 |
+
### Step 15 — wait for `DONE` (worker_0)
|
| 644 |
+
|
| 645 |
+
```bash
|
| 646 |
+
tail -f combine.log
|
| 647 |
+
# Watch for: DONE: https://huggingface.co/datasets/<HF_USER>/<RUN_NAME>
|
| 648 |
+
```
|
| 649 |
+
|
| 650 |
+
### Step 16 — verify the published dataset loads (any machine with HF access)
|
| 651 |
+
|
| 652 |
+
```bash
|
| 653 |
+
python3 -c "
|
| 654 |
+
from datasets import load_dataset
|
| 655 |
+
ds = load_dataset('fzzhang/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1', split='train')
|
| 656 |
+
print('rows:', len(ds))
|
| 657 |
+
print('keys:', list(ds[0].keys()))
|
| 658 |
+
print('roles:', [m['role'] for m in ds[0]['messages']])
|
| 659 |
+
"
|
| 660 |
+
```
|
| 661 |
+
|
| 662 |
+
### Step 17 — (optional) cleanup
|
| 663 |
+
|
| 664 |
+
```bash
|
| 665 |
+
# Every worker
|
| 666 |
+
rm -rf output/ logs/ gathered/ combined_output.jsonl combined_stats.json upload.log
|
| 667 |
+
|
| 668 |
+
# Worker_0 only
|
| 669 |
+
pkill mongod
|
| 670 |
+
rm -rf /tmp/sdg_mongo combine.log
|
| 671 |
+
|
| 672 |
+
# Delete the intermediate staging HF repo (worker_0)
|
| 673 |
+
HF_USER=fzzhang \
|
| 674 |
+
RUN_NAME=qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1 \
|
| 675 |
+
python -c "
|
| 676 |
+
import os
|
| 677 |
+
from huggingface_hub import HfApi
|
| 678 |
+
repo = f\"{os.environ['HF_USER']}/{os.environ['RUN_NAME']}-staging\"
|
| 679 |
+
HfApi().delete_repo(repo_id=repo, repo_type='dataset')
|
| 680 |
+
print('deleted', repo)
|
| 681 |
+
"
|
| 682 |
+
```
|
| 683 |
+
|
| 684 |
+
---
|
| 685 |
+
|
| 686 |
+
## Success signal
|
| 687 |
+
|
| 688 |
+
The whole pipeline succeeds when `combine.log` on worker_0 prints:
|
| 689 |
+
```
|
| 690 |
+
DONE: https://huggingface.co/datasets/<HF_USER>/<RUN_NAME>
|
| 691 |
+
```
|
| 692 |
+
|
| 693 |
+
Everything else (per-shard logs, mongo cache count, GPU util, staging repo,
|
| 694 |
+
upload markers) is plumbing that supports that one line.
|
gpu-sft/.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
runs/
|
| 4 |
+
hf_cache/
|
| 5 |
+
evalout/
|
| 6 |
+
*.npy
|
gpu-sft/EVAL_RESULTS.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GPU eval results (PARTIAL) — captured 2026-09-13 before a machine reorg
|
| 2 |
+
|
| 3 |
+
Raw per-task JSONs live on HDFS at `/mnt/hdfs/fangzhao_writable/marin-eval/results`
|
| 4 |
+
(durable across the reorg). This file is the accessible-anywhere (GitHub) copy of the
|
| 5 |
+
headline numbers so they're not gated behind HDFS access.
|
| 6 |
+
|
| 7 |
+
**Eval stack:** driver 535, cu124 **vLLM 0.8.5 + evalchemy@7f24168**, `gpu_eval_driver.py`,
|
| 8 |
+
tp=8, temp 0.7 / top_p 1.0 / max_gen 32768, seeds 42..51 (AIME*/AMC23/HMMT n=10; MATH500 &
|
| 9 |
+
OlympiadBench n=1; LiveCodeBench* n=6). Per the port's design, GPU numbers are **not
|
| 10 |
+
bit-comparable** to the TPU tables (different kernels/seeding) — deltas are computed vs the
|
| 11 |
+
`qwen3_8b_base` row below.
|
| 12 |
+
|
| 13 |
+
## qwen3_8b_base — baseline (the (+Δ) denominator)
|
| 14 |
+
|
| 15 |
+
### math
|
| 16 |
+
| task | mean | std | seeds |
|
| 17 |
+
|---|---|---|---|
|
| 18 |
+
| MATH500 | 0.9080 | – | 1 |
|
| 19 |
+
| OlympiadBench | 0.6795 | – | 1 |
|
| 20 |
+
| AIME24 | 0.7500 | 0.0478 | 10 |
|
| 21 |
+
| AIME25 | 0.6467 | 0.0358 | 10 |
|
| 22 |
+
| AIME26 | 0.6833 | 0.0572 | 10 |
|
| 23 |
+
| AMC23 | 0.9425 | 0.0290 | 10 |
|
| 24 |
+
| HMMT | **FAILED** (exited 1 @ 11s — bug, see `_raw/HMMT.evalchemy.log`) | – | – |
|
| 25 |
+
|
| 26 |
+
### code
|
| 27 |
+
| task | mean | std | seeds |
|
| 28 |
+
|---|---|---|---|
|
| 29 |
+
| LiveCodeBench | 0.6181 | 0.0230 | 6 |
|
| 30 |
+
| LiveCodeBenchv5_official | not finished (killed mid-run) | – | – |
|
| 31 |
+
| LiveCodeBenchv6_official | pending | – | – |
|
| 32 |
+
|
| 33 |
+
## Gate 2 (eval parity) — PASS
|
| 34 |
+
GPU AIME24 **0.750 [±0.048]** vs TPU reference **0.740 [±0.049]** → within ±1σ. GPU baseline
|
| 35 |
+
runs slightly higher than the TPU reference across the board (MATH500 90.8 vs 89.2,
|
| 36 |
+
OlympiadBench 68.0 vs 62.5, AIME25 64.7 vs 60.0, AIME26 68.3 vs 62.0) — a consistent, small
|
| 37 |
+
upward shift consistent with the known kernel/seeding differences (not bit-comparable, as
|
| 38 |
+
designed). The eval stack is validated.
|
| 39 |
+
|
| 40 |
+
## Still pending (resume on the next machine)
|
| 41 |
+
- **baseline:** HMMT (fix the quick failure), LiveCodeBench v5/v6.
|
| 42 |
+
- **experiments (needed for the deltas):** `hs_competition_nofilter` (math),
|
| 43 |
+
`coding_depth4v3_nofilter` (code). A last-hour salvage of hs AIME24 may have banked —
|
| 44 |
+
append here if it did.
|
| 45 |
+
- then `claude/compile_results_local.py` over the HDFS results → `results.md` with `(+Δ)`.
|
| 46 |
+
|
| 47 |
+
## Trained models (durable on HDFS, final step-1999)
|
| 48 |
+
- `/mnt/hdfs/fangzhao_writable/marin-sft/coding_depth4v3_nofilter/hf/step-1999` — DONE (2000 steps)
|
| 49 |
+
- `/mnt/hdfs/fangzhao_writable/marin-sft/hs_competition_nofilter/hf/step-1999` — DONE (2000 steps)
|
| 50 |
+
- `science_depth4v3_nofilter` — re-running fresh (`_v3`) on a separate machine
|
gpu-sft/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# gpu-sft — Qwen3 self-instill SFT + eval (TPU→GPU port)
|
| 2 |
+
|
| 3 |
+
Downstream of the SDG pipeline: **train Qwen3 on the self-instill datasets** (the
|
| 4 |
+
`fzzhang/qwen3_8b_*_instill_...` sets our SDG runs produced) → **eval** on
|
| 5 |
+
math/science/code → regenerate **`results.md`**.
|
| 6 |
+
|
| 7 |
+
## Provenance
|
| 8 |
+
All scripts here were **extracted verbatim** from Tony's handoff doc
|
| 9 |
+
`fangzhao.md` (Appendix A), via a fenced-code parser — not hand-typed.
|
| 10 |
+
**They have NOT been run on a GPU** (written on macOS, no CUDA). Treat as a strong
|
| 11 |
+
first draft to smoke-test on one node; the acceptance tests are the two parity
|
| 12 |
+
gates in §10 of the handoff. Do not trust any experiment number until Gate 1
|
| 13 |
+
(training-loss parity) and Gate 2 (eval-baseline parity) pass.
|
| 14 |
+
|
| 15 |
+
## Pipeline
|
| 16 |
+
```
|
| 17 |
+
HF dataset@rev → scripts/gpu_sft/prepare_sft_data.py (CPU) → packed .npy + assistant mask
|
| 18 |
+
→ scripts/gpu_sft/train_sft_qwen3.py (GPU FSDP) → hf/step-N/ (bf16)
|
| 19 |
+
→ scripts/gpu_eval/gpu_eval_driver.py (vLLM+evalchemy) → eval JSON
|
| 20 |
+
→ claude/compile_results_local.py → results.md
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Environment (two venvs — mandatory; see handoff §5.1)
|
| 24 |
+
- `sft-train`: torch 2.7 cu124 + transformers 4.57 + flash-attn (this dir's `requirements-gpu.txt`).
|
| 25 |
+
- `sft-eval`: vLLM (its own torch) + `datasets>=3.1,<4` + pinned evalchemy (via `scripts/gpu_eval/bootstrap_gpu_eval.sh`).
|
| 26 |
+
- `HF_HOME` on the big local disk, NOT `$HOME`. Pin `datasets<4.0` in both (v4 breaks LiveCodeBench).
|
| 27 |
+
- Gated licences: the `fzzhang` HF account must accept `cais/hle` AND `Idavidrein/gpqa` before any eval.
|
| 28 |
+
|
| 29 |
+
## Storage plan (this cluster: Azure ND96isr H100 v5, 4 nodes × 8×H100-80G, not preemptible)
|
| 30 |
+
- **No HDFS, no cross-node FS.** 8B@32K fits on ONE node → run **4 independent single-node experiments**, one per node.
|
| 31 |
+
- Working dir + `HF_HOME` + active checkpoints → **local NVMe** (`/`, ~3.3 TB free; one 8B experiment ~2 TB fits).
|
| 32 |
+
- Durable artifact = **bf16 `hf/step-N` exports → private HF repo** `fzzhang/<exp>-stepN` (existing workflow; durable + eval-anywhere).
|
| 33 |
+
- Tradeoff: HF export drops the fp32 optimizer state, so no bit-exact training resume. Given not-preemptible + ~1.5-day runs, accept re-run risk. Revisit HDFS write-perm only if bit-exact auto-resume is needed.
|
| 34 |
+
|
| 35 |
+
## Status
|
| 36 |
+
- **Phase 0 (zero-GPU): DONE** — all 8 Python files compile; the `results.md` compiler is validated end-to-end via `claude/make_fixture_eval_tree.py` → `claude/compile_results_local.py` (correct §9 format).
|
| 37 |
+
- **Phase 1 (1 node): pending** — two venvs + gated-licence preflight → `prepare_sft_data.py` on one dataset (with the §10 sanity asserts) → tiny smoke train → parity gate (`coding_depth4v3_n8_vr5`, 2000 steps) vs Tony's W&B (`tonyhlee/instill_ot4`).
|
| 38 |
+
- **Phase 2:** GPU baselines (Qwen3-8B) into a fresh results root → experiments.
|
| 39 |
+
|
| 40 |
+
## Zero-GPU compiler self-test (reproduce anywhere)
|
| 41 |
+
```
|
| 42 |
+
python claude/make_fixture_eval_tree.py /tmp/fixture
|
| 43 |
+
python claude/compile_results_local.py --root /tmp/fixture/evalout \
|
| 44 |
+
--models /tmp/fixture/evals.md --output /tmp/fixture/results.md \
|
| 45 |
+
--timestamp "2026-09-01 12:00" --no-cache
|
| 46 |
+
cat /tmp/fixture/results.md
|
| 47 |
+
```
|
gpu-sft/claude/compile_results_local.py
ADDED
|
@@ -0,0 +1,1220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Copyright The Marin Authors
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
"""Compile LOCAL evalchemy outputs into results.md.
|
| 6 |
+
|
| 7 |
+
Local-disk port of claude/compile_results.py (the GCS/Iris version that produced
|
| 8 |
+
the 2897-line results.md). Same output format, byte-for-byte: same header line,
|
| 9 |
+
same column sets, same delta/std cell format, same Val Sigma Delta / Test Sigma
|
| 10 |
+
Delta formulas, same per-column star rule, same Best Checkpoints and Missing
|
| 11 |
+
Evals blocks.
|
| 12 |
+
|
| 13 |
+
Only the INPUT layer changed: gcsfs -> os.scandir. See LAYOUT below.
|
| 14 |
+
|
| 15 |
+
Usage
|
| 16 |
+
-----
|
| 17 |
+
# one eval root
|
| 18 |
+
python compile_results_local.py --root /data/marin/evaluation/evalchemy
|
| 19 |
+
|
| 20 |
+
# several roots (e.g. two boxes rsynced into one tree); FIRST root wins on
|
| 21 |
+
# conflicting complete values, exactly like the multi-region merge did
|
| 22 |
+
python compile_results_local.py --root /data/a/evaluation/evalchemy \
|
| 23 |
+
--root /data/b/evaluation/evalchemy
|
| 24 |
+
|
| 25 |
+
# deterministic regeneration (frozen timestamp) for byte-diffing
|
| 26 |
+
python compile_results_local.py --root ... --timestamp "2026-08-08 09:19"
|
| 27 |
+
|
| 28 |
+
# only report what is not yet evaluated
|
| 29 |
+
python compile_results_local.py --root ... --missing-only
|
| 30 |
+
|
| 31 |
+
LAYOUT
|
| 32 |
+
------
|
| 33 |
+
<root>/
|
| 34 |
+
Qwen_Qwen3-8B/ # baseline: model id, '/' -> '_'
|
| 35 |
+
MATH500_seed42/**/results_<ISO>.json # single-seed benchmark
|
| 36 |
+
AIME24_seed42/**/results_<ISO>.json # per-seed dirs, seeds 42..51
|
| 37 |
+
...
|
| 38 |
+
AIME24_seed51/**/results_<ISO>.json
|
| 39 |
+
compile_AIME24_avg10seeds/compiled_results/averaged_results.json # optional
|
| 40 |
+
<experiment_name>-step<N>/ # one dir per checkpoint
|
| 41 |
+
<same benchmark dirs as above>
|
| 42 |
+
|
| 43 |
+
A trailing "-<hash>" on any of those directory names is tolerated
|
| 44 |
+
(`AIME24_seed42-1a2b3c`, `compile_AIME24_avg10seeds-009af9`) so a tree copied
|
| 45 |
+
down from GCS works unchanged.
|
| 46 |
+
|
| 47 |
+
Per-seed file schema (what the reader needs):
|
| 48 |
+
{"results": {"<evalchemy task name>": {"accuracy_avg": 0.6333}}} # or "accuracy"
|
| 49 |
+
Compile file schema:
|
| 50 |
+
[{"correct_mean": 0.6567, "correct_std": 0.0686, "num_seeds": 10}]
|
| 51 |
+
|
| 52 |
+
Scores are fractions in [0, 1]; every displayed number is value * 100.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
from __future__ import annotations
|
| 56 |
+
|
| 57 |
+
import argparse
|
| 58 |
+
import datetime
|
| 59 |
+
import json
|
| 60 |
+
import os
|
| 61 |
+
import re
|
| 62 |
+
import sys
|
| 63 |
+
import tempfile
|
| 64 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 65 |
+
from dataclasses import dataclass, field
|
| 66 |
+
from decimal import ROUND_HALF_UP, Decimal
|
| 67 |
+
from pathlib import Path
|
| 68 |
+
|
| 69 |
+
# Some eval result JSONs (HMMT) contain integers like (26!)^3 that exceed
|
| 70 |
+
# CPython's default int->str digit cap.
|
| 71 |
+
sys.set_int_max_str_digits(0)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _round_half_up(x: float, ndigits: int = 1) -> float:
|
| 75 |
+
"""Round half away from zero (i.e. 62.35 -> 62.4, not 62.3 like banker's)."""
|
| 76 |
+
quant = Decimal("1") / (Decimal("10") ** ndigits)
|
| 77 |
+
return float(Decimal(repr(x)).quantize(quant, rounding=ROUND_HALF_UP))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
# Configuration - parsed from evals.md (single source of truth)
|
| 82 |
+
# ---------------------------------------------------------------------------
|
| 83 |
+
|
| 84 |
+
DEFAULT_MODELS_MD = Path(__file__).parent / "evals.md"
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@dataclass
|
| 88 |
+
class ModelSizeGroup:
|
| 89 |
+
"""A group of experiments sharing the same model size and baseline."""
|
| 90 |
+
|
| 91 |
+
label: str # e.g. "Qwen3 4B"
|
| 92 |
+
baseline: str # e.g. "Qwen/Qwen3-4B"
|
| 93 |
+
experiments: list[str] = field(default_factory=list)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _detect_size(text: str) -> str | None:
|
| 97 |
+
"""Detect model size like '4B', '0.6B', or '1.7B' from a string."""
|
| 98 |
+
m = re.search(r"\b(\d+(?:\.\d+)?[Bb])\b", text)
|
| 99 |
+
return m.group(1).upper() if m else None
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def parse_models_md(path: Path) -> tuple[list[str], list[str], list[ModelSizeGroup]]:
|
| 103 |
+
"""Parse baselines, experiments, and model-size groups from evals.md.
|
| 104 |
+
|
| 105 |
+
Expects a ``## Models to Evaluate`` section containing ``### Baselines``
|
| 106 |
+
and one or more ``### ...`` subsections with ``- model_name`` entries.
|
| 107 |
+
Returns (all_baselines, all_experiments, model_size_groups).
|
| 108 |
+
|
| 109 |
+
NOTE: group order is `int(re.sub(r"\\D", "", size))`, so 0.6B -> 6 and
|
| 110 |
+
1.7B -> 17. That is why results.md orders sections 4B, 0.6B, 8B, 1.7B, 32B.
|
| 111 |
+
Preserved deliberately: changing it reorders the whole document.
|
| 112 |
+
"""
|
| 113 |
+
text = path.read_text()
|
| 114 |
+
baselines: list[str] = []
|
| 115 |
+
experiments: list[str] = []
|
| 116 |
+
|
| 117 |
+
size_baselines: dict[str, str] = {} # "4B" -> "Qwen/Qwen3-4B"
|
| 118 |
+
size_experiments: dict[str, list[str]] = {} # "4B" -> [...]
|
| 119 |
+
|
| 120 |
+
in_models_section = False
|
| 121 |
+
current_is_baseline = False
|
| 122 |
+
current_size: str | None = None
|
| 123 |
+
|
| 124 |
+
for line in text.splitlines():
|
| 125 |
+
stripped = line.strip()
|
| 126 |
+
|
| 127 |
+
if stripped.startswith("## "):
|
| 128 |
+
in_models_section = stripped == "## Models to Evaluate"
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
if not in_models_section:
|
| 132 |
+
continue
|
| 133 |
+
|
| 134 |
+
if stripped.startswith("### "):
|
| 135 |
+
current_is_baseline = "baseline" in stripped.lower()
|
| 136 |
+
current_size = None if current_is_baseline else _detect_size(stripped)
|
| 137 |
+
continue
|
| 138 |
+
|
| 139 |
+
if stripped.startswith("- "):
|
| 140 |
+
name = stripped[2:].strip()
|
| 141 |
+
if not name:
|
| 142 |
+
continue
|
| 143 |
+
if current_is_baseline:
|
| 144 |
+
baselines.append(name)
|
| 145 |
+
size = _detect_size(name)
|
| 146 |
+
if size:
|
| 147 |
+
size_baselines[size] = name
|
| 148 |
+
else:
|
| 149 |
+
experiments.append(name)
|
| 150 |
+
size = current_size or _detect_size(name)
|
| 151 |
+
if size:
|
| 152 |
+
size_experiments.setdefault(size, []).append(name)
|
| 153 |
+
|
| 154 |
+
groups: list[ModelSizeGroup] = []
|
| 155 |
+
for size in sorted(size_baselines.keys(), key=lambda s: int(re.sub(r"\D", "", s))):
|
| 156 |
+
groups.append(
|
| 157 |
+
ModelSizeGroup(
|
| 158 |
+
label=f"Qwen3 {size}",
|
| 159 |
+
baseline=size_baselines[size],
|
| 160 |
+
experiments=size_experiments.get(size, []),
|
| 161 |
+
)
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
return baselines, experiments, groups
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
# Populated in main(); module-level so the builder functions read them like the
|
| 168 |
+
# original script did.
|
| 169 |
+
BASELINES: list[str] = []
|
| 170 |
+
EXPERIMENTS: list[str] = []
|
| 171 |
+
MODEL_SIZE_GROUPS: list[ModelSizeGroup] = []
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
# ---------------------------------------------------------------------------
|
| 175 |
+
# Benchmark suites - val benchmarks drive Val Sigma Delta, test drive Test Sigma Delta
|
| 176 |
+
# ---------------------------------------------------------------------------
|
| 177 |
+
|
| 178 |
+
MATH_VAL = ["MATH500", "OlympiadBench", "AIME24", "AIME25"]
|
| 179 |
+
MATH_TEST = ["AIME26", "HMMT"]
|
| 180 |
+
MATH_COLUMNS = MATH_VAL + MATH_TEST
|
| 181 |
+
|
| 182 |
+
BENCHMARKS_MULTISEED = ["AIME24", "AIME25", "AIME26", "HMMT"]
|
| 183 |
+
BENCHMARKS_SINGLE = ["MATH500", "OlympiadBench"]
|
| 184 |
+
ALL_BENCHMARKS = BENCHMARKS_SINGLE + BENCHMARKS_MULTISEED
|
| 185 |
+
|
| 186 |
+
SCIENCE_VAL = ["JEEBENCH", "OLYMPIADBENCH_PHYSICS"]
|
| 187 |
+
SCIENCE_TEST = ["GPQA_DIAMOND", "HUMANITYS_LAST_EXAM"]
|
| 188 |
+
SCIENCE_COLUMNS = SCIENCE_VAL + SCIENCE_TEST
|
| 189 |
+
|
| 190 |
+
SCIENCE_MULTISEED = ["GPQA_DIAMOND", "JEEBENCH", "HUMANITYS_LAST_EXAM", "OLYMPIADBENCH_PHYSICS"]
|
| 191 |
+
SCIENCE_SINGLE: list[str] = []
|
| 192 |
+
ALL_SCIENCE_BENCHMARKS = SCIENCE_SINGLE + SCIENCE_MULTISEED
|
| 193 |
+
|
| 194 |
+
CODE_VAL = ["LIVECODEBENCH"]
|
| 195 |
+
CODE_TEST = ["LIVECODEBENCH_V5_OFFICIAL", "LIVECODEBENCH_V6_OFFICIAL"]
|
| 196 |
+
CODE_COLUMNS = CODE_VAL + CODE_TEST
|
| 197 |
+
|
| 198 |
+
CODE_MULTISEED = ["LIVECODEBENCH", "LIVECODEBENCH_V5_OFFICIAL", "LIVECODEBENCH_V6_OFFICIAL"]
|
| 199 |
+
CODE_SINGLE: list[str] = []
|
| 200 |
+
ALL_CODE_BENCHMARKS = CODE_SINGLE + CODE_MULTISEED
|
| 201 |
+
|
| 202 |
+
# Display column name -> evalchemy task name (the name used in directory names
|
| 203 |
+
# AND as the key inside results_*.json["results"]).
|
| 204 |
+
BENCH_TO_TASK = {
|
| 205 |
+
"GPQA_DIAMOND": "GPQADiamond",
|
| 206 |
+
"JEEBENCH": "JEEBench",
|
| 207 |
+
"HUMANITYS_LAST_EXAM": "HLE",
|
| 208 |
+
"OLYMPIADBENCH_PHYSICS": "OlympiadBench_Physics",
|
| 209 |
+
"LIVECODEBENCH": "LiveCodeBench",
|
| 210 |
+
"LIVECODEBENCH_V5_OFFICIAL": "LiveCodeBenchv5_official",
|
| 211 |
+
"LIVECODEBENCH_V6_OFFICIAL": "LiveCodeBenchv6_official",
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
# Benchmarks whose evalchemy compile step re-grades with naive string equality
|
| 215 |
+
# and is therefore WRONG: symbolic (HMMT), tolerance-based
|
| 216 |
+
# (OlympiadBench_Physics), execution-based (LiveCodeBench*, JEEBench). For these
|
| 217 |
+
# we never read a compile file; we average per-seed accuracy_avg ourselves.
|
| 218 |
+
SKIP_COMPILE_BENCHMARKS = {
|
| 219 |
+
"HMMT",
|
| 220 |
+
"LIVECODEBENCH",
|
| 221 |
+
"LIVECODEBENCH_V5_OFFICIAL",
|
| 222 |
+
"LIVECODEBENCH_V6_OFFICIAL",
|
| 223 |
+
"JEEBENCH",
|
| 224 |
+
"OLYMPIADBENCH_PHYSICS",
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
# A multiseed cell is emitted only when this many seeds are present.
|
| 228 |
+
BENCH_EXPECTED_SEEDS = {
|
| 229 |
+
"AIME24": 10,
|
| 230 |
+
"AIME25": 10,
|
| 231 |
+
"AIME26": 10,
|
| 232 |
+
"AMC23": 10,
|
| 233 |
+
"HMMT": 10,
|
| 234 |
+
"GPQA_DIAMOND": 3,
|
| 235 |
+
"JEEBENCH": 3,
|
| 236 |
+
"HUMANITYS_LAST_EXAM": 3,
|
| 237 |
+
"OLYMPIADBENCH_PHYSICS": 3,
|
| 238 |
+
"LIVECODEBENCH": 6,
|
| 239 |
+
"LIVECODEBENCH_V5_OFFICIAL": 6,
|
| 240 |
+
"LIVECODEBENCH_V6_OFFICIAL": 6,
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
# Single-seed benchmarks live in a seed42 dir.
|
| 244 |
+
SINGLE_SEED = 42
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def suite_for_model(name: str) -> str:
|
| 248 |
+
"""Which eval suite an experiment belongs to. Substring match on the name."""
|
| 249 |
+
lower = name.lower()
|
| 250 |
+
if "science" in lower:
|
| 251 |
+
return "science"
|
| 252 |
+
if "code" in lower or "coding" in lower or "rstarcoder" in lower:
|
| 253 |
+
return "code"
|
| 254 |
+
if "nemotron_cascade2" in lower:
|
| 255 |
+
return "science"
|
| 256 |
+
return "math"
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def benchmarks_for_model(name: str, is_baseline: bool) -> list[str]:
|
| 260 |
+
"""All benchmarks that should be checked for a model."""
|
| 261 |
+
if is_baseline:
|
| 262 |
+
return ALL_BENCHMARKS + ALL_SCIENCE_BENCHMARKS + ALL_CODE_BENCHMARKS
|
| 263 |
+
suite = suite_for_model(name)
|
| 264 |
+
if suite == "science":
|
| 265 |
+
return list(ALL_SCIENCE_BENCHMARKS)
|
| 266 |
+
if suite == "code":
|
| 267 |
+
return list(ALL_CODE_BENCHMARKS)
|
| 268 |
+
return list(ALL_BENCHMARKS)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
MAX_WORKERS = 8
|
| 272 |
+
DEFAULT_OUTPUT = "results.md"
|
| 273 |
+
RESULTS_FILE_RE = re.compile(r"results_.*\.json$")
|
| 274 |
+
HASH_SUFFIX = r"(?:-[0-9A-Za-z]+)?"
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
# ---------------------------------------------------------------------------
|
| 278 |
+
# Local filesystem helpers
|
| 279 |
+
# ---------------------------------------------------------------------------
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def list_subdirs(path: str) -> list[str]:
|
| 283 |
+
"""Basenames of immediate subdirectories of ``path``. [] if absent."""
|
| 284 |
+
try:
|
| 285 |
+
with os.scandir(path) as it:
|
| 286 |
+
return sorted(e.name for e in it if e.is_dir())
|
| 287 |
+
except (FileNotFoundError, NotADirectoryError, PermissionError):
|
| 288 |
+
return []
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def find_results_files(path: str) -> list[str]:
|
| 292 |
+
"""Recursively find results_*.json under ``path``, lexicographically sorted."""
|
| 293 |
+
out: list[str] = []
|
| 294 |
+
for dirpath, _dirnames, filenames in os.walk(path):
|
| 295 |
+
for fn in filenames:
|
| 296 |
+
if RESULTS_FILE_RE.search(fn):
|
| 297 |
+
out.append(os.path.join(dirpath, fn))
|
| 298 |
+
return sorted(out)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def read_json(path: str):
|
| 302 |
+
"""Parse a JSON file. Returns None on missing/unreadable/unparseable."""
|
| 303 |
+
try:
|
| 304 |
+
with open(path, encoding="utf-8") as fh:
|
| 305 |
+
return json.load(fh)
|
| 306 |
+
except (OSError, json.JSONDecodeError, ValueError):
|
| 307 |
+
return None
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
# ---------------------------------------------------------------------------
|
| 311 |
+
# Extracted-scalar cache
|
| 312 |
+
# ---------------------------------------------------------------------------
|
| 313 |
+
#
|
| 314 |
+
# A finished results_*.json is immutable, and LiveCodeBench files run to tens of
|
| 315 |
+
# MB while we only ever pull one scalar out. Cache the extracted scalar keyed by
|
| 316 |
+
# (path, mtime_ns, size) so a re-run after new evals land re-parses only the new
|
| 317 |
+
# files. Directory listings are NOT cached, so newly finished evals are always
|
| 318 |
+
# discovered.
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
@dataclass
|
| 322 |
+
class ScalarCache:
|
| 323 |
+
root: Path | None
|
| 324 |
+
hits: int = 0
|
| 325 |
+
misses: int = 0
|
| 326 |
+
|
| 327 |
+
def _entry_path(self, path: str) -> Path:
|
| 328 |
+
assert self.root is not None
|
| 329 |
+
return self.root / (path.lstrip("/") + ".json")
|
| 330 |
+
|
| 331 |
+
@staticmethod
|
| 332 |
+
def _stat_key(path: str) -> tuple[int, int] | None:
|
| 333 |
+
try:
|
| 334 |
+
st = os.stat(path)
|
| 335 |
+
except OSError:
|
| 336 |
+
return None
|
| 337 |
+
return (st.st_mtime_ns, st.st_size)
|
| 338 |
+
|
| 339 |
+
@staticmethod
|
| 340 |
+
def _write_atomic(path: Path, content: str) -> None:
|
| 341 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 342 |
+
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".tmp-")
|
| 343 |
+
try:
|
| 344 |
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
| 345 |
+
fh.write(content)
|
| 346 |
+
os.replace(tmp, path)
|
| 347 |
+
except BaseException:
|
| 348 |
+
try:
|
| 349 |
+
os.unlink(tmp)
|
| 350 |
+
except OSError:
|
| 351 |
+
pass
|
| 352 |
+
raise
|
| 353 |
+
|
| 354 |
+
def get(self, path: str, compute):
|
| 355 |
+
"""Return ``compute()``'s small dict record for ``path``, via cache."""
|
| 356 |
+
key = self._stat_key(path)
|
| 357 |
+
if self.root is None or key is None:
|
| 358 |
+
self.misses += 1
|
| 359 |
+
return compute()
|
| 360 |
+
|
| 361 |
+
entry = self._entry_path(path)
|
| 362 |
+
if entry.exists():
|
| 363 |
+
rec = read_json(str(entry))
|
| 364 |
+
if isinstance(rec, dict) and rec.get("_key") == list(key):
|
| 365 |
+
self.hits += 1
|
| 366 |
+
return rec.get("rec")
|
| 367 |
+
try:
|
| 368 |
+
entry.unlink()
|
| 369 |
+
except OSError:
|
| 370 |
+
pass
|
| 371 |
+
|
| 372 |
+
self.misses += 1
|
| 373 |
+
rec = compute()
|
| 374 |
+
if rec is not None:
|
| 375 |
+
self._write_atomic(entry, json.dumps({"_key": list(key), "rec": rec}))
|
| 376 |
+
return rec
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
_CACHE = ScalarCache(root=None)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
# ---------------------------------------------------------------------------
|
| 383 |
+
# Discovery
|
| 384 |
+
# ---------------------------------------------------------------------------
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
@dataclass
|
| 388 |
+
class ModelInfo:
|
| 389 |
+
name: str
|
| 390 |
+
is_baseline: bool = False
|
| 391 |
+
checkpoint_dir: str | None = None # only set by the checkpoint fallback
|
| 392 |
+
checkpoints: list[str] = field(default_factory=list) # ["step-100", ...]
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def _step_sort_key(step: str) -> int:
|
| 396 |
+
m = re.search(r"\d+", step)
|
| 397 |
+
return int(m.group()) if m else 0
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def _steps_from_checkpoint_root(exp: str, checkpoint_root: str | None) -> tuple[str | None, list[str]]:
|
| 401 |
+
"""Fallback: list <checkpoint_root>/<exp>[-<hash>]/hf/step-* .
|
| 402 |
+
|
| 403 |
+
Mirrors the GCS script's behaviour of surfacing not-yet-evaluated checkpoints
|
| 404 |
+
so they show up under Missing Evals. Picks the folder with the most steps
|
| 405 |
+
when a config hash produced more than one output dir.
|
| 406 |
+
"""
|
| 407 |
+
if not checkpoint_root:
|
| 408 |
+
return None, []
|
| 409 |
+
best_dir, best_steps = None, []
|
| 410 |
+
for name in list_subdirs(checkpoint_root):
|
| 411 |
+
m = re.match(r"^(.+?)(?:-[0-9a-f]{4,})?$", name)
|
| 412 |
+
prefix = m.group(1) if m else name
|
| 413 |
+
if not (prefix == exp or exp.startswith(prefix)):
|
| 414 |
+
continue
|
| 415 |
+
folder = os.path.join(checkpoint_root, name)
|
| 416 |
+
steps = sorted(
|
| 417 |
+
(d for d in list_subdirs(os.path.join(folder, "hf")) if re.search(r"\d+", d)),
|
| 418 |
+
key=_step_sort_key,
|
| 419 |
+
)
|
| 420 |
+
if len(steps) > len(best_steps):
|
| 421 |
+
best_dir, best_steps = folder, steps
|
| 422 |
+
return best_dir, best_steps
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def discover_models(root: str, checkpoint_root: str | None) -> list[ModelInfo]:
|
| 426 |
+
"""Discover steps from eval-result directory names under one root.
|
| 427 |
+
|
| 428 |
+
Step discovery is driven by eval dirs named ``<exp>-step<N>`` - NOT by
|
| 429 |
+
checkpoint listings - so an experiment shows up as soon as it has any eval
|
| 430 |
+
output. ``checkpoint_root`` is only consulted for experiments with zero eval
|
| 431 |
+
dirs.
|
| 432 |
+
"""
|
| 433 |
+
models: list[ModelInfo] = [ModelInfo(name=b, is_baseline=True) for b in BASELINES]
|
| 434 |
+
|
| 435 |
+
all_eval_dirs = list_subdirs(root)
|
| 436 |
+
|
| 437 |
+
for exp in EXPERIMENTS:
|
| 438 |
+
pattern = re.compile(re.escape(exp) + r"-step(\d+)$")
|
| 439 |
+
steps = sorted(
|
| 440 |
+
(f"step-{m.group(1)}" for m in (pattern.match(d) for d in all_eval_dirs) if m),
|
| 441 |
+
key=_step_sort_key,
|
| 442 |
+
)
|
| 443 |
+
if steps:
|
| 444 |
+
models.append(ModelInfo(name=exp, checkpoints=steps))
|
| 445 |
+
continue
|
| 446 |
+
folder, ckpt_steps = _steps_from_checkpoint_root(exp, checkpoint_root)
|
| 447 |
+
models.append(ModelInfo(name=exp, checkpoint_dir=folder, checkpoints=ckpt_steps))
|
| 448 |
+
|
| 449 |
+
return models
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def merge_models(region_models: list[list[ModelInfo]]) -> list[ModelInfo]:
|
| 453 |
+
"""Merge per-root discovery, preserving evals.md ordering. Steps are unioned."""
|
| 454 |
+
if not region_models:
|
| 455 |
+
return []
|
| 456 |
+
by_root = [{m.name: m for m in models} for models in region_models]
|
| 457 |
+
merged: list[ModelInfo] = []
|
| 458 |
+
for model in region_models[0]:
|
| 459 |
+
variants = [models[model.name] for models in by_root if model.name in models]
|
| 460 |
+
checkpoints = sorted({s for v in variants for s in v.checkpoints}, key=_step_sort_key)
|
| 461 |
+
ckpt_dir = next((v.checkpoint_dir for v in variants if v.checkpoint_dir), None)
|
| 462 |
+
merged.append(
|
| 463 |
+
ModelInfo(
|
| 464 |
+
name=model.name,
|
| 465 |
+
is_baseline=model.is_baseline,
|
| 466 |
+
checkpoint_dir=ckpt_dir,
|
| 467 |
+
checkpoints=checkpoints,
|
| 468 |
+
)
|
| 469 |
+
)
|
| 470 |
+
return merged
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def merge_metric_maps(
|
| 474 |
+
root_maps: list[dict[tuple[str, str, str], float | None]],
|
| 475 |
+
roots: list[str],
|
| 476 |
+
) -> dict[tuple[str, str, str], float | None]:
|
| 477 |
+
"""Merge per-root metrics. First root with a non-None value wins."""
|
| 478 |
+
merged: dict[tuple[str, str, str], float | None] = {}
|
| 479 |
+
source: dict[tuple[str, str, str], str] = {}
|
| 480 |
+
for root, metrics in zip(roots, root_maps, strict=True):
|
| 481 |
+
for key, value in metrics.items():
|
| 482 |
+
existing = merged.get(key)
|
| 483 |
+
if existing is None and value is not None:
|
| 484 |
+
merged[key] = value
|
| 485 |
+
source[key] = root
|
| 486 |
+
elif key not in merged:
|
| 487 |
+
merged[key] = None
|
| 488 |
+
elif existing is not None and value is not None and abs(existing - value) > 1e-12:
|
| 489 |
+
print(
|
| 490 |
+
f"WARNING: Conflicting complete results for {key}: "
|
| 491 |
+
f"{source[key]}={existing}, {root}={value}; keeping {source[key]}",
|
| 492 |
+
file=sys.stderr,
|
| 493 |
+
)
|
| 494 |
+
return merged
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
# ---------------------------------------------------------------------------
|
| 498 |
+
# Reading
|
| 499 |
+
# ---------------------------------------------------------------------------
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def eval_dir_name(model: ModelInfo, step: str) -> str:
|
| 503 |
+
"""Directory name under <root> holding one model/checkpoint's eval output."""
|
| 504 |
+
if model.is_baseline:
|
| 505 |
+
return model.name.replace("/", "_")
|
| 506 |
+
m = re.search(r"\d+", step)
|
| 507 |
+
return f"{model.name}-step{m.group()}" if m else model.name
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
def _pick_results_file(files: list[str], newest: bool) -> str | None:
|
| 511 |
+
"""Choose among several results_<ISO>.json in one seed dir.
|
| 512 |
+
|
| 513 |
+
Names embed an ISO timestamp, so lexicographic order is chronological.
|
| 514 |
+
``newest=True`` (default) takes the last re-run; the GCS script took the
|
| 515 |
+
first. Only matters when a seed dir was evaluated more than once.
|
| 516 |
+
"""
|
| 517 |
+
if not files:
|
| 518 |
+
return None
|
| 519 |
+
return files[-1] if newest else files[0]
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def read_seed_result(seed_dir: str, task: str, newest: bool) -> float | None:
|
| 523 |
+
"""Read one seed's accuracy from results_*.json under ``seed_dir``."""
|
| 524 |
+
results_file = _pick_results_file(find_results_files(seed_dir), newest)
|
| 525 |
+
if results_file is None:
|
| 526 |
+
return None
|
| 527 |
+
|
| 528 |
+
def _compute():
|
| 529 |
+
d = read_json(results_file)
|
| 530 |
+
if d is None:
|
| 531 |
+
return None
|
| 532 |
+
try:
|
| 533 |
+
r = d["results"][task]
|
| 534 |
+
except (KeyError, TypeError):
|
| 535 |
+
return None
|
| 536 |
+
val = r.get("accuracy_avg", r.get("accuracy"))
|
| 537 |
+
return None if val is None else {"value": val}
|
| 538 |
+
|
| 539 |
+
rec = _CACHE.get(results_file, _compute)
|
| 540 |
+
return rec["value"] if rec is not None else None
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
def read_compile_result(compile_dir: str, expected_seeds: int | None) -> tuple[float, float] | None:
|
| 544 |
+
"""Read (mean, std) from <compile_dir>/compiled_results/averaged_results.json."""
|
| 545 |
+
path = os.path.join(compile_dir, "compiled_results", "averaged_results.json")
|
| 546 |
+
|
| 547 |
+
def _compute():
|
| 548 |
+
d = read_json(path)
|
| 549 |
+
if d is None:
|
| 550 |
+
return None
|
| 551 |
+
try:
|
| 552 |
+
return {
|
| 553 |
+
"mean": d[0]["correct_mean"],
|
| 554 |
+
"std": d[0].get("correct_std", 0.0),
|
| 555 |
+
"num_seeds": d[0].get("num_seeds", 0),
|
| 556 |
+
}
|
| 557 |
+
except (KeyError, IndexError, TypeError):
|
| 558 |
+
return None
|
| 559 |
+
|
| 560 |
+
rec = _CACHE.get(path, _compute)
|
| 561 |
+
if rec is None:
|
| 562 |
+
return None
|
| 563 |
+
if expected_seeds is not None and rec.get("num_seeds", 0) < expected_seeds:
|
| 564 |
+
print(
|
| 565 |
+
f"WARNING: Incomplete seeds in {compile_dir}: {rec.get('num_seeds', 0)}/{expected_seeds}",
|
| 566 |
+
file=sys.stderr,
|
| 567 |
+
)
|
| 568 |
+
return None
|
| 569 |
+
return (rec["mean"], rec["std"])
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def _mean_and_sample_std(values: list[float]) -> tuple[float, float | None]:
|
| 573 |
+
"""Unweighted mean and SAMPLE (ddof=1) standard deviation."""
|
| 574 |
+
mean = sum(values) / len(values)
|
| 575 |
+
if len(values) < 2:
|
| 576 |
+
return mean, None
|
| 577 |
+
variance = sum((v - mean) ** 2 for v in values) / (len(values) - 1)
|
| 578 |
+
return mean, variance**0.5
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
@dataclass
|
| 582 |
+
class CellSources:
|
| 583 |
+
"""Where one (model, step, benchmark) cell can be read from."""
|
| 584 |
+
|
| 585 |
+
compile_dir: str | None = None
|
| 586 |
+
seed_dirs: dict[int, str] = field(default_factory=dict) # seed -> dir
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def _scan_eval_dir(eval_path: str, benchmarks: list[str]) -> dict[str, CellSources]:
|
| 590 |
+
"""Index one eval dir's subdirectories by benchmark."""
|
| 591 |
+
entries = list_subdirs(eval_path)
|
| 592 |
+
out: dict[str, CellSources] = {}
|
| 593 |
+
for bench in benchmarks:
|
| 594 |
+
task = BENCH_TO_TASK.get(bench, bench)
|
| 595 |
+
src = CellSources()
|
| 596 |
+
seed_re = re.compile(rf"^{re.escape(task)}_seed(\d+){HASH_SUFFIX}$")
|
| 597 |
+
compile_re = re.compile(rf"^compile_{re.escape(task)}_avg(\d+)seeds{HASH_SUFFIX}$")
|
| 598 |
+
expected = BENCH_EXPECTED_SEEDS.get(bench)
|
| 599 |
+
best_compile_n = -1
|
| 600 |
+
for name in entries:
|
| 601 |
+
m = seed_re.match(name)
|
| 602 |
+
if m:
|
| 603 |
+
src.seed_dirs[int(m.group(1))] = os.path.join(eval_path, name)
|
| 604 |
+
continue
|
| 605 |
+
m = compile_re.match(name)
|
| 606 |
+
if m:
|
| 607 |
+
n = int(m.group(1))
|
| 608 |
+
# Prefer a compile dir whose seed count matches expectations;
|
| 609 |
+
# otherwise keep the largest.
|
| 610 |
+
score = 10**6 if (expected is not None and n == expected) else n
|
| 611 |
+
if score > best_compile_n:
|
| 612 |
+
best_compile_n = score
|
| 613 |
+
src.compile_dir = os.path.join(eval_path, name)
|
| 614 |
+
if src.compile_dir or src.seed_dirs:
|
| 615 |
+
out[bench] = src
|
| 616 |
+
return out
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
def gather_results(
|
| 620 |
+
models: list[ModelInfo],
|
| 621 |
+
root: str,
|
| 622 |
+
pool: ThreadPoolExecutor,
|
| 623 |
+
per_seed_only: bool,
|
| 624 |
+
newest_results_file: bool,
|
| 625 |
+
) -> tuple[dict[tuple[str, str, str], float | None], dict[tuple[str, str, str], float | None]]:
|
| 626 |
+
"""Read every (model, step, benchmark) cell under one root.
|
| 627 |
+
|
| 628 |
+
Returns (results, std_devs), each {(model_name, step_label, benchmark): value|None}.
|
| 629 |
+
Baseline rows use the empty string as the step label.
|
| 630 |
+
"""
|
| 631 |
+
# Pass 1: index directories. Baselines merge the underscore and hyphen
|
| 632 |
+
# variants of the model id (Qwen_Qwen3-8B and Qwen-Qwen3-8B).
|
| 633 |
+
sources: dict[tuple[str, str, str], CellSources] = {}
|
| 634 |
+
results: dict[tuple[str, str, str], float | None] = {}
|
| 635 |
+
std_devs: dict[tuple[str, str, str], float | None] = {}
|
| 636 |
+
|
| 637 |
+
for model in models:
|
| 638 |
+
benchmarks = benchmarks_for_model(model.name, model.is_baseline)
|
| 639 |
+
steps = [""] if model.is_baseline else model.checkpoints
|
| 640 |
+
for step in steps:
|
| 641 |
+
dir_names = [eval_dir_name(model, step)]
|
| 642 |
+
if model.is_baseline:
|
| 643 |
+
alt = model.name.replace("/", "-")
|
| 644 |
+
if alt != dir_names[0]:
|
| 645 |
+
dir_names.append(alt)
|
| 646 |
+
per_bench: dict[str, CellSources] = {}
|
| 647 |
+
for dn in dir_names:
|
| 648 |
+
for bench, src in _scan_eval_dir(os.path.join(root, dn), benchmarks).items():
|
| 649 |
+
merged = per_bench.setdefault(bench, CellSources())
|
| 650 |
+
merged.compile_dir = merged.compile_dir or src.compile_dir
|
| 651 |
+
merged.seed_dirs.update(src.seed_dirs)
|
| 652 |
+
for bench in benchmarks:
|
| 653 |
+
key = (model.name, step, bench)
|
| 654 |
+
results[key] = None
|
| 655 |
+
std_devs[key] = None
|
| 656 |
+
if bench in per_bench:
|
| 657 |
+
sources[key] = per_bench[bench]
|
| 658 |
+
|
| 659 |
+
# Pass 2: single-seed benchmarks (MATH500, OlympiadBench) - one file, no std.
|
| 660 |
+
single_futs = {}
|
| 661 |
+
for key, src in sources.items():
|
| 662 |
+
_, _, bench = key
|
| 663 |
+
if bench not in BENCHMARKS_SINGLE:
|
| 664 |
+
continue
|
| 665 |
+
seed_dir = src.seed_dirs.get(SINGLE_SEED) or (
|
| 666 |
+
sorted(src.seed_dirs.items())[0][1] if src.seed_dirs else None
|
| 667 |
+
)
|
| 668 |
+
if seed_dir is None:
|
| 669 |
+
continue
|
| 670 |
+
task = BENCH_TO_TASK.get(bench, bench)
|
| 671 |
+
single_futs[pool.submit(read_seed_result, seed_dir, task, newest_results_file)] = key
|
| 672 |
+
for fut in as_completed(single_futs):
|
| 673 |
+
key = single_futs[fut]
|
| 674 |
+
results[key] = fut.result()
|
| 675 |
+
std_devs[key] = None
|
| 676 |
+
|
| 677 |
+
# Pass 3: multiseed benchmarks read from a compile dir when one is usable.
|
| 678 |
+
compile_futs = {}
|
| 679 |
+
for key, src in sources.items():
|
| 680 |
+
_, _, bench = key
|
| 681 |
+
if bench in BENCHMARKS_SINGLE or bench in SKIP_COMPILE_BENCHMARKS:
|
| 682 |
+
continue
|
| 683 |
+
if per_seed_only or not src.compile_dir:
|
| 684 |
+
continue
|
| 685 |
+
expected = BENCH_EXPECTED_SEEDS.get(bench)
|
| 686 |
+
compile_futs[pool.submit(read_compile_result, src.compile_dir, expected)] = key
|
| 687 |
+
for fut in as_completed(compile_futs):
|
| 688 |
+
key = compile_futs[fut]
|
| 689 |
+
got = fut.result()
|
| 690 |
+
if got is not None:
|
| 691 |
+
results[key], std_devs[key] = got
|
| 692 |
+
|
| 693 |
+
# Pass 4: everything still unread is averaged from per-seed accuracy_avg with
|
| 694 |
+
# a SAMPLE (n-1) std. Requires exactly the expected number of seeds; a
|
| 695 |
+
# partial sweep stays missing rather than reporting a biased mean.
|
| 696 |
+
seed_futs: dict[object, tuple[tuple[str, str, str], int]] = {}
|
| 697 |
+
seed_counts: dict[tuple[str, str, str], int] = {}
|
| 698 |
+
for key, src in sources.items():
|
| 699 |
+
_, _, bench = key
|
| 700 |
+
if bench in BENCHMARKS_SINGLE or results.get(key) is not None:
|
| 701 |
+
continue
|
| 702 |
+
if not src.seed_dirs:
|
| 703 |
+
continue
|
| 704 |
+
task = BENCH_TO_TASK.get(bench, bench)
|
| 705 |
+
seed_counts[key] = len(src.seed_dirs)
|
| 706 |
+
for seed, seed_dir in sorted(src.seed_dirs.items()):
|
| 707 |
+
seed_futs[pool.submit(read_seed_result, seed_dir, task, newest_results_file)] = (key, seed)
|
| 708 |
+
|
| 709 |
+
seed_values: dict[tuple[str, str, str], list[float]] = {}
|
| 710 |
+
for fut in as_completed(seed_futs):
|
| 711 |
+
key, _seed = seed_futs[fut]
|
| 712 |
+
val = fut.result()
|
| 713 |
+
if val is not None:
|
| 714 |
+
seed_values.setdefault(key, []).append(val)
|
| 715 |
+
|
| 716 |
+
for key, n_dirs in seed_counts.items():
|
| 717 |
+
_, _, bench = key
|
| 718 |
+
values = seed_values.get(key, [])
|
| 719 |
+
expected = BENCH_EXPECTED_SEEDS.get(bench, n_dirs)
|
| 720 |
+
if len(values) == expected:
|
| 721 |
+
mean, std = _mean_and_sample_std(values)
|
| 722 |
+
results[key] = mean
|
| 723 |
+
std_devs[key] = std
|
| 724 |
+
else:
|
| 725 |
+
results[key] = None
|
| 726 |
+
std_devs[key] = None
|
| 727 |
+
if values:
|
| 728 |
+
print(
|
| 729 |
+
f"WARNING: {key[0]} {key[1] or '-'} {bench}: {len(values)}/{expected} seeds; "
|
| 730 |
+
"reporting as missing",
|
| 731 |
+
file=sys.stderr,
|
| 732 |
+
)
|
| 733 |
+
|
| 734 |
+
return results, std_devs
|
| 735 |
+
|
| 736 |
+
|
| 737 |
+
# ---------------------------------------------------------------------------
|
| 738 |
+
# Computed columns
|
| 739 |
+
# ---------------------------------------------------------------------------
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
def compute_sigma(
|
| 743 |
+
model_name: str,
|
| 744 |
+
step: str,
|
| 745 |
+
results: dict[tuple[str, str, str], float | None],
|
| 746 |
+
baseline: dict[str, float | None],
|
| 747 |
+
benchmarks: list[str],
|
| 748 |
+
) -> float | None:
|
| 749 |
+
"""Sum of (value - baseline) over ``benchmarks``, in percentage points.
|
| 750 |
+
|
| 751 |
+
UNROUNDED - used for star selection and Best-Checkpoints argmax so nearly
|
| 752 |
+
tied steps are not collapsed by rounding. None if any benchmark is missing.
|
| 753 |
+
"""
|
| 754 |
+
if not benchmarks:
|
| 755 |
+
return None
|
| 756 |
+
total = 0.0
|
| 757 |
+
for b in benchmarks:
|
| 758 |
+
v = results.get((model_name, step, b))
|
| 759 |
+
bv = baseline.get(b)
|
| 760 |
+
if v is None or bv is None:
|
| 761 |
+
return None
|
| 762 |
+
total += (v - bv) * 100
|
| 763 |
+
return total
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def compute_display_sigma(
|
| 767 |
+
model_name: str,
|
| 768 |
+
step: str,
|
| 769 |
+
results: dict[tuple[str, str, str], float | None],
|
| 770 |
+
baseline: dict[str, float | None],
|
| 771 |
+
benchmarks: list[str],
|
| 772 |
+
) -> float | None:
|
| 773 |
+
"""Display-only Sigma: sum of the per-cell ROUNDED diffs, then rounded again.
|
| 774 |
+
|
| 775 |
+
This is what gets printed, so a reader adding up the visible cell deltas
|
| 776 |
+
reproduces the Sigma exactly.
|
| 777 |
+
"""
|
| 778 |
+
if not benchmarks:
|
| 779 |
+
return None
|
| 780 |
+
total = 0.0
|
| 781 |
+
for b in benchmarks:
|
| 782 |
+
v = results.get((model_name, step, b))
|
| 783 |
+
bv = baseline.get(b)
|
| 784 |
+
if v is None or bv is None:
|
| 785 |
+
return None
|
| 786 |
+
total += _round_half_up(v * 100, 1) - _round_half_up(bv * 100, 1)
|
| 787 |
+
return _round_half_up(total, 1)
|
| 788 |
+
|
| 789 |
+
|
| 790 |
+
def compute_stars(
|
| 791 |
+
model: ModelInfo,
|
| 792 |
+
results: dict[tuple[str, str, str], float | None],
|
| 793 |
+
val_sigmas: dict[str, float | None],
|
| 794 |
+
test_sigmas: dict[str, float | None],
|
| 795 |
+
columns: list[str],
|
| 796 |
+
) -> set[tuple[str, str]]:
|
| 797 |
+
"""(step, column) pairs that get a star.
|
| 798 |
+
|
| 799 |
+
Per-column, per-experiment best value. Never across experiments, never on a
|
| 800 |
+
baseline row. Needs >= 2 steps with a value in that column, compares the
|
| 801 |
+
RAW float, and stars every step tied at the max.
|
| 802 |
+
"""
|
| 803 |
+
stars: set[tuple[str, str]] = set()
|
| 804 |
+
|
| 805 |
+
for col in columns:
|
| 806 |
+
non_none = [(s, results.get((model.name, s, col))) for s in model.checkpoints]
|
| 807 |
+
non_none = [(s, v) for s, v in non_none if v is not None]
|
| 808 |
+
if len(non_none) < 2:
|
| 809 |
+
continue
|
| 810 |
+
best = max(v for _, v in non_none)
|
| 811 |
+
stars.update((s, col) for s, v in non_none if v == best)
|
| 812 |
+
|
| 813 |
+
for label, sigmas in (("Val Σ Δ", val_sigmas), ("Test Σ Δ", test_sigmas)):
|
| 814 |
+
non_none = [(s, sigmas.get(s)) for s in model.checkpoints]
|
| 815 |
+
non_none = [(s, v) for s, v in non_none if v is not None]
|
| 816 |
+
if len(non_none) < 2:
|
| 817 |
+
continue
|
| 818 |
+
best = max(v for _, v in non_none)
|
| 819 |
+
stars.update((s, label) for s, v in non_none if v == best)
|
| 820 |
+
|
| 821 |
+
return stars
|
| 822 |
+
|
| 823 |
+
|
| 824 |
+
# ---------------------------------------------------------------------------
|
| 825 |
+
# Formatting
|
| 826 |
+
# ---------------------------------------------------------------------------
|
| 827 |
+
|
| 828 |
+
|
| 829 |
+
def _fmt_cell(val: float | None, baseline_val: float | None, is_baseline: bool, std: float | None = None) -> str:
|
| 830 |
+
"""One table cell: '88.4', '65.7 [6.9]', '61.3 (-4.4)', '66.7 (+1.0) [4.4]', '❌'."""
|
| 831 |
+
if val is None:
|
| 832 |
+
return "❌"
|
| 833 |
+
pct_rounded = _round_half_up(val * 100, 1)
|
| 834 |
+
pct = f"{pct_rounded:.1f}"
|
| 835 |
+
if is_baseline or baseline_val is None:
|
| 836 |
+
if std is not None:
|
| 837 |
+
return f"{pct} [{_round_half_up(std * 100, 1):.1f}]"
|
| 838 |
+
return pct
|
| 839 |
+
# Delta is the difference of the two ALREADY-ROUNDED percentages.
|
| 840 |
+
diff = _round_half_up(pct_rounded - _round_half_up(baseline_val * 100, 1), 1)
|
| 841 |
+
sign = "+" if diff >= 0 else ""
|
| 842 |
+
if std is not None:
|
| 843 |
+
return f"{pct} ({sign}{diff:.1f}) [{_round_half_up(std * 100, 1):.1f}]"
|
| 844 |
+
return f"{pct} ({sign}{diff:.1f})"
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
def _fmt_step(step: str) -> str:
|
| 848 |
+
m = re.search(r"\d+", step)
|
| 849 |
+
return m.group() if m else step
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
def _fmt_sigma(ts: float | None) -> str:
|
| 853 |
+
if ts is None:
|
| 854 |
+
return "N/A"
|
| 855 |
+
ts_rounded = _round_half_up(ts, 1)
|
| 856 |
+
sign = "+" if ts_rounded >= 0 else ""
|
| 857 |
+
return f"{sign}{ts_rounded:.1f}"
|
| 858 |
+
|
| 859 |
+
|
| 860 |
+
def _experiment_compute_order_key(name: str) -> tuple[int, int, int, int, str]:
|
| 861 |
+
"""Order experiments from least to most sampling/filtering compute."""
|
| 862 |
+
if "nofilter" in name:
|
| 863 |
+
return (0, 0, 0, 0, name)
|
| 864 |
+
n_match = re.search(r"_n(\d+)(?:_|$)", name)
|
| 865 |
+
vr_match = re.search(r"_vr(\d+)(?:_|$)", name)
|
| 866 |
+
n_value = int(n_match.group(1)) if n_match else 10**9
|
| 867 |
+
vr_value = int(vr_match.group(1)) if vr_match else 10**9
|
| 868 |
+
variant_rank = 1 if "allvalid" in name else 0
|
| 869 |
+
return (1, n_value, vr_value, variant_rank, name)
|
| 870 |
+
|
| 871 |
+
|
| 872 |
+
SUITE_CONFIG = {
|
| 873 |
+
"math": {
|
| 874 |
+
"title": "Math",
|
| 875 |
+
"columns": MATH_COLUMNS,
|
| 876 |
+
"val": MATH_VAL,
|
| 877 |
+
"test": MATH_TEST,
|
| 878 |
+
"all_benchmarks": ALL_BENCHMARKS,
|
| 879 |
+
},
|
| 880 |
+
"science": {
|
| 881 |
+
"title": "Science",
|
| 882 |
+
"columns": SCIENCE_COLUMNS,
|
| 883 |
+
"val": SCIENCE_VAL,
|
| 884 |
+
"test": SCIENCE_TEST,
|
| 885 |
+
"all_benchmarks": ALL_SCIENCE_BENCHMARKS,
|
| 886 |
+
},
|
| 887 |
+
"code": {
|
| 888 |
+
"title": "Code",
|
| 889 |
+
"columns": CODE_COLUMNS,
|
| 890 |
+
"val": CODE_VAL,
|
| 891 |
+
"test": CODE_TEST,
|
| 892 |
+
"all_benchmarks": ALL_CODE_BENCHMARKS,
|
| 893 |
+
},
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
def _build_suite_section(
|
| 898 |
+
suite: str,
|
| 899 |
+
baseline_model: ModelInfo | None,
|
| 900 |
+
baseline_vals: dict[str, float | None],
|
| 901 |
+
experiment_models: list[ModelInfo],
|
| 902 |
+
results: dict[tuple[str, str, str], float | None],
|
| 903 |
+
std_devs: dict[tuple[str, str, str], float | None] | None = None,
|
| 904 |
+
heading_level: str = "###",
|
| 905 |
+
) -> list[str]:
|
| 906 |
+
"""Build one suite section: main table, Best Checkpoints, Missing Evals."""
|
| 907 |
+
cfg = SUITE_CONFIG[suite]
|
| 908 |
+
columns: list[str] = cfg["columns"]
|
| 909 |
+
val_benchmarks: list[str] = cfg["val"]
|
| 910 |
+
test_benchmarks: list[str] = cfg["test"]
|
| 911 |
+
all_benchmarks: list[str] = cfg["all_benchmarks"]
|
| 912 |
+
|
| 913 |
+
lines: list[str] = []
|
| 914 |
+
lines.append(f"{heading_level} {cfg['title']}")
|
| 915 |
+
lines.append("")
|
| 916 |
+
|
| 917 |
+
header_cols = ["Model", "Step"] + columns + ["Val Σ Δ", "Test Σ Δ"]
|
| 918 |
+
lines.append("| " + " | ".join(header_cols) + " |")
|
| 919 |
+
lines.append("|" + "|".join(["---"] * len(header_cols)) + "|")
|
| 920 |
+
|
| 921 |
+
if baseline_model:
|
| 922 |
+
cells = [
|
| 923 |
+
_fmt_cell(
|
| 924 |
+
baseline_vals.get(b),
|
| 925 |
+
None,
|
| 926 |
+
True,
|
| 927 |
+
std=std_devs.get((baseline_model.name, "", b)) if std_devs else None,
|
| 928 |
+
)
|
| 929 |
+
for b in columns
|
| 930 |
+
]
|
| 931 |
+
cells += ["-", "-"]
|
| 932 |
+
lines.append(f"| {baseline_model.name} | - | " + " | ".join(cells) + " |")
|
| 933 |
+
|
| 934 |
+
suite_models = [m for m in experiment_models if suite_for_model(m.name) == suite]
|
| 935 |
+
|
| 936 |
+
for model in suite_models:
|
| 937 |
+
# Blank separator row before every experiment block (including the first).
|
| 938 |
+
lines.append("| | | " + " | ".join([""] * (len(columns) + 2)) + " |")
|
| 939 |
+
|
| 940 |
+
if not model.checkpoints:
|
| 941 |
+
cells = ["❌"] * len(columns) + ["N/A", "N/A"]
|
| 942 |
+
lines.append(f"| {model.name} | - | " + " | ".join(cells) + " |")
|
| 943 |
+
continue
|
| 944 |
+
|
| 945 |
+
val_sigmas: dict[str, float | None] = {}
|
| 946 |
+
test_sigmas: dict[str, float | None] = {}
|
| 947 |
+
val_display: dict[str, float | None] = {}
|
| 948 |
+
test_display: dict[str, float | None] = {}
|
| 949 |
+
for step in model.checkpoints:
|
| 950 |
+
val_sigmas[step] = compute_sigma(model.name, step, results, baseline_vals, val_benchmarks)
|
| 951 |
+
test_sigmas[step] = compute_sigma(model.name, step, results, baseline_vals, test_benchmarks)
|
| 952 |
+
val_display[step] = compute_display_sigma(model.name, step, results, baseline_vals, val_benchmarks)
|
| 953 |
+
test_display[step] = compute_display_sigma(model.name, step, results, baseline_vals, test_benchmarks)
|
| 954 |
+
|
| 955 |
+
# Stars use the unrounded sigmas so close scores aren't tied by rounding.
|
| 956 |
+
stars = compute_stars(model, results, val_sigmas, test_sigmas, columns)
|
| 957 |
+
|
| 958 |
+
for step in model.checkpoints:
|
| 959 |
+
cells = []
|
| 960 |
+
for bench in columns:
|
| 961 |
+
v = results.get((model.name, step, bench))
|
| 962 |
+
sd = std_devs.get((model.name, step, bench)) if std_devs else None
|
| 963 |
+
s = _fmt_cell(v, baseline_vals.get(bench), False, std=sd)
|
| 964 |
+
if (step, bench) in stars:
|
| 965 |
+
s = f"⭐ {s}"
|
| 966 |
+
cells.append(s)
|
| 967 |
+
|
| 968 |
+
vs_s = _fmt_sigma(val_display.get(step))
|
| 969 |
+
if (step, "Val Σ Δ") in stars:
|
| 970 |
+
vs_s = f"⭐ {vs_s}"
|
| 971 |
+
cells.append(vs_s)
|
| 972 |
+
|
| 973 |
+
ts_s = _fmt_sigma(test_display.get(step))
|
| 974 |
+
if (step, "Test Σ Δ") in stars:
|
| 975 |
+
ts_s = f"⭐ {ts_s}"
|
| 976 |
+
cells.append(ts_s)
|
| 977 |
+
|
| 978 |
+
lines.append(f"| {model.name} | {_fmt_step(step)} | " + " | ".join(cells) + " |")
|
| 979 |
+
|
| 980 |
+
# Best Checkpoints - argmax over the UNROUNDED Val Sigma Delta, all ties emitted,
|
| 981 |
+
# rows in compute order, no blank separator rows.
|
| 982 |
+
lines.append("")
|
| 983 |
+
lines.append(f"{heading_level}# Best Checkpoints (by Val Σ Δ)")
|
| 984 |
+
lines.append("")
|
| 985 |
+
lines.append("| " + " | ".join(header_cols) + " |")
|
| 986 |
+
lines.append("|" + "|".join(["---"] * len(header_cols)) + "|")
|
| 987 |
+
|
| 988 |
+
for model in sorted(suite_models, key=lambda m: _experiment_compute_order_key(m.name)):
|
| 989 |
+
if not model.checkpoints:
|
| 990 |
+
continue
|
| 991 |
+
best_val: float = -1e9
|
| 992 |
+
best_steps: list[str] = []
|
| 993 |
+
for step in model.checkpoints:
|
| 994 |
+
vs = compute_sigma(model.name, step, results, baseline_vals, val_benchmarks)
|
| 995 |
+
if vs is None:
|
| 996 |
+
continue
|
| 997 |
+
if vs > best_val:
|
| 998 |
+
best_val, best_steps = vs, [step]
|
| 999 |
+
elif vs == best_val:
|
| 1000 |
+
best_steps.append(step)
|
| 1001 |
+
for step in best_steps:
|
| 1002 |
+
cells = [
|
| 1003 |
+
_fmt_cell(
|
| 1004 |
+
results.get((model.name, step, b)),
|
| 1005 |
+
baseline_vals.get(b),
|
| 1006 |
+
False,
|
| 1007 |
+
std=std_devs.get((model.name, step, b)) if std_devs else None,
|
| 1008 |
+
)
|
| 1009 |
+
for b in columns
|
| 1010 |
+
]
|
| 1011 |
+
cells.append(_fmt_sigma(compute_display_sigma(model.name, step, results, baseline_vals, val_benchmarks)))
|
| 1012 |
+
cells.append(_fmt_sigma(compute_display_sigma(model.name, step, results, baseline_vals, test_benchmarks)))
|
| 1013 |
+
lines.append(f"| {model.name} | {_fmt_step(step)} | " + " | ".join(cells) + " |")
|
| 1014 |
+
|
| 1015 |
+
# Missing Evals - in evals.md order, not compute order.
|
| 1016 |
+
lines.append("")
|
| 1017 |
+
lines.append(f"{heading_level}# Missing Evals")
|
| 1018 |
+
lines.append("")
|
| 1019 |
+
any_missing = False
|
| 1020 |
+
for model in suite_models:
|
| 1021 |
+
for step in model.checkpoints:
|
| 1022 |
+
missing = [b for b in all_benchmarks if results.get((model.name, step, b)) is None]
|
| 1023 |
+
if missing:
|
| 1024 |
+
lines.append(f"- {model.name} step-{_fmt_step(step)} (missing: {', '.join(missing)})")
|
| 1025 |
+
any_missing = True
|
| 1026 |
+
if not any_missing:
|
| 1027 |
+
lines.append("All evaluations complete!")
|
| 1028 |
+
|
| 1029 |
+
return lines
|
| 1030 |
+
|
| 1031 |
+
|
| 1032 |
+
def build_results_md(
|
| 1033 |
+
models: list[ModelInfo],
|
| 1034 |
+
results: dict[tuple[str, str, str], float | None],
|
| 1035 |
+
std_devs: dict[tuple[str, str, str], float | None] | None = None,
|
| 1036 |
+
timestamp: str | None = None,
|
| 1037 |
+
) -> str:
|
| 1038 |
+
"""Render the whole document. Exactly one trailing newline."""
|
| 1039 |
+
lines: list[str] = []
|
| 1040 |
+
|
| 1041 |
+
# Header counts EXCLUDE baselines and count experiment x step x suite-benchmark.
|
| 1042 |
+
total_cells = 0
|
| 1043 |
+
complete_cells = 0
|
| 1044 |
+
for m in models:
|
| 1045 |
+
if m.is_baseline:
|
| 1046 |
+
continue
|
| 1047 |
+
benchmarks = benchmarks_for_model(m.name, m.is_baseline)
|
| 1048 |
+
for s in m.checkpoints:
|
| 1049 |
+
for b in benchmarks:
|
| 1050 |
+
total_cells += 1
|
| 1051 |
+
if results.get((m.name, s, b)) is not None:
|
| 1052 |
+
complete_cells += 1
|
| 1053 |
+
|
| 1054 |
+
stamp = timestamp or datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 1055 |
+
lines.append("# Evalchemy Evaluation Results")
|
| 1056 |
+
lines.append("")
|
| 1057 |
+
lines.append("")
|
| 1058 |
+
lines.append(f"*{complete_cells} of {total_cells} benchmark results gathered. Generated at {stamp}.*")
|
| 1059 |
+
|
| 1060 |
+
model_by_name: dict[str, ModelInfo] = {m.name: m for m in models}
|
| 1061 |
+
|
| 1062 |
+
for group in MODEL_SIZE_GROUPS:
|
| 1063 |
+
lines.append("")
|
| 1064 |
+
lines.append(f"## {group.label}")
|
| 1065 |
+
|
| 1066 |
+
baseline_model = model_by_name.get(group.baseline)
|
| 1067 |
+
baseline_vals: dict[str, float | None] = {}
|
| 1068 |
+
if baseline_model:
|
| 1069 |
+
for bench in MATH_COLUMNS + SCIENCE_COLUMNS + CODE_COLUMNS:
|
| 1070 |
+
baseline_vals[bench] = results.get((baseline_model.name, "", bench))
|
| 1071 |
+
|
| 1072 |
+
experiment_models = [model_by_name[e] for e in group.experiments if e in model_by_name]
|
| 1073 |
+
|
| 1074 |
+
suites_present = {suite_for_model(m.name) for m in experiment_models}
|
| 1075 |
+
if baseline_model:
|
| 1076 |
+
for suite_key in ["math", "science", "code"]:
|
| 1077 |
+
if any(baseline_vals.get(b) is not None for b in SUITE_CONFIG[suite_key]["columns"]):
|
| 1078 |
+
suites_present.add(suite_key)
|
| 1079 |
+
|
| 1080 |
+
for suite_key in ["math", "science", "code"]:
|
| 1081 |
+
if suite_key not in suites_present:
|
| 1082 |
+
continue
|
| 1083 |
+
lines.append("")
|
| 1084 |
+
lines.extend(
|
| 1085 |
+
_build_suite_section(
|
| 1086 |
+
suite_key,
|
| 1087 |
+
baseline_model,
|
| 1088 |
+
baseline_vals,
|
| 1089 |
+
experiment_models,
|
| 1090 |
+
results,
|
| 1091 |
+
std_devs=std_devs,
|
| 1092 |
+
heading_level="###",
|
| 1093 |
+
)
|
| 1094 |
+
)
|
| 1095 |
+
|
| 1096 |
+
return "\n".join(lines) + "\n"
|
| 1097 |
+
|
| 1098 |
+
|
| 1099 |
+
# ---------------------------------------------------------------------------
|
| 1100 |
+
# Missing-eval report
|
| 1101 |
+
# ---------------------------------------------------------------------------
|
| 1102 |
+
|
| 1103 |
+
|
| 1104 |
+
def print_missing(models: list[ModelInfo], results: dict[tuple[str, str, str], float | None]) -> None:
|
| 1105 |
+
for model in models:
|
| 1106 |
+
if model.is_baseline or not model.checkpoints:
|
| 1107 |
+
continue
|
| 1108 |
+
benchmarks = benchmarks_for_model(model.name, model.is_baseline)
|
| 1109 |
+
for step in model.checkpoints:
|
| 1110 |
+
missing = [b for b in benchmarks if results.get((model.name, step, b)) is None]
|
| 1111 |
+
if missing:
|
| 1112 |
+
print(f"{model.name} {step} (missing: {', '.join(missing)}) [{suite_for_model(model.name)}]")
|
| 1113 |
+
|
| 1114 |
+
|
| 1115 |
+
# ---------------------------------------------------------------------------
|
| 1116 |
+
# Entry point
|
| 1117 |
+
# ---------------------------------------------------------------------------
|
| 1118 |
+
|
| 1119 |
+
|
| 1120 |
+
def main() -> None:
|
| 1121 |
+
global BASELINES, EXPERIMENTS, MODEL_SIZE_GROUPS, _CACHE
|
| 1122 |
+
|
| 1123 |
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 1124 |
+
parser.add_argument(
|
| 1125 |
+
"--root",
|
| 1126 |
+
dest="roots",
|
| 1127 |
+
action="append",
|
| 1128 |
+
required=True,
|
| 1129 |
+
help="Local eval-output root (repeatable; first root wins on conflicts)",
|
| 1130 |
+
)
|
| 1131 |
+
parser.add_argument(
|
| 1132 |
+
"--checkpoint-root",
|
| 1133 |
+
default=None,
|
| 1134 |
+
help="Optional <dir>/<exp>-<hash>/hf/step-N tree, used only for experiments with no eval output yet",
|
| 1135 |
+
)
|
| 1136 |
+
parser.add_argument("--models", default=str(DEFAULT_MODELS_MD), help="Markdown file with '## Models to Evaluate'")
|
| 1137 |
+
parser.add_argument("--output", default=DEFAULT_OUTPUT, help="Output path for results.md")
|
| 1138 |
+
parser.add_argument("--workers", type=int, default=MAX_WORKERS, help="Parallel file readers")
|
| 1139 |
+
parser.add_argument("--timestamp", default=None, help="Freeze the header timestamp (format: 'YYYY-MM-DD HH:MM')")
|
| 1140 |
+
parser.add_argument("--missing-only", action="store_true", help="Report missing evals; do not write results.md")
|
| 1141 |
+
parser.add_argument(
|
| 1142 |
+
"--per-seed-only",
|
| 1143 |
+
action="store_true",
|
| 1144 |
+
help=(
|
| 1145 |
+
"Ignore compile dirs entirely and average per-seed accuracy for every multiseed benchmark "
|
| 1146 |
+
"(requires per-seed dirs for all of them; benchmarks that only have a compile dir become missing)"
|
| 1147 |
+
),
|
| 1148 |
+
)
|
| 1149 |
+
parser.add_argument(
|
| 1150 |
+
"--first-results-file",
|
| 1151 |
+
action="store_true",
|
| 1152 |
+
help="On re-evaluated seed dirs, read the OLDEST results_*.json (GCS script behaviour) instead of the newest",
|
| 1153 |
+
)
|
| 1154 |
+
parser.add_argument("--cache-dir", default=None, help="Extracted-scalar cache dir (default: <output dir>/.eval_cache)")
|
| 1155 |
+
parser.add_argument("--no-cache", action="store_true", help="Disable the scalar cache")
|
| 1156 |
+
args = parser.parse_args()
|
| 1157 |
+
|
| 1158 |
+
models_md = Path(args.models)
|
| 1159 |
+
if not models_md.exists():
|
| 1160 |
+
raise SystemExit(f"Models file not found: {models_md}")
|
| 1161 |
+
BASELINES, EXPERIMENTS, MODEL_SIZE_GROUPS = parse_models_md(models_md)
|
| 1162 |
+
if not MODEL_SIZE_GROUPS:
|
| 1163 |
+
raise SystemExit(f"No model-size groups parsed from {models_md} (need '## Models to Evaluate' + '### Baselines')")
|
| 1164 |
+
|
| 1165 |
+
if args.no_cache:
|
| 1166 |
+
_CACHE = ScalarCache(root=None)
|
| 1167 |
+
else:
|
| 1168 |
+
cache_dir = args.cache_dir or os.path.join(os.path.dirname(os.path.abspath(args.output)) or ".", ".eval_cache")
|
| 1169 |
+
_CACHE = ScalarCache(root=Path(cache_dir))
|
| 1170 |
+
|
| 1171 |
+
roots = [os.path.abspath(r.rstrip("/")) for r in args.roots]
|
| 1172 |
+
print(f"Roots: {', '.join(roots)}", file=sys.stderr)
|
| 1173 |
+
print(f"Models: {len(BASELINES)} baselines, {len(EXPERIMENTS)} experiments", file=sys.stderr)
|
| 1174 |
+
|
| 1175 |
+
with ThreadPoolExecutor(max_workers=args.workers) as pool:
|
| 1176 |
+
per_root_models = [discover_models(root, args.checkpoint_root) for root in roots]
|
| 1177 |
+
models = merge_models(per_root_models)
|
| 1178 |
+
|
| 1179 |
+
per_root_results = []
|
| 1180 |
+
per_root_stds = []
|
| 1181 |
+
for root, root_models in zip(roots, per_root_models, strict=True):
|
| 1182 |
+
# Read each root against the MERGED step list so a step discovered in
|
| 1183 |
+
# one root is also looked for in the others.
|
| 1184 |
+
aligned = [
|
| 1185 |
+
ModelInfo(
|
| 1186 |
+
name=m.name,
|
| 1187 |
+
is_baseline=m.is_baseline,
|
| 1188 |
+
checkpoint_dir=m.checkpoint_dir,
|
| 1189 |
+
checkpoints=next((x.checkpoints for x in models if x.name == m.name), m.checkpoints),
|
| 1190 |
+
)
|
| 1191 |
+
for m in root_models
|
| 1192 |
+
]
|
| 1193 |
+
r, s = gather_results(
|
| 1194 |
+
aligned,
|
| 1195 |
+
root,
|
| 1196 |
+
pool,
|
| 1197 |
+
per_seed_only=args.per_seed_only,
|
| 1198 |
+
newest_results_file=not args.first_results_file,
|
| 1199 |
+
)
|
| 1200 |
+
per_root_results.append(r)
|
| 1201 |
+
per_root_stds.append(s)
|
| 1202 |
+
|
| 1203 |
+
results = merge_metric_maps(per_root_results, roots)
|
| 1204 |
+
std_devs = merge_metric_maps(per_root_stds, roots)
|
| 1205 |
+
|
| 1206 |
+
if _CACHE.root is not None:
|
| 1207 |
+
print(f"Cache: {_CACHE.hits} hits, {_CACHE.misses} reads", file=sys.stderr)
|
| 1208 |
+
|
| 1209 |
+
if args.missing_only:
|
| 1210 |
+
print_missing(models, results)
|
| 1211 |
+
return
|
| 1212 |
+
|
| 1213 |
+
md = build_results_md(models, results, std_devs, timestamp=args.timestamp)
|
| 1214 |
+
with open(args.output, "w", encoding="utf-8") as fh:
|
| 1215 |
+
fh.write(md)
|
| 1216 |
+
print(f"\nWrote {args.output}", file=sys.stderr)
|
| 1217 |
+
|
| 1218 |
+
|
| 1219 |
+
if __name__ == "__main__":
|
| 1220 |
+
main()
|
gpu-sft/claude/evals.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Local Evals
|
| 2 |
+
|
| 3 |
+
The compiler (`claude/compile_results_local.py`) parses ONLY the
|
| 4 |
+
`## Models to Evaluate` section below. Everything else in this file is prose for
|
| 5 |
+
humans.
|
| 6 |
+
|
| 7 |
+
Rules:
|
| 8 |
+
|
| 9 |
+
- `### Baselines` entries are the delta reference. One baseline per model size;
|
| 10 |
+
the size token (`0.6B`, `1.7B`, `4B`, `8B`, `32B`) is read out of the name.
|
| 11 |
+
A `## Qwen3 <SIZE>` section is emitted only if a baseline for that size exists.
|
| 12 |
+
- Every other `### ...` heading is an experiment group. The size comes from the
|
| 13 |
+
heading text, falling back to the experiment name.
|
| 14 |
+
- Each `- <name>` under an experiment heading must be the EXACT experiment name,
|
| 15 |
+
which is also the eval output directory prefix: `<name>-step<N>/`. Do not
|
| 16 |
+
truncate, do not add a `-step<N>` suffix here.
|
| 17 |
+
- Row order inside a suite table follows this file. The `#### Best Checkpoints`
|
| 18 |
+
table re-sorts by sampling compute (`nofilter` first, then `_n<N>` asc,
|
| 19 |
+
`_vr<N>` asc, `allvalid` last).
|
| 20 |
+
- Suite routing is a substring match on the name: `science` -> Science;
|
| 21 |
+
`code` / `coding` / `rstarcoder` -> Code; otherwise Math. The suite decides
|
| 22 |
+
which benchmarks are read AND which table the row lands in.
|
| 23 |
+
- Section order across sizes is `int(re.sub(r"\D", "", size))`, so 0.6B sorts as
|
| 24 |
+
6 and 1.7B as 17: 4B, 0.6B, 8B, 1.7B, 32B. Known quirk, kept for continuity
|
| 25 |
+
with the existing results.md.
|
| 26 |
+
|
| 27 |
+
## Models to Evaluate
|
| 28 |
+
|
| 29 |
+
### Baselines
|
| 30 |
+
- Qwen/Qwen3-4B
|
| 31 |
+
- Qwen/Qwen3-8B
|
| 32 |
+
|
| 33 |
+
### SFT 4B Experiments
|
| 34 |
+
- exp_sft_qwen3_4b_selfinstill_ot3_math53k_n8_vr5_round1
|
| 35 |
+
|
| 36 |
+
### SFT 8B Experiments
|
| 37 |
+
- exp_sft_qwen3_8b_selfinstill_science_depth4v3_n8_vr5_2k_lr5e6_wd01
|
| 38 |
+
- exp_sft_qwen3_8b_selfinstill_coding_depth4v3_n8_vr5_2k_lr5e6_wd01
|
gpu-sft/claude/make_fixture_eval_tree.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Copyright The Marin Authors
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
|
| 5 |
+
"""Reference writer + self-test fixture for the local results pipeline.
|
| 6 |
+
|
| 7 |
+
Two jobs:
|
| 8 |
+
|
| 9 |
+
1. ``write_seed_result`` / ``write_compile_result`` are the CANONICAL writers.
|
| 10 |
+
The local eval runner should call these (or emit byte-identical files) so
|
| 11 |
+
compile_results_local.py can read its output. Nothing else defines the
|
| 12 |
+
contract.
|
| 13 |
+
|
| 14 |
+
2. Running this file as a script materializes a small fake eval tree plus a
|
| 15 |
+
matching models markdown, so the whole pipeline can be smoke-tested with no
|
| 16 |
+
GPU and no real eval:
|
| 17 |
+
|
| 18 |
+
python make_fixture_eval_tree.py /tmp/fixture
|
| 19 |
+
python compile_results_local.py --root /tmp/fixture/evalout \\
|
| 20 |
+
--models /tmp/fixture/evals.md --output /tmp/fixture/results.md \\
|
| 21 |
+
--timestamp "2026-09-01 12:00" --no-cache
|
| 22 |
+
cat /tmp/fixture/results.md
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import json
|
| 28 |
+
import os
|
| 29 |
+
import sys
|
| 30 |
+
|
| 31 |
+
# Display column name -> evalchemy task name. Must match compile_results_local.py.
|
| 32 |
+
BENCH_TO_TASK = {
|
| 33 |
+
"GPQA_DIAMOND": "GPQADiamond",
|
| 34 |
+
"JEEBENCH": "JEEBench",
|
| 35 |
+
"HUMANITYS_LAST_EXAM": "HLE",
|
| 36 |
+
"OLYMPIADBENCH_PHYSICS": "OlympiadBench_Physics",
|
| 37 |
+
"LIVECODEBENCH": "LiveCodeBench",
|
| 38 |
+
"LIVECODEBENCH_V5_OFFICIAL": "LiveCodeBenchv5_official",
|
| 39 |
+
"LIVECODEBENCH_V6_OFFICIAL": "LiveCodeBenchv6_official",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def model_dir_name(model: str, step: int | None) -> str:
|
| 44 |
+
"""Eval output dir for a model/checkpoint.
|
| 45 |
+
|
| 46 |
+
Baseline (HF id, step None): 'Qwen/Qwen3-8B' -> 'Qwen_Qwen3-8B'.
|
| 47 |
+
Experiment: '<experiment_name>-step<N>'.
|
| 48 |
+
"""
|
| 49 |
+
if step is None:
|
| 50 |
+
return model.replace("/", "_")
|
| 51 |
+
return f"{model}-step{step}"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def write_seed_result(
|
| 55 |
+
root: str,
|
| 56 |
+
model: str,
|
| 57 |
+
step: int | None,
|
| 58 |
+
task: str,
|
| 59 |
+
seed: int,
|
| 60 |
+
accuracy: float,
|
| 61 |
+
timestamp: str = "2026-09-01T00-00-00.000000",
|
| 62 |
+
extra: dict | None = None,
|
| 63 |
+
) -> str:
|
| 64 |
+
"""Write one (checkpoint, task, seed) result file. ``accuracy`` is a fraction.
|
| 65 |
+
|
| 66 |
+
Layout: <root>/<model_dir>/<task>_seed<seed>/<model_sanitized>/results_<ISO>.json
|
| 67 |
+
The reader takes ``results[task]["accuracy_avg"]``, falling back to
|
| 68 |
+
``["accuracy"]``, so single-pass benchmarks may emit either key.
|
| 69 |
+
"""
|
| 70 |
+
d = os.path.join(root, model_dir_name(model, step), f"{task}_seed{seed}", model.replace("/", "__"))
|
| 71 |
+
os.makedirs(d, exist_ok=True)
|
| 72 |
+
payload = {"results": {task: {"accuracy_avg": accuracy, **(extra or {})}}}
|
| 73 |
+
path = os.path.join(d, f"results_{timestamp}.json")
|
| 74 |
+
with open(path, "w", encoding="utf-8") as fh:
|
| 75 |
+
json.dump(payload, fh)
|
| 76 |
+
return path
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def write_compile_result(
|
| 80 |
+
root: str,
|
| 81 |
+
model: str,
|
| 82 |
+
step: int | None,
|
| 83 |
+
task: str,
|
| 84 |
+
seeds: list[int],
|
| 85 |
+
mean: float,
|
| 86 |
+
std: float,
|
| 87 |
+
) -> str:
|
| 88 |
+
"""Write the averaged-across-seeds file for one (checkpoint, task).
|
| 89 |
+
|
| 90 |
+
Layout: <root>/<model_dir>/compile_<task>_avg<N>seeds/compiled_results/averaged_results.json
|
| 91 |
+
|
| 92 |
+
OPTIONAL. Omit it and compile_results_local.py averages the per-seed files
|
| 93 |
+
itself (mean + sample n-1 std), which is the recommended local mode:
|
| 94 |
+
evalchemy's own compile step re-grades with naive string equality and is
|
| 95 |
+
wrong for HMMT / JEEBench / OlympiadBench_Physics / LiveCodeBench*.
|
| 96 |
+
"""
|
| 97 |
+
n = len(seeds)
|
| 98 |
+
d = os.path.join(root, model_dir_name(model, step), f"compile_{task}_avg{n}seeds", "compiled_results")
|
| 99 |
+
os.makedirs(d, exist_ok=True)
|
| 100 |
+
payload = [
|
| 101 |
+
{
|
| 102 |
+
"base_model_name": model,
|
| 103 |
+
"dataset_name": task,
|
| 104 |
+
"num_seeds": n,
|
| 105 |
+
"seeds": seeds,
|
| 106 |
+
"correct_mean": mean,
|
| 107 |
+
"correct_std": std,
|
| 108 |
+
}
|
| 109 |
+
]
|
| 110 |
+
path = os.path.join(d, "averaged_results.json")
|
| 111 |
+
with open(path, "w", encoding="utf-8") as fh:
|
| 112 |
+
json.dump(payload, fh)
|
| 113 |
+
return path
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ---------------------------------------------------------------------------
|
| 117 |
+
# Fixture
|
| 118 |
+
# ---------------------------------------------------------------------------
|
| 119 |
+
|
| 120 |
+
EVALS_MD = """# Local Evals
|
| 121 |
+
|
| 122 |
+
## Models to Evaluate
|
| 123 |
+
|
| 124 |
+
### Baselines
|
| 125 |
+
- Qwen/Qwen3-8B
|
| 126 |
+
|
| 127 |
+
### SFT 8B Experiments
|
| 128 |
+
- exp_sft_qwen3_8b_selfinstill_mynewdata_n8_vr5_2k_lr5e6_wd01
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
BASELINE = "Qwen/Qwen3-8B"
|
| 132 |
+
EXPERIMENT = "exp_sft_qwen3_8b_selfinstill_mynewdata_n8_vr5_2k_lr5e6_wd01"
|
| 133 |
+
MATH_SEEDS = list(range(42, 52)) # 10 seeds
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def build(out_dir: str) -> None:
|
| 137 |
+
evalout = os.path.join(out_dir, "evalout")
|
| 138 |
+
os.makedirs(evalout, exist_ok=True)
|
| 139 |
+
with open(os.path.join(out_dir, "evals.md"), "w", encoding="utf-8") as fh:
|
| 140 |
+
fh.write(EVALS_MD)
|
| 141 |
+
|
| 142 |
+
# Baseline: MATH500/OlympiadBench single-seed, AIME* via compile files,
|
| 143 |
+
# HMMT via per-seed files (compile is never trusted for HMMT).
|
| 144 |
+
write_seed_result(evalout, BASELINE, None, "MATH500", 42, 0.884)
|
| 145 |
+
write_seed_result(evalout, BASELINE, None, "OlympiadBench", 42, 0.617)
|
| 146 |
+
write_compile_result(evalout, BASELINE, None, "AIME24", MATH_SEEDS, 0.6566666666666666, 0.06858355272004873)
|
| 147 |
+
write_compile_result(evalout, BASELINE, None, "AIME25", MATH_SEEDS, 0.5866666666666667, 0.045460606091052)
|
| 148 |
+
write_compile_result(evalout, BASELINE, None, "AIME26", MATH_SEEDS, 0.5933333333333334, 0.05426322800056239)
|
| 149 |
+
for seed, acc in zip(MATH_SEEDS, [0.38, 0.40, 0.42, 0.36, 0.44, 0.38, 0.40, 0.42, 0.34, 0.39], strict=True):
|
| 150 |
+
write_seed_result(evalout, BASELINE, None, "HMMT", seed, acc)
|
| 151 |
+
|
| 152 |
+
steps = {
|
| 153 |
+
100: dict(
|
| 154 |
+
math500=0.872,
|
| 155 |
+
olympiad=0.596,
|
| 156 |
+
aime24=0.650,
|
| 157 |
+
aime25=0.567,
|
| 158 |
+
aime26=0.523,
|
| 159 |
+
hmmt=[0.36, 0.38, 0.40, 0.34, 0.42, 0.36, 0.38, 0.40, 0.32, 0.37],
|
| 160 |
+
),
|
| 161 |
+
200: dict(
|
| 162 |
+
math500=0.906,
|
| 163 |
+
olympiad=0.642,
|
| 164 |
+
aime24=0.737,
|
| 165 |
+
aime25=0.620,
|
| 166 |
+
aime26=0.637,
|
| 167 |
+
hmmt=[0.41, 0.43, 0.45, 0.39, 0.47, 0.41, 0.43, 0.45, 0.37, 0.42],
|
| 168 |
+
),
|
| 169 |
+
}
|
| 170 |
+
for step, d in steps.items():
|
| 171 |
+
write_seed_result(evalout, EXPERIMENT, step, "MATH500", 42, d["math500"])
|
| 172 |
+
write_seed_result(evalout, EXPERIMENT, step, "OlympiadBench", 42, d["olympiad"])
|
| 173 |
+
write_compile_result(evalout, EXPERIMENT, step, "AIME24", MATH_SEEDS, d["aime24"], 0.0623)
|
| 174 |
+
write_compile_result(evalout, EXPERIMENT, step, "AIME25", MATH_SEEDS, d["aime25"], 0.0531)
|
| 175 |
+
write_compile_result(evalout, EXPERIMENT, step, "AIME26", MATH_SEEDS, d["aime26"], 0.0688)
|
| 176 |
+
for seed, acc in zip(MATH_SEEDS, d["hmmt"], strict=True):
|
| 177 |
+
write_seed_result(evalout, EXPERIMENT, step, "HMMT", seed, acc)
|
| 178 |
+
|
| 179 |
+
print(f"fixture: {evalout}\nmodels: {os.path.join(out_dir, 'evals.md')}")
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
if __name__ == "__main__":
|
| 183 |
+
build(sys.argv[1] if len(sys.argv) > 1 else "fixture")
|
gpu-sft/experiments/qwen3_chat_template.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 The Marin Authors
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
# flake8: noqa
|
| 15 |
+
|
| 16 |
+
"""
|
| 17 |
+
Updated chat template for Qwen3 that has {% generation %} tags inserted (required by Levanter).
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
QWEN_3_CHAT_TEMPLATE = r"""{%- if tools is defined and tools %}
|
| 21 |
+
{{- '<|im_start|>system\n' }}
|
| 22 |
+
{%- if messages[0].role == 'system' %}
|
| 23 |
+
{{- messages[0].content + '\n\n' }}
|
| 24 |
+
{%- endif %}
|
| 25 |
+
{{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
|
| 26 |
+
{%- for tool in tools %}
|
| 27 |
+
{{- "\n" }}
|
| 28 |
+
{{- tool | tojson }}
|
| 29 |
+
{%- endfor %}
|
| 30 |
+
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
|
| 31 |
+
{%- else %}
|
| 32 |
+
{%- if messages[0].role == 'system' %}
|
| 33 |
+
{{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
|
| 34 |
+
{%- endif %}
|
| 35 |
+
{%- endif %}
|
| 36 |
+
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
| 37 |
+
{%- for message in messages[::-1] %}
|
| 38 |
+
{%- set index = (messages|length - 1) - loop.index0 %}
|
| 39 |
+
{%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
|
| 40 |
+
{%- set ns.multi_step_tool = false %}
|
| 41 |
+
{%- set ns.last_query_index = index %}
|
| 42 |
+
{%- endif %}
|
| 43 |
+
{%- endfor %}
|
| 44 |
+
{%- for message in messages %}
|
| 45 |
+
{%- if message.content is string %}
|
| 46 |
+
{%- set content = message.content %}
|
| 47 |
+
{%- else %}
|
| 48 |
+
{%- set content = '' %}
|
| 49 |
+
{%- endif %}
|
| 50 |
+
{%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
|
| 51 |
+
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
|
| 52 |
+
{%- elif message.role == "assistant" %}
|
| 53 |
+
{%- set reasoning_content = '' %}
|
| 54 |
+
{%- if message.reasoning_content is string %}
|
| 55 |
+
{%- set reasoning_content = message.reasoning_content %}
|
| 56 |
+
{%- else %}
|
| 57 |
+
{%- if '</think>' in content %}
|
| 58 |
+
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
| 59 |
+
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
| 60 |
+
{%- endif %}
|
| 61 |
+
{%- endif %}
|
| 62 |
+
{%- if loop.index0 > ns.last_query_index %}
|
| 63 |
+
{%- if loop.last or (not loop.last and reasoning_content) %}
|
| 64 |
+
{{- '<|im_start|>' + message.role + '\n' }}{% generation %}{{- '<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}{% endgeneration %}
|
| 65 |
+
{%- else %}
|
| 66 |
+
{{- '<|im_start|>' + message.role + '\n' }}{% generation %}{{- content }}{% endgeneration %}
|
| 67 |
+
{%- endif %}
|
| 68 |
+
{%- else %}
|
| 69 |
+
{{- '<|im_start|>' + message.role + '\n' }}{% generation %}{{- content }}{% endgeneration %}
|
| 70 |
+
{%- endif %}
|
| 71 |
+
{%- if message.tool_calls %}
|
| 72 |
+
{%- for tool_call in message.tool_calls %}
|
| 73 |
+
{%- if (loop.first and content) or (not loop.first) %}
|
| 74 |
+
{% generation %}{{- '\n' }}{% endgeneration %}
|
| 75 |
+
{%- endif %}
|
| 76 |
+
{%- if tool_call.function %}
|
| 77 |
+
{%- set tool_call = tool_call.function %}
|
| 78 |
+
{%- endif %}
|
| 79 |
+
{% generation %}{{- '<tool_call>\n{"name": "' }}
|
| 80 |
+
{{- tool_call.name }}
|
| 81 |
+
{{- '", "arguments": ' }}
|
| 82 |
+
{%- if tool_call.arguments is string %}
|
| 83 |
+
{{- tool_call.arguments }}
|
| 84 |
+
{%- else %}
|
| 85 |
+
{{- tool_call.arguments | tojson }}
|
| 86 |
+
{%- endif %}
|
| 87 |
+
{{- '}\n</tool_call>' }}{% endgeneration %}
|
| 88 |
+
{%- endfor %}
|
| 89 |
+
{%- endif %}
|
| 90 |
+
{% generation %}{{- '<|im_end|>\n' }}{% endgeneration %}
|
| 91 |
+
{%- elif message.role == "tool" %}
|
| 92 |
+
{%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
|
| 93 |
+
{{- '<|im_start|>user' }}
|
| 94 |
+
{%- endif %}
|
| 95 |
+
{{- '\n<tool_response>\n' }}
|
| 96 |
+
{{- content }}
|
| 97 |
+
{{- '\n</tool_response>' }}
|
| 98 |
+
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
|
| 99 |
+
{{- '<|im_end|>\n' }}
|
| 100 |
+
{%- endif %}
|
| 101 |
+
{%- endif %}
|
| 102 |
+
{%- endfor %}
|
| 103 |
+
{%- if add_generation_prompt %}
|
| 104 |
+
{{- '<|im_start|>assistant\n' }}
|
| 105 |
+
{%- if enable_thinking is defined and enable_thinking is false %}
|
| 106 |
+
{% generation %}{{- '<think>\n\n</think>\n\n' }}{% endgeneration %}
|
| 107 |
+
{%- endif %}
|
| 108 |
+
{%- endif %}"""
|
gpu-sft/requirements-gpu.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tested shape for the GPU port. Pin these; transformers in particular is load-bearing
|
| 2 |
+
# because the script relies on cu_seq_lens_q/k reaching flash_attn_varlen_func
|
| 3 |
+
# (transformers.modeling_flash_attention_utils, "Case 2" padding-free path).
|
| 4 |
+
--extra-index-url https://download.pytorch.org/whl/cu128
|
| 5 |
+
torch==2.8.0
|
| 6 |
+
transformers==4.57.5
|
| 7 |
+
tokenizers>=0.22
|
| 8 |
+
datasets>=3.1.0,<4.0.0 # v4 dropped dataset-script support; also what the marin eval branch pins
|
| 9 |
+
accelerate>=1.0.0
|
| 10 |
+
safetensors>=0.4.5
|
| 11 |
+
numpy>=1.26
|
| 12 |
+
jinja2>=3.1.0 # only needed by prepare_sft_data.py --verify
|
| 13 |
+
sentencepiece
|
| 14 |
+
huggingface_hub>=0.26
|
| 15 |
+
# FlashAttention-2 is REQUIRED for the 32K varlen path. Build takes ~20 min, or use the
|
| 16 |
+
# prebuilt wheel matching your torch/cuda/python/abi from the flash-attention releases page.
|
| 17 |
+
flash-attn==2.8.3 --no-build-isolation
|
| 18 |
+
# optional
|
| 19 |
+
wandb>=0.18
|
gpu-sft/scripts/gpu_eval/bootstrap_gpu_eval.sh
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Copyright The Marin Authors
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
#
|
| 5 |
+
# Bootstrap a self-contained CUDA evalchemy + vLLM environment for evaluating
|
| 6 |
+
# LOCAL HF-format checkpoints on a single GPU box. No GCS, no Iris, no TPU.
|
| 7 |
+
#
|
| 8 |
+
# ./bootstrap_gpu_eval.sh /opt/marin-gpu-eval
|
| 9 |
+
#
|
| 10 |
+
# Idempotent: re-running skips completed phases. Delete the phase markers under
|
| 11 |
+
# $ROOT/.markers to force a phase to re-run.
|
| 12 |
+
#
|
| 13 |
+
# ---------------------------------------------------------------------------
|
| 14 |
+
# WHY THESE PINS
|
| 15 |
+
# ---------------------------------------------------------------------------
|
| 16 |
+
# evalchemy teetone/evalchemy @ 7f24168 (2026-03-31)
|
| 17 |
+
# This is the pin the TPU pipeline used to produce the current results.md.
|
| 18 |
+
# Use it if you need numbers comparable to the existing tables.
|
| 19 |
+
# 460b022 (2026-05-07) is the newer pin carried on the marin-agents
|
| 20 |
+
# `evalfixhfauth` branch; it skips questions whose prompt >= max_model_len
|
| 21 |
+
# instead of crashing, and adds the TTC task variants. Set
|
| 22 |
+
# EVALCHEMY_COMMIT=460b022 if you hit prompt-length crashes.
|
| 23 |
+
#
|
| 24 |
+
# lm-eval stanford-crfm/lm-evaluation-harness @ d5e3391f22cde186c827674d5c3ec7c5f4fe0cab
|
| 25 |
+
# evalchemy is NOT pip-installable; it is a source checkout that imports
|
| 26 |
+
# lm_eval. This exact fork+commit is what Marin pins. Upstream lm-eval 0.5.x
|
| 27 |
+
# moved eval_logger and broke evalchemy's eval_tracker import.
|
| 28 |
+
#
|
| 29 |
+
# datasets >=3.6,<4.0.0 HARD upper bound.
|
| 30 |
+
# datasets v4 removed dataset-script support. LiveCodeBench loads
|
| 31 |
+
# `livecodebench/code_generation_lite` with trust_remote_code=True, which is
|
| 32 |
+
# a dataset script. On datasets>=4 every LCB task fails at load time.
|
| 33 |
+
#
|
| 34 |
+
# transformers >=4.51,<5 Qwen3 architecture support landed in 4.51.0.
|
| 35 |
+
#
|
| 36 |
+
# vllm >=0.10 CUDA wheel. Do NOT install tpu-inference / the
|
| 37 |
+
# `vllm` dependency group from lib/marin/pyproject.toml: that group pins
|
| 38 |
+
# jax==0.10.0 + libtpu==0.0.40 + tpu-inference==0.22.1 and is TPU-only.
|
| 39 |
+
#
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
# HUGGING FACE AUTH -- what the `evalfixhfauth` branch actually does
|
| 42 |
+
# ---------------------------------------------------------------------------
|
| 43 |
+
# The marin-agents branch name is stale/aspirational: no commit on it touches HF
|
| 44 |
+
# authentication. The real HF-related fixes on that branch are three, and a
|
| 45 |
+
# local user must reproduce all three:
|
| 46 |
+
#
|
| 47 |
+
# 1. HF_DATASETS_TRUST_REMOTE_CODE=1 in the eval process env (LiveCodeBench
|
| 48 |
+
# needs it). This script exports it in the generated activate snippet, and
|
| 49 |
+
# gpu_eval_driver.py sets it on every subprocess.
|
| 50 |
+
# 2. datasets<4.0.0 pin (above).
|
| 51 |
+
# 3. HF_TOKEN supplied as a process env var at submit time -- it is never in
|
| 52 |
+
# source. On TPU it was threaded through `iris job run -e HF_TOKEN ...`.
|
| 53 |
+
# LOCALLY YOU MUST DO THIS INSTEAD:
|
| 54 |
+
# huggingface-cli login # writes ~/.cache/huggingface/token
|
| 55 |
+
# or export HF_TOKEN=hf_xxx # per shell / systemd unit
|
| 56 |
+
# Required for gated datasets: cais/hle (HLE) and Idavidrein/gpqa
|
| 57 |
+
# (GPQADiamond) -- you must ALSO click "Agree" on those dataset pages with
|
| 58 |
+
# the same account. The math suite (MATH500, OlympiadBench, AIME24/25/26,
|
| 59 |
+
# HMMT) and the code suite (LiveCodeBench*) are public and need no token,
|
| 60 |
+
# though an anonymous box will hit HF rate limits during a 10-seed sweep.
|
| 61 |
+
#
|
| 62 |
+
set -euo pipefail
|
| 63 |
+
|
| 64 |
+
ROOT="${1:-/opt/marin-gpu-eval}"
|
| 65 |
+
EVALCHEMY_REPO="${EVALCHEMY_REPO:-https://github.com/teetone/evalchemy.git}"
|
| 66 |
+
EVALCHEMY_COMMIT="${EVALCHEMY_COMMIT:-7f24168}"
|
| 67 |
+
PYTHON_VERSION="${PYTHON_VERSION:-3.11}"
|
| 68 |
+
LM_EVAL_REQ="lm-eval[math]@git+https://github.com/stanford-crfm/lm-evaluation-harness@d5e3391f22cde186c827674d5c3ec7c5f4fe0cab"
|
| 69 |
+
|
| 70 |
+
VENV="$ROOT/venv"
|
| 71 |
+
EVALCHEMY_DIR="$ROOT/evalchemy"
|
| 72 |
+
CACHE="$ROOT/cache"
|
| 73 |
+
MARKERS="$ROOT/.markers"
|
| 74 |
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 75 |
+
|
| 76 |
+
mkdir -p "$ROOT" "$CACHE" "$MARKERS"
|
| 77 |
+
|
| 78 |
+
log() { printf '[bootstrap] %s\n' "$*" >&2; }
|
| 79 |
+
|
| 80 |
+
if ! command -v uv >/dev/null 2>&1; then
|
| 81 |
+
log "uv not found on PATH; install it with: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
| 82 |
+
exit 1
|
| 83 |
+
fi
|
| 84 |
+
|
| 85 |
+
# --------------------------------------------------------------------------
|
| 86 |
+
# 1. venv
|
| 87 |
+
# --------------------------------------------------------------------------
|
| 88 |
+
if [[ ! -x "$VENV/bin/python" ]]; then
|
| 89 |
+
log "creating venv at $VENV (python $PYTHON_VERSION)"
|
| 90 |
+
uv venv --python "$PYTHON_VERSION" --seed "$VENV"
|
| 91 |
+
fi
|
| 92 |
+
PY="$VENV/bin/python"
|
| 93 |
+
|
| 94 |
+
# --------------------------------------------------------------------------
|
| 95 |
+
# 2. dependencies
|
| 96 |
+
# --------------------------------------------------------------------------
|
| 97 |
+
if [[ ! -f "$MARKERS/deps" ]]; then
|
| 98 |
+
log "installing CUDA vLLM + lm-eval + evalchemy runtime deps"
|
| 99 |
+
# Driver-aware: on driver 535 (no CUDA 12.6+) invoke with
|
| 100 |
+
# TORCH_PRE="torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124"
|
| 101 |
+
# VLLM_SPEC="vllm==0.8.5"
|
| 102 |
+
# vLLM >=0.9 ships cu126/cu130 wheels that will NOT run on driver 535.
|
| 103 |
+
if [[ -n "${TORCH_PRE:-}" ]]; then
|
| 104 |
+
log "pre-installing pinned torch: $TORCH_PRE"
|
| 105 |
+
UV_CACHE_DIR="$CACHE/uv" uv pip install --python "$PY" $TORCH_PRE
|
| 106 |
+
fi
|
| 107 |
+
UV_CACHE_DIR="$CACHE/uv" uv pip install --python "$PY" \
|
| 108 |
+
"${VLLM_SPEC:-vllm>=0.10}" \
|
| 109 |
+
"transformers>=4.51,<5" \
|
| 110 |
+
"datasets>=3.6,<4.0.0" \
|
| 111 |
+
"fsspec[http]>=2023.1.0" \
|
| 112 |
+
"accelerate>=1.8" \
|
| 113 |
+
"safetensors" \
|
| 114 |
+
"sentencepiece" \
|
| 115 |
+
"$LM_EVAL_REQ" \
|
| 116 |
+
"sympy>=1.12.1,<1.14" \
|
| 117 |
+
"antlr4-python3-runtime==4.11" \
|
| 118 |
+
"bespokelabs-curator==0.1.16" \
|
| 119 |
+
"sqlalchemy" \
|
| 120 |
+
"fire" \
|
| 121 |
+
"pandas" \
|
| 122 |
+
"numpy"
|
| 123 |
+
touch "$MARKERS/deps"
|
| 124 |
+
else
|
| 125 |
+
log "deps marker present; skipping install"
|
| 126 |
+
fi
|
| 127 |
+
|
| 128 |
+
# Guard: datasets v4 silently breaks every LiveCodeBench task.
|
| 129 |
+
"$PY" - <<'PYCHECK'
|
| 130 |
+
import sys
|
| 131 |
+
from importlib.metadata import version
|
| 132 |
+
dv = version("datasets")
|
| 133 |
+
major = int(dv.split(".")[0])
|
| 134 |
+
if major >= 4:
|
| 135 |
+
sys.exit(
|
| 136 |
+
f"datasets {dv} installed but LiveCodeBench requires <4.0.0 "
|
| 137 |
+
"(v4 dropped dataset-script support). Reinstall with 'datasets>=3.6,<4.0.0'."
|
| 138 |
+
)
|
| 139 |
+
print(f"[bootstrap] datasets {dv} OK")
|
| 140 |
+
PYCHECK
|
| 141 |
+
|
| 142 |
+
# --------------------------------------------------------------------------
|
| 143 |
+
# 3. evalchemy checkout, pinned + patched
|
| 144 |
+
# --------------------------------------------------------------------------
|
| 145 |
+
if [[ ! -d "$EVALCHEMY_DIR/.git" ]]; then
|
| 146 |
+
log "cloning evalchemy -> $EVALCHEMY_DIR"
|
| 147 |
+
git clone "$EVALCHEMY_REPO" "$EVALCHEMY_DIR"
|
| 148 |
+
fi
|
| 149 |
+
|
| 150 |
+
log "pinning evalchemy to $EVALCHEMY_COMMIT and reverting previous patches"
|
| 151 |
+
git -C "$EVALCHEMY_DIR" fetch --all --quiet || true
|
| 152 |
+
git -C "$EVALCHEMY_DIR" checkout --quiet "$EVALCHEMY_COMMIT"
|
| 153 |
+
# Hard-reset the files the patcher rewrites so patching is deterministic.
|
| 154 |
+
git -C "$EVALCHEMY_DIR" checkout --quiet "$EVALCHEMY_COMMIT" -- eval/
|
| 155 |
+
|
| 156 |
+
log "applying GPU patches"
|
| 157 |
+
"$PY" "$HERE/patch_evalchemy_gpu.py" "$EVALCHEMY_DIR"
|
| 158 |
+
|
| 159 |
+
# --------------------------------------------------------------------------
|
| 160 |
+
# 4. activate snippet
|
| 161 |
+
# --------------------------------------------------------------------------
|
| 162 |
+
cat > "$ROOT/activate.sh" <<ACTEOF
|
| 163 |
+
# source this before running gpu_eval_driver.py / gpu_eval_monitor.py by hand
|
| 164 |
+
export MARIN_GPU_EVAL_ROOT="$ROOT"
|
| 165 |
+
export EVALCHEMY_DIR="$EVALCHEMY_DIR"
|
| 166 |
+
export VIRTUAL_ENV="$VENV"
|
| 167 |
+
export PATH="$VENV/bin:\$PATH"
|
| 168 |
+
|
| 169 |
+
# HF / dataset behaviour required by LiveCodeBench and the math suite
|
| 170 |
+
export HF_DATASETS_TRUST_REMOTE_CODE=1
|
| 171 |
+
export HF_HOME="$CACHE/huggingface"
|
| 172 |
+
export HF_HUB_CACHE="$CACHE/huggingface/hub"
|
| 173 |
+
export HF_DATASETS_CACHE="$CACHE/datasets"
|
| 174 |
+
export HF_ALLOW_CODE_EVAL=1
|
| 175 |
+
|
| 176 |
+
# vLLM / torch caches kept off \$HOME so concurrent runs do not fight
|
| 177 |
+
export VLLM_CACHE_ROOT="$CACHE/vllm"
|
| 178 |
+
export VLLM_CONFIG_ROOT="$CACHE/vllm"
|
| 179 |
+
export TORCHINDUCTOR_CACHE_DIR="$CACHE/torchinductor"
|
| 180 |
+
export TRITON_CACHE_DIR="$CACHE/triton"
|
| 181 |
+
export TOKENIZERS_PARALLELISM=false
|
| 182 |
+
|
| 183 |
+
# Supply your own:
|
| 184 |
+
# export HF_TOKEN=hf_xxx # needed for cais/hle and Idavidrein/gpqa
|
| 185 |
+
ACTEOF
|
| 186 |
+
|
| 187 |
+
log "done"
|
| 188 |
+
log ""
|
| 189 |
+
log " source $ROOT/activate.sh"
|
| 190 |
+
log " export HF_TOKEN=... # only needed for the science suite"
|
| 191 |
+
log " python $HERE/gpu_eval_driver.py --help"
|
| 192 |
+
log ""
|
| 193 |
+
log "evalchemy: $EVALCHEMY_DIR @ $(git -C "$EVALCHEMY_DIR" rev-parse --short HEAD)"
|
| 194 |
+
log "python: $PY"
|
gpu-sft/scripts/gpu_eval/gpu_eval_driver.py
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Copyright The Marin Authors
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
"""Run Marin's evalchemy benchmark suites against a LOCAL HF checkpoint on one GPU box.
|
| 5 |
+
|
| 6 |
+
This is the local-disk, CUDA-vLLM replacement for the TPU path
|
| 7 |
+
(``experiments/evals/exp_evalchemy_eval.py`` + ``EvalchemyEvaluator``). It keeps
|
| 8 |
+
the benchmark set, the sampling parameters and -- critically -- the on-disk
|
| 9 |
+
result schema that ``claude/compile_results.py`` reads to build results.md.
|
| 10 |
+
|
| 11 |
+
python gpu_eval_driver.py \\
|
| 12 |
+
--checkpoint /data/runs/exp_sft_qwen3_8b_x/hf/step-300 \\
|
| 13 |
+
--experiment exp_sft_qwen3_8b_x \\
|
| 14 |
+
--suite math \\
|
| 15 |
+
--results-root /data/marin/evaluation/evalchemy
|
| 16 |
+
|
| 17 |
+
WHAT IS FAITHFUL TO THE TPU PIPELINE
|
| 18 |
+
* Task set and per-task seed counts (math AIME*/AMC23/HMMT x10, MATH500 and
|
| 19 |
+
OlympiadBench x1; science x3; code x6), seeds 42..51.
|
| 20 |
+
* num_fewshot=0, chat template applied, temperature 0.7, top_p 1.0
|
| 21 |
+
(both are the benchmark/vLLM defaults; Marin's --gen_kwargs never reached
|
| 22 |
+
evalchemy chat benchmarks on TPU either), max_gen_toks 32768,
|
| 23 |
+
max_model_len = 32768 + 4096 = 36864.
|
| 24 |
+
* Real per-benchmark graders (is_equiv, symbolic, execution) -- the naive
|
| 25 |
+
string re-grade bug in ``evalchemy_results_compiler.py`` is NOT reproduced.
|
| 26 |
+
* Output layout, so the existing results.md compiler needs zero changes:
|
| 27 |
+
<root>/<experiment>-step<N>/<TASK>_seed<S>-<h>/<TASK>_0shot/<model>/results_<iso>.json
|
| 28 |
+
<root>/<experiment>-step<N>/compile_<TASK>_avg<K>seeds-<h>/compiled_results/averaged_results.json
|
| 29 |
+
|
| 30 |
+
WHAT DELIBERATELY DIFFERS
|
| 31 |
+
* ONE vLLM engine per task instead of one Iris job per (task, seed).
|
| 32 |
+
TPU/JAX cannot do per-request seeds, so Marin faked multi-seed by launching
|
| 33 |
+
K whole jobs with K engine seeds. CUDA vLLM *can* do per-request seeds, so
|
| 34 |
+
we set EVALCHEMY_N_REPEAT=K and pass ``--seed 42,42,42,42``; evalchemy's
|
| 35 |
+
benchmarks then use per-request seed ``42 + repetition_index``, which maps
|
| 36 |
+
exactly onto Marin seeds 42..42+K-1. One model load instead of K.
|
| 37 |
+
Consequence: numbers are NOT bit-comparable to the TPU tables (different
|
| 38 |
+
hardware, different kernels, per-request vs engine seeding). Rankings are.
|
| 39 |
+
* tensor_parallel_size defaults to the number of visible GPUs, not 32.
|
| 40 |
+
* max_num_seqs defaults to 32-64 per the GPU profile, not 256. 256 on a
|
| 41 |
+
single 80GB card guarantees KV-cache thrash and preemption at 36864 ctx.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
from __future__ import annotations
|
| 45 |
+
|
| 46 |
+
import argparse
|
| 47 |
+
import csv
|
| 48 |
+
import dataclasses
|
| 49 |
+
import hashlib
|
| 50 |
+
import json
|
| 51 |
+
import logging
|
| 52 |
+
import os
|
| 53 |
+
import re
|
| 54 |
+
import shutil
|
| 55 |
+
import statistics
|
| 56 |
+
import subprocess
|
| 57 |
+
import sys
|
| 58 |
+
import time
|
| 59 |
+
from datetime import datetime
|
| 60 |
+
from pathlib import Path
|
| 61 |
+
from typing import Any
|
| 62 |
+
|
| 63 |
+
logger = logging.getLogger("gpu_eval_driver")
|
| 64 |
+
|
| 65 |
+
# --------------------------------------------------------------------------
|
| 66 |
+
# Task tables. Mirrors experiments/evals/exp_evalchemy_eval.py and
|
| 67 |
+
# experiments/evals/evalchemy_task_configs.py.
|
| 68 |
+
# --------------------------------------------------------------------------
|
| 69 |
+
|
| 70 |
+
SEED_BASE = 42
|
| 71 |
+
|
| 72 |
+
# task name (as evalchemy knows it) -> number of seeds
|
| 73 |
+
SUITES: dict[str, dict[str, int]] = {
|
| 74 |
+
"math": {
|
| 75 |
+
"MATH500": 1,
|
| 76 |
+
"OlympiadBench": 1,
|
| 77 |
+
"AIME24": 10,
|
| 78 |
+
"AIME25": 10,
|
| 79 |
+
"AIME26": 10,
|
| 80 |
+
"AMC23": 10,
|
| 81 |
+
"HMMT": 10,
|
| 82 |
+
},
|
| 83 |
+
"science": {
|
| 84 |
+
"GPQADiamond": 3,
|
| 85 |
+
"JEEBench": 3,
|
| 86 |
+
"HLE": 3,
|
| 87 |
+
"OlympiadBench_Physics": 3,
|
| 88 |
+
},
|
| 89 |
+
"code": {
|
| 90 |
+
"LiveCodeBench": 6,
|
| 91 |
+
"LiveCodeBenchv5_official": 6,
|
| 92 |
+
"LiveCodeBenchv6_official": 6,
|
| 93 |
+
},
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
ALL_TASK_SEEDS: dict[str, int] = {task: n for suite in SUITES.values() for task, n in suite.items()}
|
| 97 |
+
|
| 98 |
+
# Benchmarks that are single-pass by construction: they carry no ``n_repeat``
|
| 99 |
+
# attribute, so EVALCHEMY_N_REPEAT is inert and they emit ``accuracy`` rather
|
| 100 |
+
# than ``accuracy_avg``/``run_stats``.
|
| 101 |
+
SINGLE_PASS_TASKS = frozenset({"MATH500", "OlympiadBench", "OlympiadBench_Physics"})
|
| 102 |
+
|
| 103 |
+
# Generation / engine constants, matching the TPU pipeline.
|
| 104 |
+
MAX_GEN_TOKS = 32768
|
| 105 |
+
CONTEXT_BUFFER = 4096
|
| 106 |
+
MAX_MODEL_LEN = MAX_GEN_TOKS + CONTEXT_BUFFER # 36864
|
| 107 |
+
TEMPERATURE = 0.7
|
| 108 |
+
TOP_P = 1.0
|
| 109 |
+
NUM_FEWSHOT = 0
|
| 110 |
+
|
| 111 |
+
RESULTS_FILE_RE = re.compile(r"^results_.*\.json$")
|
| 112 |
+
STEP_RE = re.compile(r"step-(\d+)")
|
| 113 |
+
|
| 114 |
+
# --------------------------------------------------------------------------
|
| 115 |
+
# GPU engine profiles
|
| 116 |
+
# --------------------------------------------------------------------------
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@dataclasses.dataclass(frozen=True)
|
| 120 |
+
class GpuProfile:
|
| 121 |
+
"""vLLM engine settings for an 8B bf16 model at 36864 context."""
|
| 122 |
+
|
| 123 |
+
name: str
|
| 124 |
+
gpu_memory_utilization: float
|
| 125 |
+
max_num_seqs: int
|
| 126 |
+
max_num_batched_tokens: int
|
| 127 |
+
# Minimum bytes vLLM must be allowed to claim on each GPU before the run is
|
| 128 |
+
# worth starting: weights + activations + a few full-length sequences of KV.
|
| 129 |
+
min_budget_gib_per_gpu: float
|
| 130 |
+
min_gpus: int
|
| 131 |
+
notes: str
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
GPU_PROFILES: dict[str, GpuProfile] = {
|
| 135 |
+
# 1x 80GB is the reference config: 16.4 GiB bf16 weights, ~4-6 GiB
|
| 136 |
+
# activations/CUDA graphs, ~48 GiB KV = ~340k cached tokens = ~9 concurrent
|
| 137 |
+
# full-length (36864) sequences, far more at realistic generation lengths.
|
| 138 |
+
"a100-80g": GpuProfile(
|
| 139 |
+
name="a100-80g",
|
| 140 |
+
gpu_memory_utilization=0.90,
|
| 141 |
+
max_num_seqs=64,
|
| 142 |
+
max_num_batched_tokens=8192,
|
| 143 |
+
min_budget_gib_per_gpu=40.0,
|
| 144 |
+
min_gpus=1,
|
| 145 |
+
notes="A100-SXM/PCIe 80GB. 0.90 leaves ~8 GiB for the driver+NCCL. Do not exceed 0.92.",
|
| 146 |
+
),
|
| 147 |
+
"h100-80g": GpuProfile(
|
| 148 |
+
name="h100-80g",
|
| 149 |
+
gpu_memory_utilization=0.90,
|
| 150 |
+
max_num_seqs=64,
|
| 151 |
+
max_num_batched_tokens=16384,
|
| 152 |
+
min_budget_gib_per_gpu=40.0,
|
| 153 |
+
min_gpus=1,
|
| 154 |
+
notes="H100 80GB HBM3. Higher prefill budget than A100; FlashAttention-3 kernels.",
|
| 155 |
+
),
|
| 156 |
+
"h200-141g": GpuProfile(
|
| 157 |
+
name="h200-141g",
|
| 158 |
+
gpu_memory_utilization=0.90,
|
| 159 |
+
max_num_seqs=128,
|
| 160 |
+
max_num_batched_tokens=16384,
|
| 161 |
+
min_budget_gib_per_gpu=40.0,
|
| 162 |
+
min_gpus=1,
|
| 163 |
+
notes="H200 141GB. KV cache stops being the bottleneck; raise max_num_seqs.",
|
| 164 |
+
),
|
| 165 |
+
# 40GB: 0.92*39.6 = 36.4 GiB budget, minus 16.4 weights minus ~4 overhead
|
| 166 |
+
# leaves ~16 GiB KV = ~113k tokens = 3 full-length sequences. Runs, but slow
|
| 167 |
+
# and preemption-heavy. Use two cards.
|
| 168 |
+
"a100-40g": GpuProfile(
|
| 169 |
+
name="a100-40g",
|
| 170 |
+
gpu_memory_utilization=0.92,
|
| 171 |
+
max_num_seqs=16,
|
| 172 |
+
max_num_batched_tokens=4096,
|
| 173 |
+
min_budget_gib_per_gpu=34.0,
|
| 174 |
+
min_gpus=1,
|
| 175 |
+
notes="A100 40GB. Tight for 8B at 36864 ctx; strongly prefer --tensor-parallel-size 2.",
|
| 176 |
+
),
|
| 177 |
+
"l40s-48g": GpuProfile(
|
| 178 |
+
name="l40s-48g",
|
| 179 |
+
gpu_memory_utilization=0.92,
|
| 180 |
+
max_num_seqs=24,
|
| 181 |
+
max_num_batched_tokens=4096,
|
| 182 |
+
min_budget_gib_per_gpu=34.0,
|
| 183 |
+
min_gpus=1,
|
| 184 |
+
notes="L40S 48GB, no NVLink. Keep tensor_parallel_size=1; PCIe TP is slower than 1 card.",
|
| 185 |
+
),
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
# nvidia-smi product name substring -> profile
|
| 189 |
+
_NAME_TO_PROFILE = (
|
| 190 |
+
("H200", "h200-141g"),
|
| 191 |
+
("H100", "h100-80g"),
|
| 192 |
+
("A100-SXM4-40GB", "a100-40g"),
|
| 193 |
+
("A100 40GB", "a100-40g"),
|
| 194 |
+
("A100", "a100-80g"),
|
| 195 |
+
("L40S", "l40s-48g"),
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
@dataclasses.dataclass(frozen=True)
|
| 200 |
+
class GpuInfo:
|
| 201 |
+
index: int
|
| 202 |
+
name: str
|
| 203 |
+
total_mib: int
|
| 204 |
+
used_mib: int
|
| 205 |
+
free_mib: int
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def query_gpus() -> list[GpuInfo]:
|
| 209 |
+
"""Read nvidia-smi. Honours CUDA_VISIBLE_DEVICES ordering when set."""
|
| 210 |
+
smi = shutil.which("nvidia-smi")
|
| 211 |
+
if smi is None:
|
| 212 |
+
raise RuntimeError("nvidia-smi not found; this driver requires an NVIDIA GPU box")
|
| 213 |
+
out = subprocess.run(
|
| 214 |
+
[smi, "--query-gpu=index,name,memory.total,memory.used,memory.free", "--format=csv,noheader,nounits"],
|
| 215 |
+
check=True,
|
| 216 |
+
capture_output=True,
|
| 217 |
+
text=True,
|
| 218 |
+
).stdout
|
| 219 |
+
gpus = []
|
| 220 |
+
for line in out.strip().splitlines():
|
| 221 |
+
index, name, total, used, free = (part.strip() for part in line.split(","))
|
| 222 |
+
gpus.append(GpuInfo(int(index), name, int(total), int(used), int(free)))
|
| 223 |
+
|
| 224 |
+
visible = os.environ.get("CUDA_VISIBLE_DEVICES")
|
| 225 |
+
if visible:
|
| 226 |
+
wanted = [int(x) for x in visible.split(",") if x.strip() != ""]
|
| 227 |
+
by_index = {g.index: g for g in gpus}
|
| 228 |
+
missing = [i for i in wanted if i not in by_index]
|
| 229 |
+
if missing:
|
| 230 |
+
raise RuntimeError(f"CUDA_VISIBLE_DEVICES names GPUs {missing} that nvidia-smi does not report")
|
| 231 |
+
gpus = [by_index[i] for i in wanted]
|
| 232 |
+
return gpus
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def detect_profile(gpus: list[GpuInfo]) -> GpuProfile:
|
| 236 |
+
name = gpus[0].name
|
| 237 |
+
for needle, key in _NAME_TO_PROFILE:
|
| 238 |
+
if needle.lower() in name.lower():
|
| 239 |
+
profile = GPU_PROFILES[key]
|
| 240 |
+
# An "A100" with <60GB is the 40GB SKU regardless of the name string.
|
| 241 |
+
if key == "a100-80g" and gpus[0].total_mib < 60_000:
|
| 242 |
+
profile = GPU_PROFILES["a100-40g"]
|
| 243 |
+
logger.info("detected GPU %r -> profile %s", name, profile.name)
|
| 244 |
+
return profile
|
| 245 |
+
raise RuntimeError(
|
| 246 |
+
f"no built-in profile for GPU {name!r}; pass --gpu-profile explicitly "
|
| 247 |
+
f"(one of: {', '.join(sorted(GPU_PROFILES))})"
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def resolve_gpu_memory_utilization(profile: GpuProfile, gpus: list[GpuInfo], headroom_mib: int) -> float:
|
| 252 |
+
"""Clamp gpu_memory_utilization so co-tenants on the same card do not OOM us.
|
| 253 |
+
|
| 254 |
+
THE PITFALL THIS EXISTS FOR: vLLM interprets ``gpu_memory_utilization`` as a
|
| 255 |
+
fraction of TOTAL device memory, not of FREE device memory. If an embedding
|
| 256 |
+
server (or a JAX training process, which preallocates 75% by default) already
|
| 257 |
+
holds 10 GiB of an 80 GiB card and you ask for 0.90, vLLM will size its KV
|
| 258 |
+
pool as if it owned 72 GiB. Total demand becomes 82 GiB and one of the two
|
| 259 |
+
processes dies with CUDA OOM -- and it usually dies *mid-run*, after the KV
|
| 260 |
+
pool is grown, not at init, so you lose hours of generation.
|
| 261 |
+
|
| 262 |
+
So: measure what is already resident, and cap the fraction at what is
|
| 263 |
+
actually claimable. If that leaves less than the profile's minimum budget,
|
| 264 |
+
refuse to start rather than crash later.
|
| 265 |
+
"""
|
| 266 |
+
worst = min(gpus, key=lambda g: g.free_mib)
|
| 267 |
+
claimable_mib = worst.free_mib - headroom_mib
|
| 268 |
+
if claimable_mib <= 0:
|
| 269 |
+
raise RuntimeError(
|
| 270 |
+
f"GPU {worst.index} ({worst.name}) has only {worst.free_mib} MiB free "
|
| 271 |
+
f"(headroom {headroom_mib} MiB). Something else owns this card."
|
| 272 |
+
)
|
| 273 |
+
allowed_fraction = claimable_mib / worst.total_mib
|
| 274 |
+
gmu = min(profile.gpu_memory_utilization, allowed_fraction)
|
| 275 |
+
|
| 276 |
+
budget_gib = gmu * worst.total_mib / 1024.0
|
| 277 |
+
if budget_gib < profile.min_budget_gib_per_gpu:
|
| 278 |
+
raise RuntimeError(
|
| 279 |
+
f"only {budget_gib:.1f} GiB claimable on GPU {worst.index} "
|
| 280 |
+
f"({worst.used_mib} MiB already in use by another process) but profile "
|
| 281 |
+
f"{profile.name} needs {profile.min_budget_gib_per_gpu:.1f} GiB. "
|
| 282 |
+
"Free the card, pin the other process to a different GPU with "
|
| 283 |
+
"CUDA_VISIBLE_DEVICES, or raise --tensor-parallel-size."
|
| 284 |
+
)
|
| 285 |
+
if gmu < profile.gpu_memory_utilization:
|
| 286 |
+
logger.warning(
|
| 287 |
+
"clamping gpu_memory_utilization %.2f -> %.3f: GPU %d already has %d MiB resident",
|
| 288 |
+
profile.gpu_memory_utilization,
|
| 289 |
+
gmu,
|
| 290 |
+
worst.index,
|
| 291 |
+
worst.used_mib,
|
| 292 |
+
)
|
| 293 |
+
return round(gmu, 3)
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def kv_bytes_per_token(checkpoint: Path) -> int | None:
|
| 297 |
+
"""Estimate bf16 KV cache cost per token from the checkpoint's config.json."""
|
| 298 |
+
config_path = checkpoint / "config.json"
|
| 299 |
+
if not config_path.exists():
|
| 300 |
+
return None
|
| 301 |
+
cfg = json.loads(config_path.read_text())
|
| 302 |
+
layers = cfg.get("num_hidden_layers")
|
| 303 |
+
kv_heads = cfg.get("num_key_value_heads") or cfg.get("num_attention_heads")
|
| 304 |
+
head_dim = cfg.get("head_dim")
|
| 305 |
+
if head_dim is None and cfg.get("hidden_size") and cfg.get("num_attention_heads"):
|
| 306 |
+
head_dim = cfg["hidden_size"] // cfg["num_attention_heads"]
|
| 307 |
+
if not (layers and kv_heads and head_dim):
|
| 308 |
+
return None
|
| 309 |
+
return layers * kv_heads * head_dim * 2 * 2 # K and V, 2 bytes each
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
# --------------------------------------------------------------------------
|
| 313 |
+
# Output layout -- must stay byte-compatible with claude/compile_results.py
|
| 314 |
+
# --------------------------------------------------------------------------
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def sanitize_model_name(name: str) -> str:
|
| 318 |
+
"""Reproduce lm-eval's GeneralConfigTracker.model_name_sanitized."""
|
| 319 |
+
return re.sub(r"[\"<>:/\|\\?\*\[\]]+", "__", name)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def _hash6(*parts: str) -> str:
|
| 323 |
+
"""Stable 6-hex suffix, standing in for the Marin executor's config hash.
|
| 324 |
+
|
| 325 |
+
The results.md compiler only substring-matches ``<TASK>_seed<S>-`` and
|
| 326 |
+
``compile_<TASK>_avg<K>seeds-``, so the value is opaque -- but it must be
|
| 327 |
+
STABLE so re-runs land in the same directory instead of piling up.
|
| 328 |
+
"""
|
| 329 |
+
return hashlib.md5("\x00".join(parts).encode()).hexdigest()[:6]
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def step_of(checkpoint: Path) -> str | None:
|
| 333 |
+
match = STEP_RE.search(str(checkpoint))
|
| 334 |
+
return match.group(1) if match else None
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
@dataclasses.dataclass(frozen=True)
|
| 338 |
+
class Layout:
|
| 339 |
+
"""Every path a single (checkpoint, task) evaluation reads or writes."""
|
| 340 |
+
|
| 341 |
+
results_root: Path
|
| 342 |
+
experiment: str
|
| 343 |
+
step: str | None
|
| 344 |
+
task: str
|
| 345 |
+
seeds: tuple[int, ...]
|
| 346 |
+
model_dir_name: str
|
| 347 |
+
|
| 348 |
+
@property
|
| 349 |
+
def eval_dir(self) -> Path:
|
| 350 |
+
name = f"{self.experiment}-step{self.step}" if self.step else self.experiment
|
| 351 |
+
return self.results_root / name
|
| 352 |
+
|
| 353 |
+
def seed_dir(self, seed: int) -> Path:
|
| 354 |
+
suffix = _hash6(self.experiment, self.step or "", self.task, str(seed))
|
| 355 |
+
return self.eval_dir / f"{self.task}_seed{seed}-{suffix}"
|
| 356 |
+
|
| 357 |
+
def seed_results_dir(self, seed: int) -> Path:
|
| 358 |
+
return self.seed_dir(seed) / f"{self.task}_{NUM_FEWSHOT}shot" / self.model_dir_name
|
| 359 |
+
|
| 360 |
+
@property
|
| 361 |
+
def compile_dir(self) -> Path | None:
|
| 362 |
+
if len(self.seeds) < 2:
|
| 363 |
+
return None
|
| 364 |
+
suffix = _hash6(self.experiment, self.step or "", self.task, "compile")
|
| 365 |
+
return self.eval_dir / f"compile_{self.task}_avg{len(self.seeds)}seeds-{suffix}"
|
| 366 |
+
|
| 367 |
+
@property
|
| 368 |
+
def averaged_results(self) -> Path | None:
|
| 369 |
+
compile_dir = self.compile_dir
|
| 370 |
+
return None if compile_dir is None else compile_dir / "compiled_results" / "averaged_results.json"
|
| 371 |
+
|
| 372 |
+
@property
|
| 373 |
+
def raw_dir(self) -> Path:
|
| 374 |
+
return self.eval_dir / "_raw" / self.task
|
| 375 |
+
|
| 376 |
+
@property
|
| 377 |
+
def marker(self) -> Path:
|
| 378 |
+
return self.eval_dir / "_markers" / f"{self.task}.complete.json"
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def find_results_file(root: Path) -> Path | None:
|
| 382 |
+
candidates = [p for p in root.rglob("results_*.json") if RESULTS_FILE_RE.match(p.name)]
|
| 383 |
+
if not candidates:
|
| 384 |
+
return None
|
| 385 |
+
return max(candidates, key=lambda p: p.stat().st_mtime)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def read_task_accuracy(results_file: Path, task: str) -> float | None:
|
| 389 |
+
"""Read the metric the results.md compiler reads: accuracy_avg, else accuracy."""
|
| 390 |
+
try:
|
| 391 |
+
payload = json.loads(results_file.read_text())
|
| 392 |
+
except (OSError, json.JSONDecodeError):
|
| 393 |
+
return None
|
| 394 |
+
entry = payload.get("results", {}).get(task)
|
| 395 |
+
if not isinstance(entry, dict):
|
| 396 |
+
return None
|
| 397 |
+
for key in ("accuracy_avg", "accuracy"):
|
| 398 |
+
value = entry.get(key)
|
| 399 |
+
if isinstance(value, (int, float)):
|
| 400 |
+
return float(value)
|
| 401 |
+
return None
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def is_task_complete(layout: Layout) -> bool:
|
| 405 |
+
"""Same three-part definition the eval monitor and results.md compiler use."""
|
| 406 |
+
for seed in layout.seeds:
|
| 407 |
+
results_file = find_results_file(layout.seed_dir(seed))
|
| 408 |
+
if results_file is None or read_task_accuracy(results_file, layout.task) is None:
|
| 409 |
+
return False
|
| 410 |
+
averaged = layout.averaged_results
|
| 411 |
+
if averaged is not None:
|
| 412 |
+
if not averaged.exists():
|
| 413 |
+
return False
|
| 414 |
+
try:
|
| 415 |
+
payload = json.loads(averaged.read_text())
|
| 416 |
+
if payload[0]["num_seeds"] != len(layout.seeds):
|
| 417 |
+
return False
|
| 418 |
+
except (OSError, json.JSONDecodeError, KeyError, IndexError):
|
| 419 |
+
return False
|
| 420 |
+
return True
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
# --------------------------------------------------------------------------
|
| 424 |
+
# Running one task
|
| 425 |
+
# --------------------------------------------------------------------------
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def build_model_args(checkpoint: Path, *, profile: GpuProfile, gmu: float, tensor_parallel_size: int) -> str:
|
| 429 |
+
parts = [
|
| 430 |
+
f"pretrained={checkpoint}",
|
| 431 |
+
"dtype=bfloat16",
|
| 432 |
+
f"tensor_parallel_size={tensor_parallel_size}",
|
| 433 |
+
f"gpu_memory_utilization={gmu}",
|
| 434 |
+
f"max_model_len={MAX_MODEL_LEN}",
|
| 435 |
+
f"max_gen_toks={MAX_GEN_TOKS}",
|
| 436 |
+
f"max_num_seqs={profile.max_num_seqs}",
|
| 437 |
+
f"max_num_batched_tokens={profile.max_num_batched_tokens}",
|
| 438 |
+
"trust_remote_code=True",
|
| 439 |
+
# Deterministic-ish prefix reuse across the K repetitions of a task:
|
| 440 |
+
# every repetition sends the identical prompt, so the prefix cache turns
|
| 441 |
+
# K prefills into 1. Big win at 10 seeds.
|
| 442 |
+
"enable_prefix_caching=True",
|
| 443 |
+
]
|
| 444 |
+
return ",".join(parts)
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def subprocess_env(
|
| 448 |
+
*,
|
| 449 |
+
evalchemy_dir: Path,
|
| 450 |
+
n_repeat: int,
|
| 451 |
+
num_proc: int,
|
| 452 |
+
hf_cache: Path | None,
|
| 453 |
+
extra: dict[str, str] | None = None,
|
| 454 |
+
) -> dict[str, str]:
|
| 455 |
+
env = os.environ.copy()
|
| 456 |
+
env.update(
|
| 457 |
+
{
|
| 458 |
+
"EVALCHEMY_N_REPEAT": str(n_repeat),
|
| 459 |
+
"EVALCHEMY_NUM_PROC": str(num_proc),
|
| 460 |
+
# LiveCodeBench loads a dataset SCRIPT; without this it refuses.
|
| 461 |
+
"HF_DATASETS_TRUST_REMOTE_CODE": "1",
|
| 462 |
+
# lm-eval refuses to execute generated code without this.
|
| 463 |
+
"HF_ALLOW_CODE_EVAL": "1",
|
| 464 |
+
"TOKENIZERS_PARALLELISM": "false",
|
| 465 |
+
"PYTHONUNBUFFERED": "1",
|
| 466 |
+
# The LCB grader spawns children; keep them off the GPU entirely.
|
| 467 |
+
"VLLM_WORKER_MULTIPROC_METHOD": "spawn",
|
| 468 |
+
"PYTHONPATH": os.pathsep.join(
|
| 469 |
+
[str(evalchemy_dir), *([os.environ["PYTHONPATH"]] if os.environ.get("PYTHONPATH") else [])]
|
| 470 |
+
),
|
| 471 |
+
}
|
| 472 |
+
)
|
| 473 |
+
if hf_cache is not None:
|
| 474 |
+
env.update(
|
| 475 |
+
{
|
| 476 |
+
"HF_HOME": str(hf_cache),
|
| 477 |
+
"HF_HUB_CACHE": str(hf_cache / "hub"),
|
| 478 |
+
"HF_DATASETS_CACHE": str(hf_cache / "datasets"),
|
| 479 |
+
}
|
| 480 |
+
)
|
| 481 |
+
if extra:
|
| 482 |
+
env.update(extra)
|
| 483 |
+
return env
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def run_evalchemy(
|
| 487 |
+
*,
|
| 488 |
+
evalchemy_dir: Path,
|
| 489 |
+
checkpoint: Path,
|
| 490 |
+
task: str,
|
| 491 |
+
n_repeat: int,
|
| 492 |
+
out_dir: Path,
|
| 493 |
+
profile: GpuProfile,
|
| 494 |
+
gmu: float,
|
| 495 |
+
tensor_parallel_size: int,
|
| 496 |
+
batch_size: int,
|
| 497 |
+
hf_cache: Path | None,
|
| 498 |
+
num_proc: int,
|
| 499 |
+
debug: bool,
|
| 500 |
+
timeout: int | None,
|
| 501 |
+
) -> Path:
|
| 502 |
+
"""Invoke evalchemy once for one task, producing one results_*.json."""
|
| 503 |
+
entry = evalchemy_dir / "_marin_gpu_entry.py"
|
| 504 |
+
if not entry.exists():
|
| 505 |
+
raise RuntimeError(f"{entry} missing -- run patch_evalchemy_gpu.py against {evalchemy_dir}")
|
| 506 |
+
|
| 507 |
+
if out_dir.exists():
|
| 508 |
+
# Exactly one results file per run, so downstream globs are unambiguous.
|
| 509 |
+
shutil.rmtree(out_dir)
|
| 510 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 511 |
+
|
| 512 |
+
command = [
|
| 513 |
+
sys.executable,
|
| 514 |
+
str(entry),
|
| 515 |
+
"--model",
|
| 516 |
+
"vllm",
|
| 517 |
+
"--tasks",
|
| 518 |
+
task,
|
| 519 |
+
"--model_args",
|
| 520 |
+
build_model_args(checkpoint, profile=profile, gmu=gmu, tensor_parallel_size=tensor_parallel_size),
|
| 521 |
+
"--batch_size",
|
| 522 |
+
str(batch_size),
|
| 523 |
+
"--output_path",
|
| 524 |
+
str(out_dir),
|
| 525 |
+
"--verbosity",
|
| 526 |
+
"INFO",
|
| 527 |
+
"--apply_chat_template",
|
| 528 |
+
"--confirm_run_unsafe_code",
|
| 529 |
+
"--max_tokens",
|
| 530 |
+
str(MAX_GEN_TOKS),
|
| 531 |
+
# evalchemy hands seeds[0] to vLLM as the per-request seed and adds the
|
| 532 |
+
# repetition index, so repetition i runs at seed SEED_BASE + i.
|
| 533 |
+
"--seed",
|
| 534 |
+
",".join([str(SEED_BASE)] * 4),
|
| 535 |
+
"--gen_kwargs",
|
| 536 |
+
f"temperature={TEMPERATURE},top_p={TOP_P},max_gen_toks={MAX_GEN_TOKS}",
|
| 537 |
+
]
|
| 538 |
+
if debug:
|
| 539 |
+
command.append("--debug")
|
| 540 |
+
|
| 541 |
+
env = subprocess_env(evalchemy_dir=evalchemy_dir, n_repeat=n_repeat, num_proc=num_proc, hf_cache=hf_cache)
|
| 542 |
+
|
| 543 |
+
log_path = out_dir.parent / f"{task}.evalchemy.log"
|
| 544 |
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 545 |
+
logger.info("launching %s (n_repeat=%d) -> %s", task, n_repeat, log_path)
|
| 546 |
+
logger.debug("command: %s", " ".join(command))
|
| 547 |
+
|
| 548 |
+
started = time.time()
|
| 549 |
+
with log_path.open("w") as log_file:
|
| 550 |
+
log_file.write(f"# {' '.join(command)}\n\n")
|
| 551 |
+
log_file.flush()
|
| 552 |
+
process = subprocess.Popen(
|
| 553 |
+
command,
|
| 554 |
+
cwd=str(evalchemy_dir),
|
| 555 |
+
env=env,
|
| 556 |
+
stdout=log_file,
|
| 557 |
+
stderr=subprocess.STDOUT,
|
| 558 |
+
)
|
| 559 |
+
try:
|
| 560 |
+
returncode = process.wait(timeout=timeout)
|
| 561 |
+
except subprocess.TimeoutExpired:
|
| 562 |
+
process.kill()
|
| 563 |
+
process.wait()
|
| 564 |
+
raise RuntimeError(f"{task} exceeded timeout of {timeout}s; see {log_path}") from None
|
| 565 |
+
|
| 566 |
+
elapsed = time.time() - started
|
| 567 |
+
results_file = find_results_file(out_dir)
|
| 568 |
+
|
| 569 |
+
# Exit 0 with no results file means scoring hung or crashed silently -- the
|
| 570 |
+
# same guard the TPU evaluator carries. Treat it as a hard failure.
|
| 571 |
+
if returncode != 0:
|
| 572 |
+
raise RuntimeError(f"{task} exited {returncode} after {elapsed:.0f}s; see {log_path}")
|
| 573 |
+
if results_file is None:
|
| 574 |
+
raise RuntimeError(
|
| 575 |
+
f"{task} exited 0 after {elapsed:.0f}s but wrote no results_*.json under {out_dir}; "
|
| 576 |
+
f"scoring likely hung or crashed silently. See {log_path}"
|
| 577 |
+
)
|
| 578 |
+
logger.info("%s finished in %.0fs -> %s", task, elapsed, results_file)
|
| 579 |
+
return results_file
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
# --------------------------------------------------------------------------
|
| 583 |
+
# Fan-out: one combined run -> per-seed result files + a compile file
|
| 584 |
+
# --------------------------------------------------------------------------
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
def per_repetition_accuracies(task_entry: dict[str, Any], n_repeat: int, task: str) -> list[float]:
|
| 588 |
+
run_stats = task_entry.get("run_stats")
|
| 589 |
+
if isinstance(run_stats, list) and len(run_stats) == n_repeat:
|
| 590 |
+
return [float(stat["accuracy"]) for stat in run_stats]
|
| 591 |
+
if n_repeat == 1:
|
| 592 |
+
for key in ("accuracy_avg", "accuracy"):
|
| 593 |
+
value = task_entry.get(key)
|
| 594 |
+
if isinstance(value, (int, float)):
|
| 595 |
+
return [float(value)]
|
| 596 |
+
raise RuntimeError(
|
| 597 |
+
f"{task}: expected {n_repeat} repetitions but the result carries "
|
| 598 |
+
f"run_stats={type(run_stats).__name__} of length "
|
| 599 |
+
f"{len(run_stats) if isinstance(run_stats, list) else 'n/a'}. "
|
| 600 |
+
"The n_repeat patch probably did not apply -- re-run patch_evalchemy_gpu.py."
|
| 601 |
+
)
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def slice_examples(examples: list[dict[str, Any]], index: int) -> list[dict[str, Any]] | None:
|
| 605 |
+
"""Project a multi-repetition example list down to one repetition.
|
| 606 |
+
|
| 607 |
+
Returns None when the benchmark does not keep per-repetition outputs
|
| 608 |
+
(LiveCodeBench* keep only the last run's examples), in which case the
|
| 609 |
+
per-seed file simply omits ``examples``.
|
| 610 |
+
"""
|
| 611 |
+
sliced: list[dict[str, Any]] = []
|
| 612 |
+
for example in examples:
|
| 613 |
+
answers = example.get("model_answers")
|
| 614 |
+
outputs = example.get("model_outputs")
|
| 615 |
+
if not isinstance(answers, list) or index >= len(answers):
|
| 616 |
+
return None
|
| 617 |
+
projected = dict(example)
|
| 618 |
+
projected["model_answers"] = [answers[index]]
|
| 619 |
+
if isinstance(outputs, list) and index < len(outputs):
|
| 620 |
+
projected["model_outputs"] = [outputs[index]]
|
| 621 |
+
sliced.append(projected)
|
| 622 |
+
return sliced
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
def write_per_seed_results(
|
| 626 |
+
*,
|
| 627 |
+
raw_results_file: Path,
|
| 628 |
+
layout: Layout,
|
| 629 |
+
task: str,
|
| 630 |
+
checkpoint: Path,
|
| 631 |
+
) -> dict[int, float]:
|
| 632 |
+
"""Split one combined run into K schema-compatible per-seed result files."""
|
| 633 |
+
raw = json.loads(raw_results_file.read_text())
|
| 634 |
+
entry = raw.get("results", {}).get(task)
|
| 635 |
+
if not isinstance(entry, dict):
|
| 636 |
+
available = sorted(raw.get("results", {}))
|
| 637 |
+
raise RuntimeError(f"{raw_results_file} has no results[{task!r}]; found {available}")
|
| 638 |
+
|
| 639 |
+
seeds = layout.seeds
|
| 640 |
+
accuracies = per_repetition_accuracies(entry, len(seeds), task)
|
| 641 |
+
run_stats = entry.get("run_stats") if isinstance(entry.get("run_stats"), list) else None
|
| 642 |
+
examples = entry.get("examples") if isinstance(entry.get("examples"), list) else None
|
| 643 |
+
|
| 644 |
+
per_seed: dict[int, float] = {}
|
| 645 |
+
timestamp = datetime.now().isoformat().replace(":", "-")
|
| 646 |
+
|
| 647 |
+
for index, seed in enumerate(seeds):
|
| 648 |
+
accuracy = accuracies[index]
|
| 649 |
+
stat = run_stats[index] if run_stats and index < len(run_stats) else {}
|
| 650 |
+
num_total = stat.get("num_total", entry.get("num_total"))
|
| 651 |
+
num_solved = stat.get("num_solved", entry.get("num_solved"))
|
| 652 |
+
|
| 653 |
+
seed_entry: dict[str, Any] = {
|
| 654 |
+
"num_total": num_total,
|
| 655 |
+
"num_solved": num_solved,
|
| 656 |
+
# Both keys, because the results.md compiler prefers accuracy_avg and
|
| 657 |
+
# falls back to accuracy, while some downstream readers expect the
|
| 658 |
+
# single-pass shape.
|
| 659 |
+
"accuracy": accuracy,
|
| 660 |
+
"accuracy_avg": accuracy,
|
| 661 |
+
"accuracy_std_err": 0.0,
|
| 662 |
+
"num_repeat": 1,
|
| 663 |
+
"solved_avg": num_solved,
|
| 664 |
+
"run_stats": [
|
| 665 |
+
{
|
| 666 |
+
"repetition": 1,
|
| 667 |
+
"num_total": num_total,
|
| 668 |
+
"num_solved": num_solved,
|
| 669 |
+
"accuracy": accuracy,
|
| 670 |
+
}
|
| 671 |
+
],
|
| 672 |
+
}
|
| 673 |
+
if examples is not None:
|
| 674 |
+
projected = slice_examples(examples, index)
|
| 675 |
+
if projected is not None:
|
| 676 |
+
seed_entry["examples"] = projected
|
| 677 |
+
# Carry through any extra per-difficulty metrics LCB emits.
|
| 678 |
+
for key, value in entry.items():
|
| 679 |
+
if key.startswith("accuracy_") and key.endswith("_avg") and key != "accuracy_avg":
|
| 680 |
+
seed_entry[key] = value
|
| 681 |
+
|
| 682 |
+
payload = {k: v for k, v in raw.items() if k != "results"}
|
| 683 |
+
payload["results"] = {task: seed_entry}
|
| 684 |
+
payload["_marin_gpu_port"] = {
|
| 685 |
+
"source_results_file": str(raw_results_file),
|
| 686 |
+
"repetition_index": index,
|
| 687 |
+
"seed": seed,
|
| 688 |
+
"note": (
|
| 689 |
+
"Synthesised from a single K-repetition evalchemy run. On GPU the "
|
| 690 |
+
"seed is per-request (SEED_BASE + repetition_index), not an engine "
|
| 691 |
+
"seed as on TPU."
|
| 692 |
+
),
|
| 693 |
+
"checkpoint": str(checkpoint),
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
out_dir = layout.seed_results_dir(seed)
|
| 697 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 698 |
+
for stale in out_dir.glob("results_*.json"):
|
| 699 |
+
stale.unlink()
|
| 700 |
+
(out_dir / f"results_{timestamp}.json").write_text(json.dumps(payload, indent=2, default=str))
|
| 701 |
+
per_seed[seed] = accuracy
|
| 702 |
+
|
| 703 |
+
logger.info("%s per-seed accuracies: %s", task, {s: round(a, 4) for s, a in per_seed.items()})
|
| 704 |
+
return per_seed
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
def write_compile_results(*, layout: Layout, per_seed: dict[int, float], checkpoint: Path) -> None:
|
| 708 |
+
"""Write the averaged_results.json that claude/compile_results.py reads.
|
| 709 |
+
|
| 710 |
+
Schema matches ``experiments/evals/evalchemy_results_compiler.py`` exactly:
|
| 711 |
+
a one-element JSON array with base_model_name, dataset_name, num_seeds,
|
| 712 |
+
seeds[], correct_mean, correct_std, correct_per_seed{}. ``correct_std`` is a
|
| 713 |
+
SAMPLE standard deviation (ddof=1), matching pandas Series.std(), and it is
|
| 714 |
+
what renders as the bracketed [x.x] in results.md.
|
| 715 |
+
|
| 716 |
+
Unlike the TPU compile step, the numbers here come from each benchmark's own
|
| 717 |
+
grader rather than a naive string re-match, so HMMT / JEEBench /
|
| 718 |
+
OlympiadBench_Physics / LiveCodeBench* are correct in this file too. The
|
| 719 |
+
results.md compiler still bypasses the compile dir for those six and averages
|
| 720 |
+
the per-seed files instead; both paths now agree.
|
| 721 |
+
"""
|
| 722 |
+
compile_dir = layout.compile_dir
|
| 723 |
+
if compile_dir is None:
|
| 724 |
+
return
|
| 725 |
+
|
| 726 |
+
seeds = sorted(per_seed)
|
| 727 |
+
values = [per_seed[s] for s in seeds]
|
| 728 |
+
mean = statistics.fmean(values)
|
| 729 |
+
std = statistics.stdev(values) if len(values) > 1 else 0.0
|
| 730 |
+
|
| 731 |
+
record = {
|
| 732 |
+
"base_model_name": layout.model_dir_name.lower(),
|
| 733 |
+
"dataset_name": layout.task.lower(),
|
| 734 |
+
"num_seeds": len(seeds),
|
| 735 |
+
"seeds": seeds,
|
| 736 |
+
"correct_mean": mean,
|
| 737 |
+
"correct_std": std,
|
| 738 |
+
"correct_per_seed": {str(s): per_seed[s] for s in seeds},
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
out_dir = compile_dir / "compiled_results"
|
| 742 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 743 |
+
(out_dir / "averaged_results.json").write_text(json.dumps([record], indent=2))
|
| 744 |
+
|
| 745 |
+
with (out_dir / "averaged_results.csv").open("w", newline="") as handle:
|
| 746 |
+
writer = csv.writer(handle)
|
| 747 |
+
writer.writerow(["base_model_name", "dataset_name", "num_seeds", "seeds", "correct_mean", "correct_std"])
|
| 748 |
+
writer.writerow(
|
| 749 |
+
[record["base_model_name"], record["dataset_name"], record["num_seeds"], seeds, mean, std]
|
| 750 |
+
)
|
| 751 |
+
|
| 752 |
+
# compiled_results.{json,csv} exist for parity with the TPU compile step.
|
| 753 |
+
# NOTE: the TPU version stores one row per graded EXAMPLE; we store one row
|
| 754 |
+
# per seed, because we take accuracy from the benchmark's own grader rather
|
| 755 |
+
# than re-grading examples with string equality. Nothing downstream reads it.
|
| 756 |
+
rows = [
|
| 757 |
+
{
|
| 758 |
+
"dataset_name": layout.task.lower(),
|
| 759 |
+
"model_name": layout.model_dir_name.lower(),
|
| 760 |
+
"seed": seed,
|
| 761 |
+
"accuracy": per_seed[seed],
|
| 762 |
+
"checkpoint": str(checkpoint),
|
| 763 |
+
}
|
| 764 |
+
for seed in seeds
|
| 765 |
+
]
|
| 766 |
+
(out_dir / "compiled_results.json").write_text(json.dumps(rows, indent=2))
|
| 767 |
+
with (out_dir / "compiled_results.csv").open("w", newline="") as handle:
|
| 768 |
+
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
| 769 |
+
writer.writeheader()
|
| 770 |
+
writer.writerows(rows)
|
| 771 |
+
|
| 772 |
+
logger.info(
|
| 773 |
+
"%s compiled: mean=%.4f std=%.4f over %d seeds -> %s",
|
| 774 |
+
layout.task,
|
| 775 |
+
mean,
|
| 776 |
+
std,
|
| 777 |
+
len(seeds),
|
| 778 |
+
out_dir / "averaged_results.json",
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
# --------------------------------------------------------------------------
|
| 783 |
+
# Checkpoint validation
|
| 784 |
+
# --------------------------------------------------------------------------
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
def validate_checkpoint(checkpoint: Path) -> None:
|
| 788 |
+
"""Reject half-written HF exports before burning an hour of GPU on them.
|
| 789 |
+
|
| 790 |
+
Levanter writes hf/step-N incrementally while training continues; a monitor
|
| 791 |
+
that races the export will otherwise load a truncated shard set.
|
| 792 |
+
"""
|
| 793 |
+
if not checkpoint.is_dir():
|
| 794 |
+
raise RuntimeError(f"{checkpoint} is not a directory")
|
| 795 |
+
if not (checkpoint / "config.json").exists():
|
| 796 |
+
raise RuntimeError(f"{checkpoint}/config.json missing")
|
| 797 |
+
if not any((checkpoint / name).exists() for name in ("tokenizer_config.json", "tokenizer.json")):
|
| 798 |
+
raise RuntimeError(f"{checkpoint} has no tokenizer files")
|
| 799 |
+
|
| 800 |
+
index_path = checkpoint / "model.safetensors.index.json"
|
| 801 |
+
if index_path.exists():
|
| 802 |
+
index = json.loads(index_path.read_text())
|
| 803 |
+
shards = sorted(set(index.get("weight_map", {}).values()))
|
| 804 |
+
missing = [s for s in shards if not (checkpoint / s).exists()]
|
| 805 |
+
if missing:
|
| 806 |
+
raise RuntimeError(f"{checkpoint} is incomplete: {len(missing)} of {len(shards)} shards missing: {missing[:3]}")
|
| 807 |
+
return
|
| 808 |
+
if not (checkpoint / "model.safetensors").exists():
|
| 809 |
+
raise RuntimeError(f"{checkpoint} has neither model.safetensors nor a shard index")
|
| 810 |
+
|
| 811 |
+
|
| 812 |
+
# --------------------------------------------------------------------------
|
| 813 |
+
# Entry point
|
| 814 |
+
# --------------------------------------------------------------------------
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def resolve_tasks(args: argparse.Namespace) -> dict[str, int]:
|
| 818 |
+
if args.tasks:
|
| 819 |
+
selected = {}
|
| 820 |
+
for name in args.tasks:
|
| 821 |
+
if name not in ALL_TASK_SEEDS:
|
| 822 |
+
raise SystemExit(f"unknown task {name!r}; known: {', '.join(sorted(ALL_TASK_SEEDS))}")
|
| 823 |
+
selected[name] = ALL_TASK_SEEDS[name]
|
| 824 |
+
elif args.suite:
|
| 825 |
+
selected = dict(SUITES[args.suite])
|
| 826 |
+
else:
|
| 827 |
+
raise SystemExit("pass --suite or --tasks")
|
| 828 |
+
if args.seeds is not None:
|
| 829 |
+
selected = {task: args.seeds for task in selected}
|
| 830 |
+
return selected
|
| 831 |
+
|
| 832 |
+
|
| 833 |
+
def evaluate_task(args: argparse.Namespace, task: str, n_seeds: int, profile: GpuProfile, gmu: float) -> dict[str, Any]:
|
| 834 |
+
checkpoint = args.checkpoint.resolve()
|
| 835 |
+
effective_seeds = 1 if task in SINGLE_PASS_TASKS else n_seeds
|
| 836 |
+
seeds = tuple(SEED_BASE + i for i in range(effective_seeds))
|
| 837 |
+
|
| 838 |
+
layout = Layout(
|
| 839 |
+
results_root=args.results_root.resolve(),
|
| 840 |
+
experiment=args.experiment,
|
| 841 |
+
step=step_of(checkpoint),
|
| 842 |
+
task=task,
|
| 843 |
+
seeds=seeds,
|
| 844 |
+
model_dir_name=sanitize_model_name(str(checkpoint)),
|
| 845 |
+
)
|
| 846 |
+
|
| 847 |
+
if not args.force and is_task_complete(layout):
|
| 848 |
+
logger.info("%s already complete for %s; skipping", task, layout.eval_dir.name)
|
| 849 |
+
return {"task": task, "status": "cached", "seeds": list(seeds)}
|
| 850 |
+
|
| 851 |
+
started = time.time()
|
| 852 |
+
raw_results_file = run_evalchemy(
|
| 853 |
+
evalchemy_dir=args.evalchemy_dir.resolve(),
|
| 854 |
+
checkpoint=checkpoint,
|
| 855 |
+
task=task,
|
| 856 |
+
n_repeat=effective_seeds,
|
| 857 |
+
out_dir=layout.raw_dir,
|
| 858 |
+
profile=profile,
|
| 859 |
+
gmu=gmu,
|
| 860 |
+
tensor_parallel_size=args.tensor_parallel_size,
|
| 861 |
+
batch_size=args.batch_size,
|
| 862 |
+
hf_cache=args.hf_cache,
|
| 863 |
+
num_proc=args.num_proc,
|
| 864 |
+
debug=args.debug,
|
| 865 |
+
timeout=args.task_timeout,
|
| 866 |
+
)
|
| 867 |
+
per_seed = write_per_seed_results(
|
| 868 |
+
raw_results_file=raw_results_file, layout=layout, task=task, checkpoint=checkpoint
|
| 869 |
+
)
|
| 870 |
+
write_compile_results(layout=layout, per_seed=per_seed, checkpoint=checkpoint)
|
| 871 |
+
|
| 872 |
+
summary = {
|
| 873 |
+
"task": task,
|
| 874 |
+
"status": "ok",
|
| 875 |
+
"seeds": list(seeds),
|
| 876 |
+
"mean": statistics.fmean(per_seed.values()),
|
| 877 |
+
"std": statistics.stdev(per_seed.values()) if len(per_seed) > 1 else 0.0,
|
| 878 |
+
"per_seed": {str(k): v for k, v in per_seed.items()},
|
| 879 |
+
"elapsed_seconds": round(time.time() - started, 1),
|
| 880 |
+
}
|
| 881 |
+
layout.marker.parent.mkdir(parents=True, exist_ok=True)
|
| 882 |
+
layout.marker.write_text(json.dumps({**summary, "checkpoint": str(checkpoint)}, indent=2))
|
| 883 |
+
if args.prune_raw and layout.raw_dir.exists():
|
| 884 |
+
shutil.rmtree(layout.raw_dir)
|
| 885 |
+
logger.info("pruned %s", layout.raw_dir)
|
| 886 |
+
return summary
|
| 887 |
+
|
| 888 |
+
|
| 889 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 890 |
+
parser = argparse.ArgumentParser(
|
| 891 |
+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
| 892 |
+
)
|
| 893 |
+
parser.add_argument("--checkpoint", type=Path, required=True, help="Local HF checkpoint dir (.../hf/step-N)")
|
| 894 |
+
parser.add_argument(
|
| 895 |
+
"--experiment",
|
| 896 |
+
required=True,
|
| 897 |
+
help="Experiment name WITHOUT a -stepN suffix; the step is taken from --checkpoint",
|
| 898 |
+
)
|
| 899 |
+
parser.add_argument("--suite", choices=sorted(SUITES), help="math | science | code")
|
| 900 |
+
parser.add_argument("--tasks", nargs="+", help="Explicit evalchemy task names (overrides --suite)")
|
| 901 |
+
parser.add_argument("--seeds", type=int, help="Override the per-task seed count (default: TPU parity)")
|
| 902 |
+
parser.add_argument("--results-root", type=Path, required=True, help="Local stand-in for gs://.../evaluation/evalchemy")
|
| 903 |
+
parser.add_argument(
|
| 904 |
+
"--evalchemy-dir",
|
| 905 |
+
type=Path,
|
| 906 |
+
default=Path(os.environ.get("EVALCHEMY_DIR", "/opt/marin-gpu-eval/evalchemy")),
|
| 907 |
+
help="Patched evalchemy checkout (default: $EVALCHEMY_DIR)",
|
| 908 |
+
)
|
| 909 |
+
parser.add_argument("--gpu-profile", choices=sorted(GPU_PROFILES), help="Default: auto-detect from nvidia-smi")
|
| 910 |
+
parser.add_argument("--tensor-parallel-size", type=int, help="Default: number of visible GPUs")
|
| 911 |
+
parser.add_argument("--batch-size", type=int, default=64, help="lm-eval batch size (default 64)")
|
| 912 |
+
parser.add_argument(
|
| 913 |
+
"--gpu-memory-utilization",
|
| 914 |
+
type=float,
|
| 915 |
+
help="Override the profile value. Still clamped by free VRAM unless --no-vram-clamp.",
|
| 916 |
+
)
|
| 917 |
+
parser.add_argument(
|
| 918 |
+
"--vram-headroom-mib",
|
| 919 |
+
type=int,
|
| 920 |
+
default=2048,
|
| 921 |
+
help="MiB left unclaimed on the busiest GPU when clamping (default 2048)",
|
| 922 |
+
)
|
| 923 |
+
parser.add_argument("--no-vram-clamp", action="store_true", help="Trust the profile value verbatim (unsafe)")
|
| 924 |
+
parser.add_argument("--hf-cache", type=Path, help="HF_HOME for the eval subprocess")
|
| 925 |
+
parser.add_argument("--num-proc", type=int, default=8, help="Dataset map/filter workers (default 8)")
|
| 926 |
+
parser.add_argument("--task-timeout", type=int, default=None, help="Seconds per task before SIGKILL")
|
| 927 |
+
parser.add_argument("--debug", action="store_true", help="Smoke mode: evalchemy limits each task to 10 examples")
|
| 928 |
+
parser.add_argument(
|
| 929 |
+
"--prune-raw",
|
| 930 |
+
action="store_true",
|
| 931 |
+
help="Delete <eval_dir>/_raw/<TASK> after a successful fan-out. The raw file holds all K "
|
| 932 |
+
"repetitions at full 32768-token outputs and is the largest artifact on disk.",
|
| 933 |
+
)
|
| 934 |
+
parser.add_argument("--force", action="store_true", help="Re-run tasks that already have complete results")
|
| 935 |
+
parser.add_argument("--dry-run", action="store_true", help="Print the plan and exit")
|
| 936 |
+
parser.add_argument("--log-level", default="INFO")
|
| 937 |
+
return parser
|
| 938 |
+
|
| 939 |
+
|
| 940 |
+
def main(argv: list[str] | None = None) -> int:
|
| 941 |
+
args = build_parser().parse_args(argv)
|
| 942 |
+
logging.basicConfig(
|
| 943 |
+
level=getattr(logging, args.log_level.upper()),
|
| 944 |
+
format="%(asctime)s %(levelname)-7s %(name)s | %(message)s",
|
| 945 |
+
)
|
| 946 |
+
|
| 947 |
+
tasks = resolve_tasks(args)
|
| 948 |
+
validate_checkpoint(args.checkpoint.resolve())
|
| 949 |
+
|
| 950 |
+
gpus = query_gpus()
|
| 951 |
+
profile = GPU_PROFILES[args.gpu_profile] if args.gpu_profile else detect_profile(gpus)
|
| 952 |
+
if args.gpu_memory_utilization is not None:
|
| 953 |
+
profile = dataclasses.replace(profile, gpu_memory_utilization=args.gpu_memory_utilization)
|
| 954 |
+
if args.tensor_parallel_size is None:
|
| 955 |
+
args.tensor_parallel_size = len(gpus)
|
| 956 |
+
|
| 957 |
+
gmu = (
|
| 958 |
+
profile.gpu_memory_utilization
|
| 959 |
+
if args.no_vram_clamp
|
| 960 |
+
else resolve_gpu_memory_utilization(profile, gpus, args.vram_headroom_mib)
|
| 961 |
+
)
|
| 962 |
+
|
| 963 |
+
logger.info("checkpoint %s (step %s)", args.checkpoint, step_of(args.checkpoint.resolve()))
|
| 964 |
+
logger.info("gpus %s", [f"{g.index}:{g.name}:{g.free_mib}MiB free" for g in gpus])
|
| 965 |
+
logger.info("profile %s (%s)", profile.name, profile.notes)
|
| 966 |
+
logger.info(
|
| 967 |
+
"engine tp=%d gmu=%.3f max_num_seqs=%d max_model_len=%d",
|
| 968 |
+
args.tensor_parallel_size,
|
| 969 |
+
gmu,
|
| 970 |
+
profile.max_num_seqs,
|
| 971 |
+
MAX_MODEL_LEN,
|
| 972 |
+
)
|
| 973 |
+
per_token = kv_bytes_per_token(args.checkpoint.resolve())
|
| 974 |
+
if per_token:
|
| 975 |
+
seq_gib = per_token * MAX_MODEL_LEN / 1024**3
|
| 976 |
+
logger.info("kv cache %d B/token -> %.2f GiB per full-length sequence", per_token, seq_gib)
|
| 977 |
+
logger.info("tasks %s", {t: (1 if t in SINGLE_PASS_TASKS else n) for t, n in tasks.items()})
|
| 978 |
+
|
| 979 |
+
if args.dry_run:
|
| 980 |
+
return 0
|
| 981 |
+
|
| 982 |
+
summaries: list[dict[str, Any]] = []
|
| 983 |
+
failures: list[tuple[str, str]] = []
|
| 984 |
+
for task, n_seeds in tasks.items():
|
| 985 |
+
try:
|
| 986 |
+
summaries.append(evaluate_task(args, task, n_seeds, profile, gmu))
|
| 987 |
+
except Exception as error: # one bad task must not abandon the rest
|
| 988 |
+
logger.exception("task %s failed", task)
|
| 989 |
+
failures.append((task, str(error)))
|
| 990 |
+
|
| 991 |
+
print(json.dumps({"summaries": summaries, "failures": failures}, indent=2))
|
| 992 |
+
return 1 if failures else 0
|
| 993 |
+
|
| 994 |
+
|
| 995 |
+
if __name__ == "__main__":
|
| 996 |
+
sys.exit(main())
|
gpu-sft/scripts/gpu_eval/gpu_eval_monitor.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Copyright The Marin Authors
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
"""Eval-during-training monitor for a single local GPU box.
|
| 5 |
+
|
| 6 |
+
Local replacement for ``claude/eval_monitor.py``: GCS listing becomes a
|
| 7 |
+
filesystem walk, ``iris job run`` becomes a ``gpu_eval_driver.py`` subprocess,
|
| 8 |
+
and the "is another job already running this?" question becomes an OS file lock.
|
| 9 |
+
|
| 10 |
+
python gpu_eval_monitor.py \\
|
| 11 |
+
--run-dir /data/runs/exp_sft_qwen3_8b_x \\
|
| 12 |
+
--experiment exp_sft_qwen3_8b_x \\
|
| 13 |
+
--suite math \\
|
| 14 |
+
--results-root /data/marin/evaluation/evalchemy \\
|
| 15 |
+
--gpu-lock /var/tmp/marin-gpu.lock
|
| 16 |
+
|
| 17 |
+
HOW GPU ACCESS IS SERIALISED AGAINST TRAINING
|
| 18 |
+
One advisory ``flock(2)`` on --gpu-lock. The lock is taken and released
|
| 19 |
+
around EACH TASK, not around a whole suite, so a training restart never
|
| 20 |
+
waits hours behind a 10-seed AIME sweep.
|
| 21 |
+
|
| 22 |
+
The training launcher MUST take the same lock. Wrap it:
|
| 23 |
+
|
| 24 |
+
flock /var/tmp/marin-gpu.lock -c 'python -m levanter.main.train_lm ...'
|
| 25 |
+
|
| 26 |
+
(util-linux ``flock`` and Python's ``fcntl.flock`` both call flock(2) on the
|
| 27 |
+
file, so they interlock correctly. ``flock`` blocks by default; add ``-w N``
|
| 28 |
+
for a timeout.)
|
| 29 |
+
|
| 30 |
+
The lock alone is not sufficient, because JAX preallocates ~75% of every
|
| 31 |
+
visible GPU at import and holds it until the process exits, and because
|
| 32 |
+
nothing forces a stray notebook or embedding server to cooperate. So the
|
| 33 |
+
monitor ALSO refuses to launch until the busiest visible GPU reports at
|
| 34 |
+
least --min-free-vram-mib free. Both gates must pass.
|
| 35 |
+
|
| 36 |
+
If you have spare cards, the better answer is physical separation: give
|
| 37 |
+
training ``CUDA_VISIBLE_DEVICES=0,1,2,3`` and run this monitor with
|
| 38 |
+
``--cuda-visible-devices 4,5,6,7 --no-gpu-lock``. Then nothing contends and
|
| 39 |
+
evals never stall the run.
|
| 40 |
+
|
| 41 |
+
DEDUPE
|
| 42 |
+
Three layers, matching the TPU monitor:
|
| 43 |
+
1. Artifact probe -- gpu_eval_driver.is_task_complete() checks that every
|
| 44 |
+
expected per-seed results file parses and the compile file has the full
|
| 45 |
+
seed count. This is the source of truth; markers are only a fast path.
|
| 46 |
+
2. In-process set of (step, task) pairs launched this session.
|
| 47 |
+
3. The lock itself, which prevents two monitors from racing one GPU.
|
| 48 |
+
|
| 49 |
+
CHECKPOINT READINESS
|
| 50 |
+
A step is only eligible once its HF export is structurally complete: every
|
| 51 |
+
shard named in model.safetensors.index.json exists, config.json exists, and
|
| 52 |
+
the tokenizer is present. Levanter writes hf/step-N while training
|
| 53 |
+
continues, so a naive directory-mtime check will load a truncated export.
|
| 54 |
+
A --settle-seconds quiet period on the newest file guards the tail.
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
from __future__ import annotations
|
| 58 |
+
|
| 59 |
+
import argparse
|
| 60 |
+
import contextlib
|
| 61 |
+
import errno
|
| 62 |
+
import fcntl
|
| 63 |
+
import json
|
| 64 |
+
import logging
|
| 65 |
+
import os
|
| 66 |
+
import re
|
| 67 |
+
import signal
|
| 68 |
+
import subprocess
|
| 69 |
+
import sys
|
| 70 |
+
import time
|
| 71 |
+
from collections.abc import Iterator
|
| 72 |
+
from datetime import UTC, datetime
|
| 73 |
+
from pathlib import Path
|
| 74 |
+
from typing import Any
|
| 75 |
+
|
| 76 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 77 |
+
|
| 78 |
+
import gpu_eval_driver as driver # noqa: E402
|
| 79 |
+
|
| 80 |
+
logger = logging.getLogger("gpu_eval_monitor")
|
| 81 |
+
|
| 82 |
+
STEP_DIR_RE = re.compile(r"^(?:step-|checkpoint-)(\d+)$")
|
| 83 |
+
DEFAULT_POLL_SECONDS = 300
|
| 84 |
+
DEFAULT_MAX_RUNTIME_DAYS = 14
|
| 85 |
+
|
| 86 |
+
_SHUTDOWN = False
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _handle_signal(signum: int, _frame: Any) -> None:
|
| 90 |
+
global _SHUTDOWN
|
| 91 |
+
logger.warning("received signal %d; finishing the current task then exiting", signum)
|
| 92 |
+
_SHUTDOWN = True
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# --------------------------------------------------------------------------
|
| 96 |
+
# GPU lock
|
| 97 |
+
# --------------------------------------------------------------------------
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@contextlib.contextmanager
|
| 101 |
+
def gpu_lock(path: Path | None, *, poll_seconds: int, wait_seconds: int | None) -> Iterator[bool]:
|
| 102 |
+
"""Exclusive flock(2) on ``path``. Yields True when held, False when disabled."""
|
| 103 |
+
if path is None:
|
| 104 |
+
yield False
|
| 105 |
+
return
|
| 106 |
+
|
| 107 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 108 |
+
deadline = None if wait_seconds is None else time.time() + wait_seconds
|
| 109 |
+
handle = path.open("a+")
|
| 110 |
+
try:
|
| 111 |
+
announced = False
|
| 112 |
+
while True:
|
| 113 |
+
try:
|
| 114 |
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
| 115 |
+
break
|
| 116 |
+
except OSError as error:
|
| 117 |
+
if error.errno not in (errno.EACCES, errno.EAGAIN):
|
| 118 |
+
raise
|
| 119 |
+
if deadline is not None and time.time() >= deadline:
|
| 120 |
+
raise TimeoutError(f"could not acquire {path} within {wait_seconds}s") from None
|
| 121 |
+
if not announced:
|
| 122 |
+
logger.info("waiting for GPU lock %s (held by the training job?)", path)
|
| 123 |
+
announced = True
|
| 124 |
+
if _SHUTDOWN:
|
| 125 |
+
raise TimeoutError("shutdown requested while waiting for the GPU lock") from None
|
| 126 |
+
time.sleep(min(poll_seconds, 15))
|
| 127 |
+
handle.seek(0)
|
| 128 |
+
handle.truncate()
|
| 129 |
+
handle.write(f"{os.getpid()} gpu_eval_monitor {datetime.now(UTC).isoformat()}\n")
|
| 130 |
+
handle.flush()
|
| 131 |
+
logger.info("acquired GPU lock %s", path)
|
| 132 |
+
yield True
|
| 133 |
+
finally:
|
| 134 |
+
with contextlib.suppress(OSError):
|
| 135 |
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
| 136 |
+
handle.close()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def free_vram_mib() -> int:
|
| 140 |
+
"""Free MiB on the busiest visible GPU."""
|
| 141 |
+
return min(gpu.free_mib for gpu in driver.query_gpus())
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# --------------------------------------------------------------------------
|
| 145 |
+
# Checkpoint discovery
|
| 146 |
+
# --------------------------------------------------------------------------
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def discover_steps(run_dir: Path, subdir: str) -> list[tuple[int, Path]]:
|
| 150 |
+
"""Return sorted (step, path) for every checkpoint directory under run_dir/subdir."""
|
| 151 |
+
root = run_dir / subdir if subdir else run_dir
|
| 152 |
+
if not root.is_dir():
|
| 153 |
+
return []
|
| 154 |
+
found: list[tuple[int, Path]] = []
|
| 155 |
+
for child in root.iterdir():
|
| 156 |
+
if not child.is_dir():
|
| 157 |
+
continue
|
| 158 |
+
match = STEP_DIR_RE.match(child.name)
|
| 159 |
+
if match:
|
| 160 |
+
found.append((int(match.group(1)), child))
|
| 161 |
+
return sorted(found)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def checkpoint_is_settled(checkpoint: Path, settle_seconds: int) -> bool:
|
| 165 |
+
"""True when nothing under the checkpoint has been written for settle_seconds."""
|
| 166 |
+
if settle_seconds <= 0:
|
| 167 |
+
return True
|
| 168 |
+
newest = 0.0
|
| 169 |
+
for path in checkpoint.rglob("*"):
|
| 170 |
+
if path.is_file():
|
| 171 |
+
newest = max(newest, path.stat().st_mtime)
|
| 172 |
+
return newest > 0 and (time.time() - newest) >= settle_seconds
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def checkpoint_is_ready(checkpoint: Path, settle_seconds: int) -> tuple[bool, str]:
|
| 176 |
+
try:
|
| 177 |
+
driver.validate_checkpoint(checkpoint)
|
| 178 |
+
except RuntimeError as error:
|
| 179 |
+
return False, str(error)
|
| 180 |
+
if not checkpoint_is_settled(checkpoint, settle_seconds):
|
| 181 |
+
return False, f"still being written (quiet period {settle_seconds}s not met)"
|
| 182 |
+
return True, "ready"
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# --------------------------------------------------------------------------
|
| 186 |
+
# Work items
|
| 187 |
+
# --------------------------------------------------------------------------
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def pending_tasks(
|
| 191 |
+
*,
|
| 192 |
+
experiment: str,
|
| 193 |
+
checkpoint: Path,
|
| 194 |
+
tasks: dict[str, int],
|
| 195 |
+
results_root: Path,
|
| 196 |
+
) -> list[str]:
|
| 197 |
+
"""Tasks for this checkpoint whose artifacts are not already complete."""
|
| 198 |
+
step = driver.step_of(checkpoint)
|
| 199 |
+
model_dir_name = driver.sanitize_model_name(str(checkpoint.resolve()))
|
| 200 |
+
pending = []
|
| 201 |
+
for task, n_seeds in tasks.items():
|
| 202 |
+
effective = 1 if task in driver.SINGLE_PASS_TASKS else n_seeds
|
| 203 |
+
layout = driver.Layout(
|
| 204 |
+
results_root=results_root.resolve(),
|
| 205 |
+
experiment=experiment,
|
| 206 |
+
step=step,
|
| 207 |
+
task=task,
|
| 208 |
+
seeds=tuple(driver.SEED_BASE + i for i in range(effective)),
|
| 209 |
+
model_dir_name=model_dir_name,
|
| 210 |
+
)
|
| 211 |
+
if not driver.is_task_complete(layout):
|
| 212 |
+
pending.append(task)
|
| 213 |
+
return pending
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def run_driver(args: argparse.Namespace, checkpoint: Path, task: str) -> tuple[int, float]:
|
| 217 |
+
"""Run gpu_eval_driver.py for exactly one task in a fresh process.
|
| 218 |
+
|
| 219 |
+
A fresh process per task is deliberate: vLLM does not reliably return all
|
| 220 |
+
device memory to the allocator on engine teardown, so a long-lived driver
|
| 221 |
+
would slowly starve itself (and the training job) across a 5-task suite.
|
| 222 |
+
"""
|
| 223 |
+
command = [
|
| 224 |
+
sys.executable,
|
| 225 |
+
str(Path(__file__).resolve().parent / "gpu_eval_driver.py"),
|
| 226 |
+
"--checkpoint",
|
| 227 |
+
str(checkpoint),
|
| 228 |
+
"--experiment",
|
| 229 |
+
args.experiment,
|
| 230 |
+
"--tasks",
|
| 231 |
+
task,
|
| 232 |
+
"--results-root",
|
| 233 |
+
str(args.results_root),
|
| 234 |
+
"--evalchemy-dir",
|
| 235 |
+
str(args.evalchemy_dir),
|
| 236 |
+
"--num-proc",
|
| 237 |
+
str(args.num_proc),
|
| 238 |
+
]
|
| 239 |
+
if args.gpu_profile:
|
| 240 |
+
command += ["--gpu-profile", args.gpu_profile]
|
| 241 |
+
if args.tensor_parallel_size:
|
| 242 |
+
command += ["--tensor-parallel-size", str(args.tensor_parallel_size)]
|
| 243 |
+
if args.hf_cache:
|
| 244 |
+
command += ["--hf-cache", str(args.hf_cache)]
|
| 245 |
+
if args.task_timeout:
|
| 246 |
+
command += ["--task-timeout", str(args.task_timeout)]
|
| 247 |
+
if args.prune_raw:
|
| 248 |
+
command.append("--prune-raw")
|
| 249 |
+
if args.debug:
|
| 250 |
+
command.append("--debug")
|
| 251 |
+
|
| 252 |
+
env = os.environ.copy()
|
| 253 |
+
if args.cuda_visible_devices is not None:
|
| 254 |
+
env["CUDA_VISIBLE_DEVICES"] = args.cuda_visible_devices
|
| 255 |
+
|
| 256 |
+
logger.info("launching driver: %s %s", task, checkpoint)
|
| 257 |
+
started = time.time()
|
| 258 |
+
process = subprocess.run(command, env=env, check=False)
|
| 259 |
+
return process.returncode, time.time() - started
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
# --------------------------------------------------------------------------
|
| 263 |
+
# Event log
|
| 264 |
+
# --------------------------------------------------------------------------
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class EventLog:
|
| 268 |
+
def __init__(self, path: Path | None) -> None:
|
| 269 |
+
self.path = path
|
| 270 |
+
if path is not None:
|
| 271 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 272 |
+
|
| 273 |
+
def emit(self, event: str, **fields: Any) -> None:
|
| 274 |
+
record = {"ts": datetime.now(UTC).isoformat(), "event": event, **fields}
|
| 275 |
+
logger.info("%s %s", event, {k: v for k, v in fields.items() if k != "ts"})
|
| 276 |
+
if self.path is not None:
|
| 277 |
+
with self.path.open("a") as handle:
|
| 278 |
+
handle.write(json.dumps(record, default=str) + "\n")
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
# --------------------------------------------------------------------------
|
| 282 |
+
# Main loop
|
| 283 |
+
# --------------------------------------------------------------------------
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def select_steps(steps: list[tuple[int, Path]], args: argparse.Namespace) -> list[tuple[int, Path]]:
|
| 287 |
+
selected = [
|
| 288 |
+
(step, path)
|
| 289 |
+
for step, path in steps
|
| 290 |
+
if step >= args.min_step
|
| 291 |
+
and (args.max_step is None or step <= args.max_step)
|
| 292 |
+
and (args.step_stride <= 1 or step % args.step_stride == 0)
|
| 293 |
+
]
|
| 294 |
+
if args.newest_first:
|
| 295 |
+
selected.reverse()
|
| 296 |
+
if args.latest_only and selected:
|
| 297 |
+
selected = selected[:1] if args.newest_first else selected[-1:]
|
| 298 |
+
return selected
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def monitor(args: argparse.Namespace) -> int:
|
| 302 |
+
events = EventLog(args.event_log)
|
| 303 |
+
tasks = driver.SUITES[args.suite] if args.suite else {t: driver.ALL_TASK_SEEDS[t] for t in args.tasks}
|
| 304 |
+
if args.seeds is not None:
|
| 305 |
+
tasks = {task: args.seeds for task in tasks}
|
| 306 |
+
|
| 307 |
+
deadline = time.time() + args.max_runtime_days * 24 * 3600
|
| 308 |
+
launched: set[tuple[int, str]] = set()
|
| 309 |
+
|
| 310 |
+
events.emit(
|
| 311 |
+
"start",
|
| 312 |
+
run_dir=str(args.run_dir),
|
| 313 |
+
experiment=args.experiment,
|
| 314 |
+
tasks=list(tasks),
|
| 315 |
+
poll_seconds=args.poll,
|
| 316 |
+
gpu_lock=str(args.gpu_lock) if args.gpu_lock else None,
|
| 317 |
+
min_free_vram_mib=args.min_free_vram_mib,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
while not _SHUTDOWN and time.time() < deadline:
|
| 321 |
+
steps = select_steps(discover_steps(args.run_dir, args.checkpoint_subdir), args)
|
| 322 |
+
if not steps:
|
| 323 |
+
events.emit("no_checkpoints", root=str(args.run_dir / args.checkpoint_subdir))
|
| 324 |
+
|
| 325 |
+
for step, checkpoint in steps:
|
| 326 |
+
if _SHUTDOWN:
|
| 327 |
+
break
|
| 328 |
+
|
| 329 |
+
ready, reason = checkpoint_is_ready(checkpoint, args.settle_seconds)
|
| 330 |
+
if not ready:
|
| 331 |
+
events.emit("checkpoint_not_ready", step=step, reason=reason)
|
| 332 |
+
continue
|
| 333 |
+
|
| 334 |
+
todo = pending_tasks(
|
| 335 |
+
experiment=args.experiment,
|
| 336 |
+
checkpoint=checkpoint,
|
| 337 |
+
tasks=tasks,
|
| 338 |
+
results_root=args.results_root,
|
| 339 |
+
)
|
| 340 |
+
todo = [t for t in todo if (step, t) not in launched or args.retry_failed]
|
| 341 |
+
if not todo:
|
| 342 |
+
continue
|
| 343 |
+
|
| 344 |
+
events.emit("step_pending", step=step, tasks=todo)
|
| 345 |
+
|
| 346 |
+
for task in todo:
|
| 347 |
+
if _SHUTDOWN:
|
| 348 |
+
break
|
| 349 |
+
|
| 350 |
+
free = free_vram_mib()
|
| 351 |
+
if free < args.min_free_vram_mib:
|
| 352 |
+
events.emit(
|
| 353 |
+
"vram_blocked",
|
| 354 |
+
step=step,
|
| 355 |
+
task=task,
|
| 356 |
+
free_mib=free,
|
| 357 |
+
required_mib=args.min_free_vram_mib,
|
| 358 |
+
hint="training or another process still holds the card",
|
| 359 |
+
)
|
| 360 |
+
break # nothing on this box will run; go back to sleep
|
| 361 |
+
|
| 362 |
+
try:
|
| 363 |
+
with gpu_lock(args.gpu_lock, poll_seconds=args.poll, wait_seconds=args.lock_wait_seconds):
|
| 364 |
+
# Re-check under the lock: free VRAM can change while we
|
| 365 |
+
# queued behind the training job, and another monitor may
|
| 366 |
+
# have finished this task in the meantime.
|
| 367 |
+
free = free_vram_mib()
|
| 368 |
+
if free < args.min_free_vram_mib:
|
| 369 |
+
events.emit("vram_blocked_after_lock", step=step, task=task, free_mib=free)
|
| 370 |
+
break
|
| 371 |
+
if task not in pending_tasks(
|
| 372 |
+
experiment=args.experiment,
|
| 373 |
+
checkpoint=checkpoint,
|
| 374 |
+
tasks={task: tasks[task]},
|
| 375 |
+
results_root=args.results_root,
|
| 376 |
+
):
|
| 377 |
+
events.emit("raced", step=step, task=task)
|
| 378 |
+
continue
|
| 379 |
+
|
| 380 |
+
launched.add((step, task))
|
| 381 |
+
returncode, elapsed = run_driver(args, checkpoint, task)
|
| 382 |
+
except TimeoutError as error:
|
| 383 |
+
events.emit("lock_timeout", step=step, task=task, detail=str(error))
|
| 384 |
+
break
|
| 385 |
+
|
| 386 |
+
if returncode == 0:
|
| 387 |
+
events.emit("task_done", step=step, task=task, elapsed_seconds=round(elapsed, 1))
|
| 388 |
+
else:
|
| 389 |
+
events.emit(
|
| 390 |
+
"task_failed",
|
| 391 |
+
step=step,
|
| 392 |
+
task=task,
|
| 393 |
+
returncode=returncode,
|
| 394 |
+
elapsed_seconds=round(elapsed, 1),
|
| 395 |
+
hint=f"see {args.results_root}/{args.experiment}-step{step}/_raw/{task}.evalchemy.log",
|
| 396 |
+
)
|
| 397 |
+
if args.stop_on_failure:
|
| 398 |
+
events.emit("stop_on_failure", step=step, task=task)
|
| 399 |
+
return 1
|
| 400 |
+
|
| 401 |
+
if args.once:
|
| 402 |
+
break
|
| 403 |
+
events.emit("sleep", seconds=args.poll)
|
| 404 |
+
slept = 0
|
| 405 |
+
while slept < args.poll and not _SHUTDOWN:
|
| 406 |
+
time.sleep(min(5, args.poll - slept))
|
| 407 |
+
slept += 5
|
| 408 |
+
|
| 409 |
+
events.emit("exit", shutdown=_SHUTDOWN, expired=time.time() >= deadline)
|
| 410 |
+
return 0
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 414 |
+
parser = argparse.ArgumentParser(
|
| 415 |
+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
| 416 |
+
)
|
| 417 |
+
parser.add_argument("--run-dir", type=Path, required=True, help="Training output dir containing hf/step-N")
|
| 418 |
+
parser.add_argument("--checkpoint-subdir", default="hf", help="Subdir holding checkpoints (default: hf)")
|
| 419 |
+
parser.add_argument("--experiment", required=True, help="Experiment name WITHOUT a -stepN suffix")
|
| 420 |
+
parser.add_argument("--results-root", type=Path, required=True)
|
| 421 |
+
parser.add_argument("--suite", choices=sorted(driver.SUITES))
|
| 422 |
+
parser.add_argument("--tasks", nargs="+", help="Explicit task names (overrides --suite)")
|
| 423 |
+
parser.add_argument("--seeds", type=int, help="Override per-task seed count")
|
| 424 |
+
parser.add_argument(
|
| 425 |
+
"--evalchemy-dir",
|
| 426 |
+
type=Path,
|
| 427 |
+
default=Path(os.environ.get("EVALCHEMY_DIR", "/opt/marin-gpu-eval/evalchemy")),
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
parser.add_argument("--gpu-lock", type=Path, default=Path("/var/tmp/marin-gpu.lock"))
|
| 431 |
+
parser.add_argument("--no-gpu-lock", action="store_true", help="Skip locking (use with disjoint --cuda-visible-devices)")
|
| 432 |
+
parser.add_argument("--lock-wait-seconds", type=int, default=None, help="Give up waiting for the lock (default: wait forever)")
|
| 433 |
+
parser.add_argument(
|
| 434 |
+
"--min-free-vram-mib",
|
| 435 |
+
type=int,
|
| 436 |
+
default=70_000,
|
| 437 |
+
help="Refuse to launch below this much free VRAM on the busiest visible GPU. "
|
| 438 |
+
"70000 suits an 8B bf16 model at 36864 ctx on an 80GB card.",
|
| 439 |
+
)
|
| 440 |
+
parser.add_argument("--cuda-visible-devices", help="CUDA_VISIBLE_DEVICES for eval subprocesses")
|
| 441 |
+
parser.add_argument("--gpu-profile", choices=sorted(driver.GPU_PROFILES))
|
| 442 |
+
parser.add_argument("--tensor-parallel-size", type=int)
|
| 443 |
+
|
| 444 |
+
parser.add_argument("--poll", type=int, default=DEFAULT_POLL_SECONDS, help="Seconds between passes")
|
| 445 |
+
parser.add_argument("--settle-seconds", type=int, default=120, help="Quiet period a checkpoint must show")
|
| 446 |
+
parser.add_argument("--step-stride", type=int, default=1, help="Only eval steps divisible by this")
|
| 447 |
+
parser.add_argument("--min-step", type=int, default=0)
|
| 448 |
+
parser.add_argument("--max-step", type=int, default=None)
|
| 449 |
+
parser.add_argument("--latest-only", action="store_true", help="Only ever evaluate the newest eligible step")
|
| 450 |
+
parser.add_argument("--newest-first", action="store_true", help="Walk steps newest-first")
|
| 451 |
+
parser.add_argument("--retry-failed", action="store_true", help="Re-attempt tasks that failed earlier this session")
|
| 452 |
+
parser.add_argument("--stop-on-failure", action="store_true")
|
| 453 |
+
parser.add_argument("--once", action="store_true", help="Single pass, then exit")
|
| 454 |
+
parser.add_argument("--max-runtime-days", type=float, default=DEFAULT_MAX_RUNTIME_DAYS)
|
| 455 |
+
parser.add_argument("--task-timeout", type=int, default=None, help="Seconds per task before the driver is killed")
|
| 456 |
+
parser.add_argument("--num-proc", type=int, default=8)
|
| 457 |
+
parser.add_argument("--hf-cache", type=Path)
|
| 458 |
+
parser.add_argument("--prune-raw", action="store_true")
|
| 459 |
+
parser.add_argument("--debug", action="store_true", help="Smoke mode (10 examples per task)")
|
| 460 |
+
parser.add_argument("--event-log", type=Path, help="Append JSONL events here")
|
| 461 |
+
parser.add_argument("--log-level", default="INFO")
|
| 462 |
+
return parser
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def main(argv: list[str] | None = None) -> int:
|
| 466 |
+
args = build_parser().parse_args(argv)
|
| 467 |
+
logging.basicConfig(
|
| 468 |
+
level=getattr(logging, args.log_level.upper()),
|
| 469 |
+
format="%(asctime)s %(levelname)-7s %(name)s | %(message)s",
|
| 470 |
+
)
|
| 471 |
+
if not args.suite and not args.tasks:
|
| 472 |
+
raise SystemExit("pass --suite or --tasks")
|
| 473 |
+
if args.tasks:
|
| 474 |
+
unknown = [t for t in args.tasks if t not in driver.ALL_TASK_SEEDS]
|
| 475 |
+
if unknown:
|
| 476 |
+
raise SystemExit(f"unknown tasks: {unknown}")
|
| 477 |
+
if args.no_gpu_lock:
|
| 478 |
+
args.gpu_lock = None
|
| 479 |
+
|
| 480 |
+
signal.signal(signal.SIGINT, _handle_signal)
|
| 481 |
+
signal.signal(signal.SIGTERM, _handle_signal)
|
| 482 |
+
return monitor(args)
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
if __name__ == "__main__":
|
| 486 |
+
sys.exit(main())
|
gpu-sft/scripts/gpu_eval/patch_evalchemy_gpu.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Copyright The Marin Authors
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
"""Patch a cloned evalchemy checkout for single-GPU offline vLLM evaluation.
|
| 5 |
+
|
| 6 |
+
This is the GPU analogue of the runtime patching that
|
| 7 |
+
``lib/marin/src/marin/evaluation/evaluators/evalchemy_evaluator.py`` does on TPU.
|
| 8 |
+
The TPU-only patches (per-request seed stripping, fake ``vllm-tpu`` version
|
| 9 |
+
metadata, ``MODEL_IMPL_TYPE=vllm``, GCS ``AutoConfig`` redirection) are dropped;
|
| 10 |
+
two GPU-specific ones are added.
|
| 11 |
+
|
| 12 |
+
Patches applied (all idempotent, all fail loudly if their marker is missing so a
|
| 13 |
+
pin bump cannot silently no-op):
|
| 14 |
+
|
| 15 |
+
1. ``eval/eval_tracker.py`` -- lm-eval ``eval_logger`` import shim.
|
| 16 |
+
2. ``eval/eval.py`` -- ``utils.eval_logger`` attribute shim.
|
| 17 |
+
3. ``*/eval_instruct.py`` -- ``self.n_repeat`` becomes env-driven
|
| 18 |
+
(``EVALCHEMY_N_REPEAT``) so one vLLM engine can produce all repetitions.
|
| 19 |
+
4. ``*/livecodebench_utils.py`` -- fork -> spawn for the sandboxed test runner.
|
| 20 |
+
REQUIRED on GPU: CUDA context and vLLM/filelock state are not fork-safe and a
|
| 21 |
+
forked child either deadlocks or corrupts the parent's CUDA context.
|
| 22 |
+
5. ``LiveCodeBench*/eval_instruct.py`` -- dataset ``num_proc`` becomes env-driven
|
| 23 |
+
(``EVALCHEMY_NUM_PROC``) so LCB's dataset filter does not oversubscribe the
|
| 24 |
+
box while a training job is running.
|
| 25 |
+
6. Writes ``_marin_gpu_entry.py`` -- the launcher the driver invokes. It applies
|
| 26 |
+
the ``vllm.utils.get_open_port`` compatibility shim and lifts Python's
|
| 27 |
+
int->str digit cap before delegating to ``eval.eval``.
|
| 28 |
+
|
| 29 |
+
Usage:
|
| 30 |
+
python patch_evalchemy_gpu.py /opt/evalchemy
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import argparse
|
| 36 |
+
import logging
|
| 37 |
+
import re
|
| 38 |
+
import sys
|
| 39 |
+
from pathlib import Path
|
| 40 |
+
|
| 41 |
+
logger = logging.getLogger("patch_evalchemy_gpu")
|
| 42 |
+
|
| 43 |
+
# Every benchmark that carries a ``self.n_repeat`` assignment at the pinned
|
| 44 |
+
# commits. Mirrors N_REPEAT_BENCHMARK_PATHS in Marin's TPU evaluator.
|
| 45 |
+
N_REPEAT_BENCHMARKS = (
|
| 46 |
+
"AIME24",
|
| 47 |
+
"AIME25",
|
| 48 |
+
"AIME26",
|
| 49 |
+
"AMC23",
|
| 50 |
+
"HMMT",
|
| 51 |
+
"GPQADiamond",
|
| 52 |
+
"JEEBench",
|
| 53 |
+
"HLE",
|
| 54 |
+
"LiveCodeBench",
|
| 55 |
+
"LiveCodeBenchv5_official",
|
| 56 |
+
"LiveCodeBenchv6_official",
|
| 57 |
+
"CodeForces",
|
| 58 |
+
"CodeElo",
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Benchmarks with no ``n_repeat`` at all -- single pass by construction.
|
| 62 |
+
SINGLE_PASS_BENCHMARKS = ("MATH500", "OlympiadBench", "OlympiadBench_Physics")
|
| 63 |
+
|
| 64 |
+
LCB_BENCHMARKS = ("LiveCodeBench", "LiveCodeBenchv5_official", "LiveCodeBenchv6_official")
|
| 65 |
+
|
| 66 |
+
LCB_UTILITY_FILES = (
|
| 67 |
+
"eval/chat_benchmarks/LiveCodeBench/livecodebench_utils.py",
|
| 68 |
+
"eval/chat_benchmarks/LiveCodeBenchv5/livecodebench_utils.py",
|
| 69 |
+
"eval/chat_benchmarks/LiveCodeBenchv5_official/livecodebench_utils.py",
|
| 70 |
+
"eval/chat_benchmarks/LiveCodeBenchv6_official/livecodebench_utils.py",
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
OLD_TRACKER_IMPORT = (
|
| 74 |
+
"from lm_eval.utils import eval_logger, handle_non_serializable, hash_string, simple_parse_args_string"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
NEW_TRACKER_IMPORT = '''try:
|
| 78 |
+
from lm_eval.utils import eval_logger, handle_non_serializable, hash_string, simple_parse_args_string
|
| 79 |
+
except ImportError:
|
| 80 |
+
try:
|
| 81 |
+
from lm_eval.logging_utils import eval_logger
|
| 82 |
+
from lm_eval.utils import handle_non_serializable, hash_string, simple_parse_args_string
|
| 83 |
+
except ImportError:
|
| 84 |
+
import logging
|
| 85 |
+
eval_logger = logging.getLogger("lm-eval")
|
| 86 |
+
from lm_eval.utils import handle_non_serializable, hash_string, simple_parse_args_string'''
|
| 87 |
+
|
| 88 |
+
EVAL_PY_MARKER = "from lm_eval.tasks import TaskManager as PretrainTaskManager"
|
| 89 |
+
|
| 90 |
+
EVAL_PY_LOGGER_PATCH = '''
|
| 91 |
+
# Patch utils.eval_logger for lm-eval compatibility (marin gpu port)
|
| 92 |
+
if not hasattr(utils, "eval_logger"):
|
| 93 |
+
try:
|
| 94 |
+
from lm_eval.logging_utils import eval_logger as _eval_logger
|
| 95 |
+
utils.eval_logger = _eval_logger
|
| 96 |
+
except ImportError:
|
| 97 |
+
import logging as _logging
|
| 98 |
+
utils.eval_logger = _logging.getLogger("lm-eval")
|
| 99 |
+
'''
|
| 100 |
+
|
| 101 |
+
OLD_LCB_FORK = """ manager = multiprocessing.Manager()
|
| 102 |
+
result = manager.list()
|
| 103 |
+
p = multiprocessing.Process(target=run_tests_for_one_example, args=(test_cases, completion, result, is_extracted))"""
|
| 104 |
+
|
| 105 |
+
NEW_LCB_SPAWN = """ # marin gpu port: CUDA/vLLM/filelock state is not safe to inherit through
|
| 106 |
+
# fork. A forked grader child either deadlocks on the CUDA driver mutex or
|
| 107 |
+
# corrupts the parent engine's context. Spawn is the only safe context here.
|
| 108 |
+
multiprocessing_context = multiprocessing.get_context("spawn")
|
| 109 |
+
manager = multiprocessing_context.Manager()
|
| 110 |
+
result = manager.list()
|
| 111 |
+
p = multiprocessing_context.Process(
|
| 112 |
+
target=run_tests_for_one_example,
|
| 113 |
+
args=(test_cases, completion, result, is_extracted),
|
| 114 |
+
)"""
|
| 115 |
+
|
| 116 |
+
ENTRY_MODULE = '''#!/usr/bin/env python3
|
| 117 |
+
"""Launcher for evalchemy under a local CUDA vLLM install (marin gpu port).
|
| 118 |
+
|
| 119 |
+
Generated by patch_evalchemy_gpu.py -- do not edit by hand.
|
| 120 |
+
|
| 121 |
+
Applies two process-wide fixes that must happen before ``eval.eval`` imports
|
| 122 |
+
lm-eval, then delegates. Invoked as::
|
| 123 |
+
|
| 124 |
+
python _marin_gpu_entry.py --model vllm --tasks AIME24 ...
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
import runpy
|
| 128 |
+
import socket
|
| 129 |
+
import sys
|
| 130 |
+
|
| 131 |
+
# HMMT and some OlympiadBench answers are enormous integers (e.g. (26!)^3).
|
| 132 |
+
# json.dumps calls str() on them; without this the results file is never written
|
| 133 |
+
# and the eval exits 0 with no output.
|
| 134 |
+
sys.set_int_max_str_digits(0)
|
| 135 |
+
|
| 136 |
+
# lm-eval's vllm_causallms imports get_open_port from vllm.utils, which vLLM
|
| 137 |
+
# moved/removed in newer releases. Re-provide it before lm-eval is imported.
|
| 138 |
+
try:
|
| 139 |
+
import vllm.utils as _vllm_utils
|
| 140 |
+
except ImportError:
|
| 141 |
+
_vllm_utils = None
|
| 142 |
+
|
| 143 |
+
if _vllm_utils is not None and not hasattr(_vllm_utils, "get_open_port"):
|
| 144 |
+
def _get_open_port() -> int:
|
| 145 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
| 146 |
+
sock.bind(("", 0))
|
| 147 |
+
return sock.getsockname()[1]
|
| 148 |
+
|
| 149 |
+
_vllm_utils.get_open_port = _get_open_port
|
| 150 |
+
|
| 151 |
+
sys.argv = ["eval.eval", *sys.argv[1:]]
|
| 152 |
+
runpy.run_module("eval.eval", run_name="__main__", alter_sys=True)
|
| 153 |
+
'''
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def _patch_eval_tracker(root: Path) -> None:
|
| 157 |
+
path = root / "eval" / "eval_tracker.py"
|
| 158 |
+
source = path.read_text()
|
| 159 |
+
if "from lm_eval.logging_utils import eval_logger" in source:
|
| 160 |
+
logger.info("eval_tracker.py already patched")
|
| 161 |
+
return
|
| 162 |
+
if OLD_TRACKER_IMPORT not in source:
|
| 163 |
+
raise RuntimeError(f"marker missing in {path}: lm-eval import shim cannot be applied")
|
| 164 |
+
path.write_text(source.replace(OLD_TRACKER_IMPORT, NEW_TRACKER_IMPORT, 1))
|
| 165 |
+
logger.info("patched %s", path)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _patch_eval_py(root: Path) -> None:
|
| 169 |
+
path = root / "eval" / "eval.py"
|
| 170 |
+
source = path.read_text()
|
| 171 |
+
if "# Patch utils.eval_logger" in source:
|
| 172 |
+
logger.info("eval.py already patched")
|
| 173 |
+
return
|
| 174 |
+
if EVAL_PY_MARKER not in source:
|
| 175 |
+
raise RuntimeError(f"marker missing in {path}: eval_logger shim cannot be applied")
|
| 176 |
+
path.write_text(source.replace(EVAL_PY_MARKER, EVAL_PY_MARKER + EVAL_PY_LOGGER_PATCH, 1))
|
| 177 |
+
logger.info("patched %s", path)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _patch_n_repeat(root: Path) -> list[str]:
|
| 181 |
+
patched: list[str] = []
|
| 182 |
+
for name in N_REPEAT_BENCHMARKS:
|
| 183 |
+
path = root / "eval" / "chat_benchmarks" / name / "eval_instruct.py"
|
| 184 |
+
if not path.exists():
|
| 185 |
+
logger.warning("benchmark %s not present at this pin; skipping n_repeat patch", name)
|
| 186 |
+
continue
|
| 187 |
+
source = path.read_text()
|
| 188 |
+
if "EVALCHEMY_N_REPEAT" in source:
|
| 189 |
+
patched.append(name)
|
| 190 |
+
continue
|
| 191 |
+
# __import__ rather than a bare ``os`` reference: several benchmark
|
| 192 |
+
# modules do not import os, and inserting a top-level import is fragile
|
| 193 |
+
# across pin bumps (it would shadow a module docstring).
|
| 194 |
+
new_source, count = re.subn(
|
| 195 |
+
r"self\.n_repeat\s*=\s*\d+",
|
| 196 |
+
'self.n_repeat = int(__import__("os").environ.get("EVALCHEMY_N_REPEAT", "1"))',
|
| 197 |
+
source,
|
| 198 |
+
count=1,
|
| 199 |
+
)
|
| 200 |
+
if count != 1:
|
| 201 |
+
raise RuntimeError(f"no 'self.n_repeat = <int>' assignment found in {path}")
|
| 202 |
+
path.write_text(new_source)
|
| 203 |
+
patched.append(name)
|
| 204 |
+
logger.info("patched n_repeat in %s", path)
|
| 205 |
+
return patched
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _patch_lcb_num_proc(root: Path) -> None:
|
| 209 |
+
for name in LCB_BENCHMARKS:
|
| 210 |
+
path = root / "eval" / "chat_benchmarks" / name / "eval_instruct.py"
|
| 211 |
+
if not path.exists():
|
| 212 |
+
continue
|
| 213 |
+
source = path.read_text()
|
| 214 |
+
if "EVALCHEMY_NUM_PROC" in source:
|
| 215 |
+
continue
|
| 216 |
+
if "num_proc=cpu_count" not in source:
|
| 217 |
+
raise RuntimeError(f"'num_proc=cpu_count' not found in {path}")
|
| 218 |
+
source = source.replace(
|
| 219 |
+
"num_proc=cpu_count",
|
| 220 |
+
'num_proc=int(__import__("os").environ.get("EVALCHEMY_NUM_PROC", str(cpu_count)))',
|
| 221 |
+
1,
|
| 222 |
+
)
|
| 223 |
+
path.write_text(source)
|
| 224 |
+
logger.info("patched num_proc in %s", path)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _patch_lcb_spawn(root: Path) -> None:
|
| 228 |
+
for relative in LCB_UTILITY_FILES:
|
| 229 |
+
path = root / relative
|
| 230 |
+
if not path.exists():
|
| 231 |
+
logger.warning("%s not present at this pin; skipping spawn patch", relative)
|
| 232 |
+
continue
|
| 233 |
+
source = path.read_text()
|
| 234 |
+
if "multiprocessing_context = multiprocessing.get_context" in source:
|
| 235 |
+
continue
|
| 236 |
+
if OLD_LCB_FORK not in source:
|
| 237 |
+
raise RuntimeError(f"LiveCodeBench fork block not found in {path}")
|
| 238 |
+
path.write_text(source.replace(OLD_LCB_FORK, NEW_LCB_SPAWN, 1))
|
| 239 |
+
logger.info("patched fork->spawn in %s", path)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _write_entry(root: Path) -> Path:
|
| 243 |
+
path = root / "_marin_gpu_entry.py"
|
| 244 |
+
path.write_text(ENTRY_MODULE)
|
| 245 |
+
path.chmod(0o755)
|
| 246 |
+
logger.info("wrote %s", path)
|
| 247 |
+
return path
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def patch(root: Path) -> None:
|
| 251 |
+
if not (root / "eval" / "eval.py").exists():
|
| 252 |
+
raise RuntimeError(f"{root} does not look like an evalchemy checkout (no eval/eval.py)")
|
| 253 |
+
_patch_eval_tracker(root)
|
| 254 |
+
_patch_eval_py(root)
|
| 255 |
+
patched = _patch_n_repeat(root)
|
| 256 |
+
_patch_lcb_num_proc(root)
|
| 257 |
+
_patch_lcb_spawn(root)
|
| 258 |
+
_write_entry(root)
|
| 259 |
+
logger.info("n_repeat is env-driven for: %s", ", ".join(patched))
|
| 260 |
+
logger.info("single-pass benchmarks (no n_repeat): %s", ", ".join(SINGLE_PASS_BENCHMARKS))
|
| 261 |
+
logger.info("evalchemy at %s is ready for single-GPU vLLM evaluation", root)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def main() -> int:
|
| 265 |
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
| 266 |
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 267 |
+
parser.add_argument("evalchemy_dir", type=Path, help="Path to the cloned evalchemy checkout")
|
| 268 |
+
args = parser.parse_args()
|
| 269 |
+
patch(args.evalchemy_dir.resolve())
|
| 270 |
+
return 0
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
if __name__ == "__main__":
|
| 274 |
+
sys.exit(main())
|
gpu-sft/scripts/gpu_eval/run_all_evals.sh
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Run the Qwen3-8B baseline + the completed SFT checkpoints through evalchemy, sequentially,
|
| 3 |
+
# writing results to HDFS so a machine kill doesn't lose completed evals.
|
| 4 |
+
#
|
| 5 |
+
# source /opt/tiger/lmc_muon/gpu-eval/activate.sh # (this script also sources it)
|
| 6 |
+
# nohup bash gpu-sft/scripts/gpu_eval/run_all_evals.sh \
|
| 7 |
+
# > /mnt/hdfs/fangzhao_writable/marin-eval/eval_all.log 2>&1 &
|
| 8 |
+
#
|
| 9 |
+
# Each eval uses the full node (tp defaults to #GPUs). Jobs run one at a time. Results go to
|
| 10 |
+
# $RESULTS on HDFS; a completed task's JSON is durable immediately, so a kill only costs the
|
| 11 |
+
# in-flight task (re-run the script — the driver skips finished tasks unless --force).
|
| 12 |
+
source /opt/tiger/lmc_muon/gpu-eval/activate.sh 2>/dev/null || true
|
| 13 |
+
set -uo pipefail
|
| 14 |
+
|
| 15 |
+
DRIVER=/opt/tiger/lmc_muon/self-verified-distillation/gpu-sft/scripts/gpu_eval/gpu_eval_driver.py
|
| 16 |
+
RESULTS="${RESULTS:-/mnt/hdfs/fangzhao_writable/marin-eval/results}"
|
| 17 |
+
HDFS_SFT=/mnt/hdfs/fangzhao_writable/marin-sft
|
| 18 |
+
MODELS=/opt/tiger/lmc_muon/models
|
| 19 |
+
PROFILE="${PROFILE:-h100-80g}"
|
| 20 |
+
mkdir -p "$RESULTS" "$MODELS"
|
| 21 |
+
|
| 22 |
+
run() { # run <checkpoint_dir> <experiment> <suite>
|
| 23 |
+
echo "=========== eval $2 / $3 $(date -u) ==========="
|
| 24 |
+
python "$DRIVER" --checkpoint "$1" --experiment "$2" --suite "$3" \
|
| 25 |
+
--gpu-profile "$PROFILE" --results-root "$RESULTS" \
|
| 26 |
+
|| echo "!! $2/$3 FAILED (continuing to next)"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
# Stage the completed checkpoints from HDFS -> local, keeping a 'step-1999' leaf so the driver
|
| 30 |
+
# parses the step number (16 GB each, ~30 s at 574 MB/s).
|
| 31 |
+
for e in coding_depth4v3_nofilter hs_competition_nofilter; do
|
| 32 |
+
DST="$MODELS/$e/step-1999"
|
| 33 |
+
if [ ! -f "$DST/config.json" ]; then
|
| 34 |
+
echo "staging $e/hf/step-1999 from HDFS -> $DST"
|
| 35 |
+
mkdir -p "$MODELS/$e" && cp -r "$HDFS_SFT/$e/hf/step-1999" "$DST"
|
| 36 |
+
fi
|
| 37 |
+
done
|
| 38 |
+
|
| 39 |
+
if [ "${PARALLEL:-0}" = "1" ]; then
|
| 40 |
+
LOGDIR="$RESULTS/_logs"; mkdir -p "$LOGDIR"
|
| 41 |
+
echo "PARALLEL mode: 4 jobs x tp=2 across GPU pairs (per-job logs in $LOGDIR)"
|
| 42 |
+
CUDA_VISIBLE_DEVICES=0,1 python "$DRIVER" --checkpoint "$MODELS/Qwen3-8B" --experiment qwen3_8b_base --suite math --tensor-parallel-size 2 --gpu-profile "$PROFILE" --results-root "$RESULTS" > "$LOGDIR/base_math.log" 2>&1 &
|
| 43 |
+
CUDA_VISIBLE_DEVICES=2,3 python "$DRIVER" --checkpoint "$MODELS/Qwen3-8B" --experiment qwen3_8b_base --suite code --tensor-parallel-size 2 --gpu-profile "$PROFILE" --results-root "$RESULTS" > "$LOGDIR/base_code.log" 2>&1 &
|
| 44 |
+
CUDA_VISIBLE_DEVICES=4,5 python "$DRIVER" --checkpoint "$MODELS/hs_competition_nofilter/step-1999" --experiment hs_competition_nofilter --suite math --tensor-parallel-size 2 --gpu-profile "$PROFILE" --results-root "$RESULTS" > "$LOGDIR/hs_math.log" 2>&1 &
|
| 45 |
+
CUDA_VISIBLE_DEVICES=6,7 python "$DRIVER" --checkpoint "$MODELS/coding_depth4v3_nofilter/step-1999" --experiment coding_depth4v3_nofilter --suite code --tensor-parallel-size 2 --gpu-profile "$PROFILE" --results-root "$RESULTS" > "$LOGDIR/coding_code.log" 2>&1 &
|
| 46 |
+
wait
|
| 47 |
+
echo "=========== ALL PARALLEL EVALS DONE $(date -u) ==========="
|
| 48 |
+
echo "results -> $RESULTS ; per-job logs -> $LOGDIR"
|
| 49 |
+
exit 0
|
| 50 |
+
fi
|
| 51 |
+
|
| 52 |
+
# 1) baseline (the denominator for every (+delta)) on both suites
|
| 53 |
+
run "$MODELS/Qwen3-8B" qwen3_8b_base math
|
| 54 |
+
run "$MODELS/Qwen3-8B" qwen3_8b_base code
|
| 55 |
+
# 2) the two completed checkpoints on their matching suites
|
| 56 |
+
run "$MODELS/hs_competition_nofilter/step-1999" hs_competition_nofilter math
|
| 57 |
+
run "$MODELS/coding_depth4v3_nofilter/step-1999" coding_depth4v3_nofilter code
|
| 58 |
+
|
| 59 |
+
echo "=========== ALL EVALS DONE $(date -u) ==========="
|
| 60 |
+
echo "results -> $RESULTS"
|
gpu-sft/scripts/gpu_sft/accelerate_fsdp_qwen3.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Optional: run train_sft_qwen3.py under `accelerate launch` instead of `torchrun`.
|
| 2 |
+
# The script builds its own FSDP wrapper, so accelerate is used ONLY as a launcher here
|
| 3 |
+
# (it sets RANK/LOCAL_RANK/WORLD_SIZE). These fsdp_config values document the exact
|
| 4 |
+
# equivalent settings for anyone who prefers accelerate's Accelerator.prepare().
|
| 5 |
+
#
|
| 6 |
+
# accelerate launch --config_file accelerate_fsdp_qwen3.yaml train_sft_qwen3.py ...
|
| 7 |
+
#
|
| 8 |
+
# If you DO switch to Accelerator.prepare(), you must keep:
|
| 9 |
+
# * mixed_precision bf16 with fp32 master weights and fp32 gradient reduction
|
| 10 |
+
# * FULL_SHARD + transformer-layer wrapping of Qwen3DecoderLayer
|
| 11 |
+
# * use_orig_params: true (the weight-decay parameter groups depend on it)
|
| 12 |
+
# * accelerator.clip_grad_norm_ AFTER all accumulation micro-steps
|
| 13 |
+
# * your own loss normalisation; accelerate's default 1/gradient_accumulation_steps
|
| 14 |
+
# is NOT the recipe's per-microbatch token-weighted mean.
|
| 15 |
+
compute_environment: LOCAL_MACHINE
|
| 16 |
+
distributed_type: FSDP
|
| 17 |
+
downcast_bf16: 'no'
|
| 18 |
+
machine_rank: 0
|
| 19 |
+
main_training_function: main
|
| 20 |
+
mixed_precision: bf16
|
| 21 |
+
num_machines: 1
|
| 22 |
+
num_processes: 8
|
| 23 |
+
rdzv_backend: static
|
| 24 |
+
same_network: true
|
| 25 |
+
use_cpu: false
|
| 26 |
+
enable_cpu_affinity: true
|
| 27 |
+
fsdp_config:
|
| 28 |
+
fsdp_version: 1
|
| 29 |
+
fsdp_sharding_strategy: FULL_SHARD
|
| 30 |
+
fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP
|
| 31 |
+
fsdp_transformer_layer_cls_to_wrap: Qwen3DecoderLayer
|
| 32 |
+
fsdp_backward_prefetch: BACKWARD_PRE
|
| 33 |
+
fsdp_forward_prefetch: false
|
| 34 |
+
fsdp_use_orig_params: true
|
| 35 |
+
fsdp_cpu_ram_efficient_loading: true
|
| 36 |
+
fsdp_sync_module_states: true
|
| 37 |
+
fsdp_offload_params: false
|
| 38 |
+
fsdp_state_dict_type: SHARDED_STATE_DICT
|
| 39 |
+
fsdp_activation_checkpointing: false # the script calls gradient_checkpointing_enable itself
|
| 40 |
+
fsdp_reshard_after_forward: true
|
| 41 |
+
fsdp_limit_all_gathers: true
|
gpu-sft/scripts/gpu_sft/deepspeed_zero3_qwen3.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": [
|
| 3 |
+
"Fallback for 40 GB-class GPUs or shops standardised on DeepSpeed. Only the sharding",
|
| 4 |
+
"backend changes; the recipe (LR schedule, clipping, loss normalisation, packing) still",
|
| 5 |
+
"comes from train_sft_qwen3.py, so run it with --skip-checks and replace the FSDP block",
|
| 6 |
+
"with deepspeed.initialize, or drive it through accelerate's DeepSpeed plugin.",
|
| 7 |
+
"bf16.enabled with fp32 optimizer states reproduces jmp p=f32,c=bfloat16.",
|
| 8 |
+
"gradient_clipping is DISABLED here because the script clips explicitly before step().",
|
| 9 |
+
"Optimizer/scheduler are declared 'none' so the script's AdamW + LambdaLR stay in charge."
|
| 10 |
+
],
|
| 11 |
+
"train_micro_batch_size_per_gpu": 1,
|
| 12 |
+
"gradient_accumulation_steps": 8,
|
| 13 |
+
"gradient_clipping": 0.0,
|
| 14 |
+
"steps_per_print": 1,
|
| 15 |
+
"wall_clock_breakdown": false,
|
| 16 |
+
"bf16": { "enabled": true },
|
| 17 |
+
"fp16": { "enabled": false },
|
| 18 |
+
"zero_optimization": {
|
| 19 |
+
"stage": 3,
|
| 20 |
+
"overlap_comm": true,
|
| 21 |
+
"contiguous_gradients": true,
|
| 22 |
+
"reduce_bucket_size": 67108864,
|
| 23 |
+
"stage3_prefetch_bucket_size": 67108864,
|
| 24 |
+
"stage3_param_persistence_threshold": 65536,
|
| 25 |
+
"stage3_max_live_parameters": 1000000000,
|
| 26 |
+
"stage3_max_reuse_distance": 1000000000,
|
| 27 |
+
"stage3_gather_16bit_weights_on_model_save": true,
|
| 28 |
+
"offload_optimizer": { "device": "none" },
|
| 29 |
+
"offload_param": { "device": "none" }
|
| 30 |
+
},
|
| 31 |
+
"activation_checkpointing": {
|
| 32 |
+
"partition_activations": false,
|
| 33 |
+
"cpu_checkpointing": false,
|
| 34 |
+
"contiguous_memory_optimization": false,
|
| 35 |
+
"number_checkpoints": 36,
|
| 36 |
+
"synchronize_checkpoint_boundary": false,
|
| 37 |
+
"profile": false
|
| 38 |
+
},
|
| 39 |
+
"_offload_note": "On 8x40 GB set offload_optimizer.device='cpu' with pin_memory=true: that frees ~12 GB/GPU at a 20-40% throughput cost and does not change the math."
|
| 40 |
+
}
|
gpu-sft/scripts/gpu_sft/launch_sft_8b.sh
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Qwen3-8B self-instill SFT on a single NVIDIA node. Local disk only: no GCS, no Iris.
|
| 3 |
+
#
|
| 4 |
+
# ./launch_sft_8b.sh <hf-dataset-id> <7-char-revision> <run-name>
|
| 5 |
+
#
|
| 6 |
+
# Stage 1 (CPU, ~10-30 min) is cached; stage 2 is the training run.
|
| 7 |
+
set -euo pipefail
|
| 8 |
+
|
| 9 |
+
DATASET_ID="${1:?usage: launch_sft_8b.sh <dataset-id> <revision> <run-name>}"
|
| 10 |
+
REVISION="${2:?}"
|
| 11 |
+
RUN_NAME="${3:?}"
|
| 12 |
+
|
| 13 |
+
ROOT="${SFT_ROOT:-/data/sft}"
|
| 14 |
+
DATA_DIR="${ROOT}/data/$(echo "${DATASET_ID}" | tr '/' '_')-${REVISION}"
|
| 15 |
+
OUT_DIR="${ROOT}/runs/${RUN_NAME}"
|
| 16 |
+
NPROC="${NPROC:-$(nvidia-smi --list-gpus | wc -l)}"
|
| 17 |
+
|
| 18 |
+
export HF_HOME="${HF_HOME:-${ROOT}/hf}"
|
| 19 |
+
export TOKENIZERS_PARALLELISM=false
|
| 20 |
+
export OMP_NUM_THREADS="${OMP_NUM_THREADS:-8}"
|
| 21 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 22 |
+
export NCCL_DEBUG="${NCCL_DEBUG:-WARN}"
|
| 23 |
+
export PYTHONUNBUFFERED=1
|
| 24 |
+
# W&B is optional; the TPU runs used WANDB_MODE=offline on the workers.
|
| 25 |
+
export WANDB_MODE="${WANDB_MODE:-offline}"
|
| 26 |
+
|
| 27 |
+
mkdir -p "${OUT_DIR}"
|
| 28 |
+
|
| 29 |
+
# ----------------------------------------------------------------------------------
|
| 30 |
+
# Stage 1: HF dataset -> packed local .npy (idempotent, skipped if manifest exists)
|
| 31 |
+
# ----------------------------------------------------------------------------------
|
| 32 |
+
if [[ ! -f "${DATA_DIR}/manifest.json" ]]; then
|
| 33 |
+
python prepare_sft_data.py \
|
| 34 |
+
--dataset-id "${DATASET_ID}" \
|
| 35 |
+
--revision "${REVISION}" \
|
| 36 |
+
--tokenizer Qwen/Qwen3-8B \
|
| 37 |
+
--max-seq-len 32768 \
|
| 38 |
+
--text-replacements marin \
|
| 39 |
+
--num-proc "$(nproc)" \
|
| 40 |
+
--out "${DATA_DIR}" 2>&1 | tee "${OUT_DIR}/prepare.log"
|
| 41 |
+
|
| 42 |
+
# one-time fidelity gate: our encoder must agree with levanter's segment-split encoder
|
| 43 |
+
python prepare_sft_data.py \
|
| 44 |
+
--dataset-id "${DATASET_ID}" --revision "${REVISION}" \
|
| 45 |
+
--tokenizer Qwen/Qwen3-8B --verify 200 2>&1 | tee -a "${OUT_DIR}/prepare.log"
|
| 46 |
+
fi
|
| 47 |
+
|
| 48 |
+
# ----------------------------------------------------------------------------------
|
| 49 |
+
# Stage 1.5: print the plan (LR schedule, epochs, export cadence) before burning GPUs
|
| 50 |
+
# ----------------------------------------------------------------------------------
|
| 51 |
+
python train_sft_qwen3.py --data "${DATA_DIR}" --out "${OUT_DIR}" --recipe qwen3-8b --dry-run
|
| 52 |
+
|
| 53 |
+
# ----------------------------------------------------------------------------------
|
| 54 |
+
# Stage 2: training
|
| 55 |
+
#
|
| 56 |
+
# Batch geometry (must reproduce global batch 64 x 32768 tokens = 2,097,152 tok/step):
|
| 57 |
+
# NPROC=8 : --per-device-batch 1, loss-groups 2 -> 4 micro-steps/group, 8 accum/step
|
| 58 |
+
# NPROC=4 : --per-device-batch 1, loss-groups 2 -> 8 micro-steps/group, 16 accum/step
|
| 59 |
+
# NPROC=16: --per-device-batch 1, loss-groups 2 -> 2 micro-steps/group, 4 accum/step
|
| 60 |
+
#
|
| 61 |
+
# Memory per GPU, Qwen3-8B (8.19e9 params), seq 32768, FSDP FULL_SHARD over 8 GPUs,
|
| 62 |
+
# fp32 master + bf16 compute + full activation checkpointing + chunked CE(2048):
|
| 63 |
+
# fp32 master params 4 * 8.19e9 / 8 = 4.1 GB
|
| 64 |
+
# Adam m + v (fp32) 8 * 8.19e9 / 8 = 8.2 GB
|
| 65 |
+
# fp32 grad shard 4 * 8.19e9 / 8 = 4.1 GB
|
| 66 |
+
# bf16 all-gather buffers root(1.24e9)+layers = 3.7 GB
|
| 67 |
+
# checkpointed layer inputs 36*32768*4096*2 = 9.7 GB
|
| 68 |
+
# one layer recompute working set = 3.2 GB
|
| 69 |
+
# chunked CE (2048 x 151936 fp32, fwd+grad) = 2.5 GB
|
| 70 |
+
# last hidden + grad = 0.5 GB
|
| 71 |
+
# ----------------------------------------------------------
|
| 72 |
+
# ~36 GB + NCCL/fragmentation -> ~40 GB (fits 80 GB comfortably)
|
| 73 |
+
#
|
| 74 |
+
# 4 GPUs : optimiser/param/grad term triples to 32.8 GB -> ~50 GB, still fits 80 GB.
|
| 75 |
+
# 8x40 GB A100: ~30-34 GB, tight. Use --loss-chunk-size 1024 --wrap-embeddings,
|
| 76 |
+
# or 16 GPUs, or the 8K fallback below.
|
| 77 |
+
#
|
| 78 |
+
# Wall clock: ~3.9e20 FLOPs total (1.75e20 dense + 1.2e20 attention at 32K, x4/3 for
|
| 79 |
+
# recompute). 8xH100 at 30-35% MFU -> 40-50 h. 8xB200/H200 -> ~20-25 h.
|
| 80 |
+
# ----------------------------------------------------------------------------------
|
| 81 |
+
torchrun --standalone --nproc_per_node="${NPROC}" train_sft_qwen3.py \
|
| 82 |
+
--data "${DATA_DIR}" \
|
| 83 |
+
--out "${OUT_DIR}" \
|
| 84 |
+
--recipe qwen3-8b \
|
| 85 |
+
--train-batch-size 64 \
|
| 86 |
+
--per-device-batch 1 \
|
| 87 |
+
--loss-groups 2 \
|
| 88 |
+
--num-train-steps 2000 \
|
| 89 |
+
--steps-per-hf-export 100 \
|
| 90 |
+
--steps-per-checkpoint 20 \
|
| 91 |
+
--loss-chunk-size 2048 \
|
| 92 |
+
--hf-export-dtype bfloat16 \
|
| 93 |
+
--resume auto \
|
| 94 |
+
2>&1 | tee -a "${OUT_DIR}/train.log"
|
| 95 |
+
|
| 96 |
+
# ----------------------------------------------------------------------------------
|
| 97 |
+
# 8K-context fallback (OOM at 32K, or no FlashAttention build).
|
| 98 |
+
# Keeps tokens/step, step count and LR schedule identical: 256 x 8192 = 2,097,152.
|
| 99 |
+
# Documents longer than 8192 are LEFT-SLICED (kept from the beginning) exactly as
|
| 100 |
+
# levanter's slice_strategy="left" does; check `docs_over_max_seq_len` in the
|
| 101 |
+
# prepare-stage stats before accepting this.
|
| 102 |
+
#
|
| 103 |
+
# python prepare_sft_data.py ... --max-seq-len 8192 --out ${DATA_DIR}-8k
|
| 104 |
+
# torchrun --standalone --nproc_per_node=8 train_sft_qwen3.py \
|
| 105 |
+
# --data ${DATA_DIR}-8k --out ${OUT_DIR}-8k --recipe qwen3-8b \
|
| 106 |
+
# --train-batch-size 256 --per-device-batch 4 --loss-groups 2 \
|
| 107 |
+
# --num-train-steps 2000 --loss-chunk-size 2048
|
| 108 |
+
# ----------------------------------------------------------------------------------
|
gpu-sft/scripts/gpu_sft/prepare_sft_data.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# ruff: noqa: E501
|
| 3 |
+
"""Stage 1 of the GPU port of the Marin/Levanter Qwen3 self-instill SFT recipe.
|
| 4 |
+
|
| 5 |
+
HF dataset (pinned revision)
|
| 6 |
+
-> Marin conversation transform (DEFAULT_TEXT_REPLACEMENTS)
|
| 7 |
+
-> Qwen3 chat template with {% generation %} assistant masking
|
| 8 |
+
-> Levanter greedy *contiguous* packing (max 64 segments / 32768 tokens)
|
| 9 |
+
-> local .npy memmaps + manifest.json
|
| 10 |
+
|
| 11 |
+
Every step here is a deliberate port of the TPU pipeline. Where the TPU pipeline has a
|
| 12 |
+
bug or a surprising behaviour, this script reproduces it by default and exposes a flag to
|
| 13 |
+
turn it off. Read the "FIDELITY NOTES" block before changing anything.
|
| 14 |
+
|
| 15 |
+
FIDELITY NOTES
|
| 16 |
+
--------------
|
| 17 |
+
1. TEXT REPLACEMENTS (reproduced by default; `--text-replacements none` to disable).
|
| 18 |
+
marin/transform/conversation/transform_conversation.py applies
|
| 19 |
+
DEFAULT_TEXT_REPLACEMENTS = {"<think>": "<|start_think|>", "</think>": "<|end_think|>"}
|
| 20 |
+
to every message because multi_turn_adapter() leaves `replacements=None`.
|
| 21 |
+
Those two strings are Marin/Llama3 tokens, NOT Qwen3 tokens, so under the Qwen3
|
| 22 |
+
tokenizer they encode as ordinary multi-token text. Consequence: the chat template's
|
| 23 |
+
`'</think>' in content` reasoning-split branch never fires, `reasoning_content` stays
|
| 24 |
+
empty, and every FINAL assistant turn is rendered as
|
| 25 |
+
|
| 26 |
+
<|im_start|>assistant\n<think>\n\n</think>\n\n<|start_think|>...<|end_think|>...<|im_end|>\n
|
| 27 |
+
|
| 28 |
+
i.e. an EMPTY Qwen3 think block followed by the real reasoning in non-special text.
|
| 29 |
+
This is what the TPU checkpoints were trained on. Disabling it changes the token
|
| 30 |
+
layout and the assistant mask, and the resulting model will not match the TPU runs.
|
| 31 |
+
|
| 32 |
+
2. DOCUMENT ORDER. Packing is greedy over *adjacent* documents in cache order. We use HF
|
| 33 |
+
dataset row order. The TPU cache order came from the zephyr shard writer; if that
|
| 34 |
+
interleaved shards, pack boundaries differ. Impact is limited: cross-document
|
| 35 |
+
attention is blocked and per-token loss weights are unchanged, so only the grouping
|
| 36 |
+
of documents into 32768-token windows (and hence the microbatch denominators) moves.
|
| 37 |
+
|
| 38 |
+
3. VOCAB. We do NOT resize embeddings. The HF checkpoint already has vocab_size=151936
|
| 39 |
+
while len(tokenizer) is 151669; Levanter padded the *tokenizer* up to the model. We
|
| 40 |
+
emit the 267 <|padding_i|> tokens only at HF-export time (see train_sft_qwen3.py) so
|
| 41 |
+
the exported tokenizer matches the TPU exports. Token ids are unaffected.
|
| 42 |
+
|
| 43 |
+
4. NO SHUFFLE. The TPU experiment passes `shuffle=DATASET_SIZE` (an int) where levanter
|
| 44 |
+
expects bool | BlockShuffleConfig, which makes shuffling a silent no-op. The trainer
|
| 45 |
+
therefore walks packed examples in order and wraps around. Nothing to do here.
|
| 46 |
+
|
| 47 |
+
Usage
|
| 48 |
+
-----
|
| 49 |
+
python prepare_sft_data.py \\
|
| 50 |
+
--dataset-id fzzhang/qwen3_8b_hs_competition_depth2_probval_instill_n8_valredundancy5_round1 \\
|
| 51 |
+
--revision <7-char-sha> \\
|
| 52 |
+
--tokenizer Qwen/Qwen3-8B \\
|
| 53 |
+
--max-seq-len 32768 \\
|
| 54 |
+
--out /data/sft/hs_competition_depth2 \\
|
| 55 |
+
--num-proc 16
|
| 56 |
+
|
| 57 |
+
# prove the encoder matches Levanter's segment-split encoder on 200 sampled docs
|
| 58 |
+
python prepare_sft_data.py ... --verify 200
|
| 59 |
+
|
| 60 |
+
# packing statistics only (answers "how many epochs is 2000 steps?")
|
| 61 |
+
python prepare_sft_data.py ... --stats-only
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
from __future__ import annotations
|
| 65 |
+
|
| 66 |
+
import argparse
|
| 67 |
+
import hashlib
|
| 68 |
+
import json
|
| 69 |
+
import os
|
| 70 |
+
import re
|
| 71 |
+
import sys
|
| 72 |
+
import time
|
| 73 |
+
from itertools import chain
|
| 74 |
+
from typing import Any
|
| 75 |
+
|
| 76 |
+
import numpy as np
|
| 77 |
+
|
| 78 |
+
# ----------------------------------------------------------------------------------
|
| 79 |
+
# Verbatim copy of marin experiments/qwen3_chat_template.py :: QWEN_3_CHAT_TEMPLATE
|
| 80 |
+
# (upstream Qwen3 template + {% generation %} tags around assistant content, tool-call
|
| 81 |
+
# bodies and the trailing '<|im_end|>\n').
|
| 82 |
+
# ----------------------------------------------------------------------------------
|
| 83 |
+
QWEN3_CHAT_TEMPLATE = r"""{%- if tools is defined and tools %}
|
| 84 |
+
{{- '<|im_start|>system\n' }}
|
| 85 |
+
{%- if messages[0].role == 'system' %}
|
| 86 |
+
{{- messages[0].content + '\n\n' }}
|
| 87 |
+
{%- endif %}
|
| 88 |
+
{{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
|
| 89 |
+
{%- for tool in tools %}
|
| 90 |
+
{{- "\n" }}
|
| 91 |
+
{{- tool | tojson }}
|
| 92 |
+
{%- endfor %}
|
| 93 |
+
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
|
| 94 |
+
{%- else %}
|
| 95 |
+
{%- if messages[0].role == 'system' %}
|
| 96 |
+
{{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
|
| 97 |
+
{%- endif %}
|
| 98 |
+
{%- endif %}
|
| 99 |
+
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
| 100 |
+
{%- for message in messages[::-1] %}
|
| 101 |
+
{%- set index = (messages|length - 1) - loop.index0 %}
|
| 102 |
+
{%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
|
| 103 |
+
{%- set ns.multi_step_tool = false %}
|
| 104 |
+
{%- set ns.last_query_index = index %}
|
| 105 |
+
{%- endif %}
|
| 106 |
+
{%- endfor %}
|
| 107 |
+
{%- for message in messages %}
|
| 108 |
+
{%- if message.content is string %}
|
| 109 |
+
{%- set content = message.content %}
|
| 110 |
+
{%- else %}
|
| 111 |
+
{%- set content = '' %}
|
| 112 |
+
{%- endif %}
|
| 113 |
+
{%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
|
| 114 |
+
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
|
| 115 |
+
{%- elif message.role == "assistant" %}
|
| 116 |
+
{%- set reasoning_content = '' %}
|
| 117 |
+
{%- if message.reasoning_content is string %}
|
| 118 |
+
{%- set reasoning_content = message.reasoning_content %}
|
| 119 |
+
{%- else %}
|
| 120 |
+
{%- if '</think>' in content %}
|
| 121 |
+
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
| 122 |
+
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
| 123 |
+
{%- endif %}
|
| 124 |
+
{%- endif %}
|
| 125 |
+
{%- if loop.index0 > ns.last_query_index %}
|
| 126 |
+
{%- if loop.last or (not loop.last and reasoning_content) %}
|
| 127 |
+
{{- '<|im_start|>' + message.role + '\n' }}{% generation %}{{- '<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}{% endgeneration %}
|
| 128 |
+
{%- else %}
|
| 129 |
+
{{- '<|im_start|>' + message.role + '\n' }}{% generation %}{{- content }}{% endgeneration %}
|
| 130 |
+
{%- endif %}
|
| 131 |
+
{%- else %}
|
| 132 |
+
{{- '<|im_start|>' + message.role + '\n' }}{% generation %}{{- content }}{% endgeneration %}
|
| 133 |
+
{%- endif %}
|
| 134 |
+
{%- if message.tool_calls %}
|
| 135 |
+
{%- for tool_call in message.tool_calls %}
|
| 136 |
+
{%- if (loop.first and content) or (not loop.first) %}
|
| 137 |
+
{% generation %}{{- '\n' }}{% endgeneration %}
|
| 138 |
+
{%- endif %}
|
| 139 |
+
{%- if tool_call.function %}
|
| 140 |
+
{%- set tool_call = tool_call.function %}
|
| 141 |
+
{%- endif %}
|
| 142 |
+
{% generation %}{{- '<tool_call>\n{"name": "' }}
|
| 143 |
+
{{- tool_call.name }}
|
| 144 |
+
{{- '", "arguments": ' }}
|
| 145 |
+
{%- if tool_call.arguments is string %}
|
| 146 |
+
{{- tool_call.arguments }}
|
| 147 |
+
{%- else %}
|
| 148 |
+
{{- tool_call.arguments | tojson }}
|
| 149 |
+
{%- endif %}
|
| 150 |
+
{{- '}\n</tool_call>' }}{% endgeneration %}
|
| 151 |
+
{%- endfor %}
|
| 152 |
+
{%- endif %}
|
| 153 |
+
{% generation %}{{- '<|im_end|>\n' }}{% endgeneration %}
|
| 154 |
+
{%- elif message.role == "tool" %}
|
| 155 |
+
{%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
|
| 156 |
+
{{- '<|im_start|>user' }}
|
| 157 |
+
{%- endif %}
|
| 158 |
+
{{- '\n<tool_response>\n' }}
|
| 159 |
+
{{- content }}
|
| 160 |
+
{{- '\n</tool_response>' }}
|
| 161 |
+
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
|
| 162 |
+
{{- '<|im_end|>\n' }}
|
| 163 |
+
{%- endif %}
|
| 164 |
+
{%- endif %}
|
| 165 |
+
{%- endfor %}
|
| 166 |
+
{%- if add_generation_prompt %}
|
| 167 |
+
{{- '<|im_start|>assistant\n' }}
|
| 168 |
+
{%- if enable_thinking is defined and enable_thinking is false %}
|
| 169 |
+
{% generation %}{{- '<think>\n\n</think>\n\n' }}{% endgeneration %}
|
| 170 |
+
{%- endif %}
|
| 171 |
+
{%- endif %}"""
|
| 172 |
+
|
| 173 |
+
DEFAULT_TEXT_REPLACEMENTS = {"<think>": "<|start_think|>", "</think>": "<|end_think|>"}
|
| 174 |
+
|
| 175 |
+
# levanter/tokenizers.py sentinels, used only by --verify.
|
| 176 |
+
_GEN_START = "__MARIN_GEN_START_7f3a9c__"
|
| 177 |
+
_GEN_END = "__MARIN_GEN_END_7f3a9c__"
|
| 178 |
+
|
| 179 |
+
MAX_SEGMENTS_PER_EXAMPLE = 64 # levanter datasets.py: 64 when pack is True
|
| 180 |
+
FORMAT_VERSION = 2
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# ----------------------------------------------------------------------------------
|
| 184 |
+
# message normalisation (port of multi_turn_adapter + transform_row)
|
| 185 |
+
# ----------------------------------------------------------------------------------
|
| 186 |
+
def normalize_messages(
|
| 187 |
+
row: dict[str, Any],
|
| 188 |
+
conversation_column: str,
|
| 189 |
+
role_key: str,
|
| 190 |
+
content_key: str,
|
| 191 |
+
replacements: dict[str, str] | None,
|
| 192 |
+
) -> list[dict[str, Any]]:
|
| 193 |
+
raw = row[conversation_column]
|
| 194 |
+
out: list[dict[str, Any]] = []
|
| 195 |
+
for m in raw:
|
| 196 |
+
role = m[role_key]
|
| 197 |
+
content = m.get(content_key)
|
| 198 |
+
if isinstance(content, str) and replacements:
|
| 199 |
+
for old, new in replacements.items():
|
| 200 |
+
content = content.replace(old, new)
|
| 201 |
+
msg: dict[str, Any] = {"role": role, "content": content}
|
| 202 |
+
# marin's OpenAIChatMessage carries these; the template only reads tool_calls.
|
| 203 |
+
if m.get("tool_calls"):
|
| 204 |
+
msg["tool_calls"] = m["tool_calls"]
|
| 205 |
+
out.append(msg)
|
| 206 |
+
return out
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# ----------------------------------------------------------------------------------
|
| 210 |
+
# encoding
|
| 211 |
+
# ----------------------------------------------------------------------------------
|
| 212 |
+
def encode_conversation(tokenizer, messages: list[dict[str, Any]]) -> tuple[list[int], list[int]]:
|
| 213 |
+
"""HF path: render once, tokenize once, map {% generation %} char spans to tokens."""
|
| 214 |
+
enc = tokenizer.apply_chat_template(
|
| 215 |
+
messages,
|
| 216 |
+
chat_template=QWEN3_CHAT_TEMPLATE,
|
| 217 |
+
tokenize=True,
|
| 218 |
+
add_generation_prompt=False,
|
| 219 |
+
return_dict=True,
|
| 220 |
+
return_assistant_tokens_mask=True,
|
| 221 |
+
)
|
| 222 |
+
return list(enc["input_ids"]), list(enc["assistant_masks"])
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def _levanter_env():
|
| 226 |
+
"""Recreate levanter._make_jinja_env([_GenerationSentinelExtension])."""
|
| 227 |
+
import time as _time
|
| 228 |
+
|
| 229 |
+
import jinja2
|
| 230 |
+
import jinja2.ext
|
| 231 |
+
import jinja2.sandbox
|
| 232 |
+
|
| 233 |
+
class _GenerationSentinelExtension(jinja2.ext.Extension):
|
| 234 |
+
tags = {"generation"}
|
| 235 |
+
|
| 236 |
+
def parse(self, parser):
|
| 237 |
+
lineno = next(parser.stream).lineno
|
| 238 |
+
body = parser.parse_statements(("name:endgeneration",), drop_needle=True)
|
| 239 |
+
call = self.call_method("_wrap_generation", [])
|
| 240 |
+
return jinja2.nodes.CallBlock(call, [], [], body).set_lineno(lineno)
|
| 241 |
+
|
| 242 |
+
@staticmethod
|
| 243 |
+
def _wrap_generation(caller):
|
| 244 |
+
return _GEN_START + caller() + _GEN_END
|
| 245 |
+
|
| 246 |
+
def _raise(message):
|
| 247 |
+
raise jinja2.exceptions.TemplateError(message)
|
| 248 |
+
|
| 249 |
+
# Match transformers.apply_chat_template, which renders with the DEFAULT lenient
|
| 250 |
+
# Undefined: the Qwen3 template references optional fields (message.tool_calls,
|
| 251 |
+
# message.reasoning_content) that plain {role, content} messages lack, and under
|
| 252 |
+
# StrictUndefined `{% if message.tool_calls %}` raises UndefinedError. Lenient
|
| 253 |
+
# Undefined evaluates missing attrs as falsy, exactly as the real encoding path does.
|
| 254 |
+
env = jinja2.sandbox.SandboxedEnvironment(
|
| 255 |
+
undefined=jinja2.Undefined,
|
| 256 |
+
trim_blocks=True,
|
| 257 |
+
lstrip_blocks=True,
|
| 258 |
+
extensions=[_GenerationSentinelExtension],
|
| 259 |
+
)
|
| 260 |
+
env.globals.update({"raise_exception": _raise, "strftime_now": lambda fmt: _time.strftime(fmt)})
|
| 261 |
+
return env
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def encode_conversation_levanter(tokenizer, messages: list[dict[str, Any]]) -> tuple[list[int], list[int]]:
|
| 265 |
+
"""Reference path: levanter's segment-split encoder, used only by --verify."""
|
| 266 |
+
env = _levanter_env()
|
| 267 |
+
rendered = env.from_string(QWEN3_CHAT_TEMPLATE).render(
|
| 268 |
+
messages=messages,
|
| 269 |
+
add_generation_prompt=False,
|
| 270 |
+
bos_token=tokenizer.bos_token or "",
|
| 271 |
+
eos_token=tokenizer.eos_token or "",
|
| 272 |
+
)
|
| 273 |
+
parts = re.split(f"({re.escape(_GEN_START)}|{re.escape(_GEN_END)})", rendered)
|
| 274 |
+
ids: list[int] = []
|
| 275 |
+
mask: list[int] = []
|
| 276 |
+
is_assistant = False
|
| 277 |
+
for part in parts:
|
| 278 |
+
if part == _GEN_START:
|
| 279 |
+
is_assistant = True
|
| 280 |
+
continue
|
| 281 |
+
if part == _GEN_END:
|
| 282 |
+
is_assistant = False
|
| 283 |
+
continue
|
| 284 |
+
if not part:
|
| 285 |
+
continue
|
| 286 |
+
seg = tokenizer.encode(part, add_special_tokens=False)
|
| 287 |
+
ids.extend(seg)
|
| 288 |
+
mask.extend([1 if is_assistant else 0] * len(seg))
|
| 289 |
+
return ids, mask
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
# ----------------------------------------------------------------------------------
|
| 293 |
+
# packing (line-by-line port of levanter/data/packing.py::pack_documents,
|
| 294 |
+
# slice_strategy="left", pad_with_zeros=True, max_segments_per_example=64)
|
| 295 |
+
# ----------------------------------------------------------------------------------
|
| 296 |
+
def pack_document_ranges(lengths: np.ndarray, max_length: int, max_segments: int) -> list[tuple[int, int]]:
|
| 297 |
+
n = int(len(lengths))
|
| 298 |
+
ranges: list[tuple[int, int]] = []
|
| 299 |
+
i = 0
|
| 300 |
+
while i < n:
|
| 301 |
+
start = i
|
| 302 |
+
total_segments = 0
|
| 303 |
+
running = 0
|
| 304 |
+
while i < n:
|
| 305 |
+
if total_segments + 1 > max_segments:
|
| 306 |
+
break
|
| 307 |
+
candidate = running + int(lengths[i])
|
| 308 |
+
end_pack_after_this = False
|
| 309 |
+
if candidate > max_length:
|
| 310 |
+
if i == start:
|
| 311 |
+
# single oversized document: keep it and slice from the left
|
| 312 |
+
end_pack_after_this = True
|
| 313 |
+
else:
|
| 314 |
+
break
|
| 315 |
+
running = candidate
|
| 316 |
+
total_segments += 1
|
| 317 |
+
i += 1
|
| 318 |
+
if end_pack_after_this:
|
| 319 |
+
break
|
| 320 |
+
if i == start: # defensive; unreachable with max_segments >= 1
|
| 321 |
+
i = start + 1
|
| 322 |
+
ranges.append((start, i))
|
| 323 |
+
return ranges
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# ----------------------------------------------------------------------------------
|
| 327 |
+
def build(args) -> None:
|
| 328 |
+
from datasets import load_dataset
|
| 329 |
+
from transformers import AutoTokenizer
|
| 330 |
+
|
| 331 |
+
t0 = time.time()
|
| 332 |
+
replacements = DEFAULT_TEXT_REPLACEMENTS if args.text_replacements == "marin" else None
|
| 333 |
+
print(f"[prepare] text replacements: {replacements}", flush=True)
|
| 334 |
+
|
| 335 |
+
tokenizer = AutoTokenizer.from_pretrained(args.tokenizer)
|
| 336 |
+
if not tokenizer.is_fast:
|
| 337 |
+
raise RuntimeError("A fast tokenizer is required for return_assistant_tokens_mask.")
|
| 338 |
+
|
| 339 |
+
ds = load_dataset(args.dataset_id, revision=args.revision, split=args.split)
|
| 340 |
+
if args.limit:
|
| 341 |
+
ds = ds.select(range(min(args.limit, len(ds))))
|
| 342 |
+
print(f"[prepare] loaded {len(ds)} rows from {args.dataset_id}@{args.revision}", flush=True)
|
| 343 |
+
|
| 344 |
+
conversation_column = args.conversation_column
|
| 345 |
+
role_key, content_key = args.role_key, args.content_key
|
| 346 |
+
|
| 347 |
+
def _map(batch, indices):
|
| 348 |
+
tok = tokenizer
|
| 349 |
+
ids_out, mask_out, len_out = [], [], []
|
| 350 |
+
for k in range(len(indices)):
|
| 351 |
+
row = {conversation_column: batch[conversation_column][k]}
|
| 352 |
+
messages = normalize_messages(row, conversation_column, role_key, content_key, replacements)
|
| 353 |
+
ids, mask = encode_conversation(tok, messages)
|
| 354 |
+
if not any(mask):
|
| 355 |
+
raise ValueError(
|
| 356 |
+
f"Row {indices[k]} produced no assistant tokens. levanter's ChatProcessor raises here too."
|
| 357 |
+
)
|
| 358 |
+
ids_out.append(ids)
|
| 359 |
+
mask_out.append(mask)
|
| 360 |
+
len_out.append(len(ids))
|
| 361 |
+
return {"input_ids": ids_out, "assistant_mask": mask_out, "length": len_out}
|
| 362 |
+
|
| 363 |
+
enc = ds.map(
|
| 364 |
+
_map,
|
| 365 |
+
batched=True,
|
| 366 |
+
batch_size=16,
|
| 367 |
+
with_indices=True,
|
| 368 |
+
num_proc=args.num_proc,
|
| 369 |
+
remove_columns=ds.column_names,
|
| 370 |
+
desc="tokenize",
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
lengths = np.asarray(enc["length"], dtype=np.int64)
|
| 374 |
+
L = args.max_seq_len
|
| 375 |
+
ranges = pack_document_ranges(lengths, L, MAX_SEGMENTS_PER_EXAMPLE)
|
| 376 |
+
n_packs = len(ranges)
|
| 377 |
+
|
| 378 |
+
stats = {
|
| 379 |
+
"n_documents": int(len(lengths)),
|
| 380 |
+
"doc_tokens_total": int(lengths.sum()),
|
| 381 |
+
"doc_len_mean": float(lengths.mean()),
|
| 382 |
+
"doc_len_p50": float(np.percentile(lengths, 50)),
|
| 383 |
+
"doc_len_p90": float(np.percentile(lengths, 90)),
|
| 384 |
+
"doc_len_p99": float(np.percentile(lengths, 99)),
|
| 385 |
+
"doc_len_max": int(lengths.max()),
|
| 386 |
+
"docs_over_max_seq_len": int((lengths > L).sum()),
|
| 387 |
+
"tokens_truncated": int(np.clip(lengths - L, 0, None).sum()),
|
| 388 |
+
"n_packed_examples": n_packs,
|
| 389 |
+
"docs_per_pack_mean": float(len(lengths) / max(n_packs, 1)),
|
| 390 |
+
"pack_fill_ratio": float(min(lengths.sum(), n_packs * L) / max(n_packs * L, 1)),
|
| 391 |
+
}
|
| 392 |
+
for gb in (64, 128, 256):
|
| 393 |
+
stats[f"epochs_at_2000_steps_bs{gb}"] = round(2000 * gb / max(n_packs, 1), 3)
|
| 394 |
+
print("[prepare] stats: " + json.dumps(stats, indent=2), flush=True)
|
| 395 |
+
if args.stats_only:
|
| 396 |
+
return
|
| 397 |
+
|
| 398 |
+
os.makedirs(args.out, exist_ok=True)
|
| 399 |
+
tokens = np.lib.format.open_memmap(
|
| 400 |
+
os.path.join(args.out, "tokens.npy"), mode="w+", dtype=np.uint32, shape=(n_packs, L)
|
| 401 |
+
)
|
| 402 |
+
amask = np.lib.format.open_memmap(
|
| 403 |
+
os.path.join(args.out, "assistant_mask.npy"), mode="w+", dtype=np.uint8, shape=(n_packs, L)
|
| 404 |
+
)
|
| 405 |
+
doc_lens_flat: list[int] = []
|
| 406 |
+
pack_offsets = np.zeros(n_packs + 1, dtype=np.int64)
|
| 407 |
+
assistant_counts = np.zeros(n_packs, dtype=np.int64)
|
| 408 |
+
real_tokens = np.zeros(n_packs, dtype=np.int64)
|
| 409 |
+
|
| 410 |
+
for p, (s, e) in enumerate(ranges):
|
| 411 |
+
rows = enc[s:e]
|
| 412 |
+
ids = list(chain.from_iterable(rows["input_ids"]))
|
| 413 |
+
msk = list(chain.from_iterable(rows["assistant_mask"]))
|
| 414 |
+
dlens = [len(x) for x in rows["input_ids"]]
|
| 415 |
+
if len(ids) > L:
|
| 416 |
+
assert e - s == 1, "levanter never packs two docs when one alone overflows"
|
| 417 |
+
ids, msk, dlens = ids[:L], msk[:L], [L]
|
| 418 |
+
n = len(ids)
|
| 419 |
+
tokens[p, :n] = np.asarray(ids, dtype=np.uint32)
|
| 420 |
+
tokens[p, n:] = 0 # pad_with_zeros=True
|
| 421 |
+
amask[p, :n] = np.asarray(msk, dtype=np.uint8)
|
| 422 |
+
amask[p, n:] = 0
|
| 423 |
+
doc_lens_flat.extend(dlens)
|
| 424 |
+
pack_offsets[p + 1] = len(doc_lens_flat)
|
| 425 |
+
# levanter loss weight = roll(mask, -1) * not_last_mask => sum(mask[1:])
|
| 426 |
+
assistant_counts[p] = int(sum(msk)) - int(msk[0])
|
| 427 |
+
real_tokens[p] = n
|
| 428 |
+
if p % 2000 == 0:
|
| 429 |
+
print(f"[prepare] packed {p}/{n_packs}", flush=True)
|
| 430 |
+
|
| 431 |
+
tokens.flush()
|
| 432 |
+
amask.flush()
|
| 433 |
+
np.save(os.path.join(args.out, "doc_lens.npy"), np.asarray(doc_lens_flat, dtype=np.int32))
|
| 434 |
+
np.save(os.path.join(args.out, "pack_offsets.npy"), pack_offsets)
|
| 435 |
+
np.save(os.path.join(args.out, "assistant_counts.npy"), assistant_counts)
|
| 436 |
+
np.save(os.path.join(args.out, "real_tokens.npy"), real_tokens)
|
| 437 |
+
|
| 438 |
+
stats["assistant_tokens_total"] = int(assistant_counts.sum())
|
| 439 |
+
stats["assistant_token_fraction"] = float(assistant_counts.sum() / max(real_tokens.sum(), 1))
|
| 440 |
+
cfg = {
|
| 441 |
+
"format_version": FORMAT_VERSION,
|
| 442 |
+
"dataset_id": args.dataset_id,
|
| 443 |
+
"revision": args.revision,
|
| 444 |
+
"split": args.split,
|
| 445 |
+
"tokenizer": args.tokenizer,
|
| 446 |
+
"max_seq_len": L,
|
| 447 |
+
"max_segments_per_example": MAX_SEGMENTS_PER_EXAMPLE,
|
| 448 |
+
"text_replacements": replacements,
|
| 449 |
+
"chat_template_sha256": hashlib.sha256(QWEN3_CHAT_TEMPLATE.encode()).hexdigest(),
|
| 450 |
+
"tokenizer_len_unpadded": len(tokenizer),
|
| 451 |
+
"n_packs": n_packs,
|
| 452 |
+
"stats": stats,
|
| 453 |
+
"built_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
| 454 |
+
}
|
| 455 |
+
with open(os.path.join(args.out, "manifest.json"), "w") as f:
|
| 456 |
+
json.dump(cfg, f, indent=2)
|
| 457 |
+
print(f"[prepare] wrote {n_packs} packed examples to {args.out} in {time.time() - t0:.0f}s", flush=True)
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
# ----------------------------------------------------------------------------------
|
| 461 |
+
def verify(args) -> None:
|
| 462 |
+
"""Assert the HF encoder == levanter's segment-split encoder on sampled documents."""
|
| 463 |
+
from datasets import load_dataset
|
| 464 |
+
from transformers import AutoTokenizer
|
| 465 |
+
|
| 466 |
+
replacements = DEFAULT_TEXT_REPLACEMENTS if args.text_replacements == "marin" else None
|
| 467 |
+
tokenizer = AutoTokenizer.from_pretrained(args.tokenizer)
|
| 468 |
+
ds = load_dataset(args.dataset_id, revision=args.revision, split=args.split)
|
| 469 |
+
rng = np.random.default_rng(0)
|
| 470 |
+
idx = rng.choice(len(ds), size=min(args.verify, len(ds)), replace=False)
|
| 471 |
+
|
| 472 |
+
bad = 0
|
| 473 |
+
for j, i in enumerate(idx):
|
| 474 |
+
messages = normalize_messages(
|
| 475 |
+
ds[int(i)], args.conversation_column, args.role_key, args.content_key, replacements
|
| 476 |
+
)
|
| 477 |
+
a_ids, a_mask = encode_conversation(tokenizer, messages)
|
| 478 |
+
b_ids, b_mask = encode_conversation_levanter(tokenizer, messages)
|
| 479 |
+
if a_ids != b_ids or a_mask != b_mask:
|
| 480 |
+
bad += 1
|
| 481 |
+
n = min(len(a_ids), len(b_ids))
|
| 482 |
+
first = next((k for k in range(n) if a_ids[k] != b_ids[k] or a_mask[k] != b_mask[k]), n)
|
| 483 |
+
print(
|
| 484 |
+
f"[verify] MISMATCH row={int(i)} len_hf={len(a_ids)} len_lev={len(b_ids)} first_diff={first}",
|
| 485 |
+
file=sys.stderr,
|
| 486 |
+
)
|
| 487 |
+
print(f" hf : {a_ids[max(0, first - 5) : first + 5]}", file=sys.stderr)
|
| 488 |
+
print(f" lev: {b_ids[max(0, first - 5) : first + 5]}", file=sys.stderr)
|
| 489 |
+
if j == 0:
|
| 490 |
+
txt = tokenizer.decode(a_ids[:120])
|
| 491 |
+
print(f"[verify] sample head: {txt!r}", flush=True)
|
| 492 |
+
print(f"[verify] assistant tokens: {sum(a_mask)}/{len(a_mask)}", flush=True)
|
| 493 |
+
if bad:
|
| 494 |
+
raise SystemExit(f"[verify] {bad}/{len(idx)} documents disagree with the levanter encoder")
|
| 495 |
+
print(f"[verify] OK: {len(idx)} documents byte-identical to the levanter encoder", flush=True)
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def main() -> None:
|
| 499 |
+
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 500 |
+
p.add_argument("--dataset-id", required=True)
|
| 501 |
+
p.add_argument("--revision", required=True, help="7-char HF sha, pinned exactly like instruction_datasets.py")
|
| 502 |
+
p.add_argument("--split", default="train")
|
| 503 |
+
p.add_argument("--tokenizer", default="Qwen/Qwen3-8B")
|
| 504 |
+
p.add_argument("--max-seq-len", type=int, default=32768)
|
| 505 |
+
p.add_argument("--out", default=None)
|
| 506 |
+
p.add_argument("--num-proc", type=int, default=max(1, (os.cpu_count() or 8) // 2))
|
| 507 |
+
p.add_argument("--limit", type=int, default=0)
|
| 508 |
+
p.add_argument("--conversation-column", default="messages")
|
| 509 |
+
p.add_argument("--role-key", default="role")
|
| 510 |
+
p.add_argument("--content-key", default="content")
|
| 511 |
+
p.add_argument("--text-replacements", choices=("marin", "none"), default="marin")
|
| 512 |
+
p.add_argument("--verify", type=int, default=0, help="verify N sampled docs against the levanter encoder and exit")
|
| 513 |
+
p.add_argument("--stats-only", action="store_true")
|
| 514 |
+
args = p.parse_args()
|
| 515 |
+
|
| 516 |
+
if args.verify:
|
| 517 |
+
verify(args)
|
| 518 |
+
return
|
| 519 |
+
if not args.out and not args.stats_only:
|
| 520 |
+
p.error("--out is required unless --stats-only/--verify")
|
| 521 |
+
build(args)
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
if __name__ == "__main__":
|
| 525 |
+
main()
|
gpu-sft/scripts/gpu_sft/push_exports.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Watch a training run's hf/ dir and upload each new step-N export to a private HF
|
| 3 |
+
repo, so ephemeral-disk checkpoints survive a machine kill and are eval-able anywhere.
|
| 4 |
+
|
| 5 |
+
train_sft_qwen3.py writes a bf16 hf/step-N/ every steps_per_hf_export. This uploads each
|
| 6 |
+
one (once it has finished writing) to <repo> under path_in_repo=step-N/. One repo per
|
| 7 |
+
experiment; steps are subfolders. Idempotent via a local .pushed_steps.json marker, so a
|
| 8 |
+
restart of this watcher (or of the whole run) re-uploads nothing.
|
| 9 |
+
|
| 10 |
+
Run in the background alongside training (run_sft.sh does this for you):
|
| 11 |
+
python push_exports.py --out <rundir> --repo fzzhang/<exp>_sft --until-step 2000
|
| 12 |
+
|
| 13 |
+
HF auth: uses your `huggingface-cli login` token (~/.cache/huggingface/token).
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def step_dirs(hf_dir: Path) -> dict[int, Path]:
|
| 24 |
+
out: dict[int, Path] = {}
|
| 25 |
+
if not hf_dir.is_dir():
|
| 26 |
+
return out
|
| 27 |
+
for d in hf_dir.glob("step-*"):
|
| 28 |
+
if d.is_dir():
|
| 29 |
+
try:
|
| 30 |
+
out[int(d.name.split("-", 1)[1])] = d
|
| 31 |
+
except (IndexError, ValueError):
|
| 32 |
+
pass
|
| 33 |
+
return out
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def is_stable(d: Path, stable_sec: float) -> bool:
|
| 37 |
+
"""True if no file in the dir was modified within the last stable_sec (write finished)."""
|
| 38 |
+
files = [p for p in d.rglob("*") if p.is_file()]
|
| 39 |
+
if not files:
|
| 40 |
+
return False
|
| 41 |
+
return (time.time() - max(p.stat().st_mtime for p in files)) > stable_sec
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def main() -> None:
|
| 45 |
+
ap = argparse.ArgumentParser(description="Upload hf/step-N exports to HF as they land")
|
| 46 |
+
ap.add_argument("--out", required=True, help="training run dir (contains hf/)")
|
| 47 |
+
ap.add_argument("--repo", required=True, help="target HF repo id, e.g. fzzhang/<exp>_sft")
|
| 48 |
+
ap.add_argument("--poll", type=int, default=120, help="seconds between scans")
|
| 49 |
+
ap.add_argument("--stable-sec", type=float, default=60.0,
|
| 50 |
+
help="a step dir must be idle this long before upload (guards partial writes)")
|
| 51 |
+
ap.add_argument("--every", type=int, default=1,
|
| 52 |
+
help="upload every Nth export (exports are every 100 steps; 1=all). "
|
| 53 |
+
"The --until-step export is always uploaded regardless.")
|
| 54 |
+
ap.add_argument("--until-step", type=int, default=0,
|
| 55 |
+
help="exit after this step is pushed (0 = run forever / until killed)")
|
| 56 |
+
ap.add_argument("--once", action="store_true",
|
| 57 |
+
help="scan once, push all unpushed steps (ignore --stable-sec), then exit")
|
| 58 |
+
args = ap.parse_args()
|
| 59 |
+
|
| 60 |
+
from huggingface_hub import HfApi
|
| 61 |
+
|
| 62 |
+
api = HfApi()
|
| 63 |
+
api.create_repo(args.repo, repo_type="model", private=True, exist_ok=True)
|
| 64 |
+
|
| 65 |
+
hf_dir = Path(args.out) / "hf"
|
| 66 |
+
marker = Path(args.out) / ".pushed_steps.json"
|
| 67 |
+
pushed = set(json.loads(marker.read_text())) if marker.exists() else set()
|
| 68 |
+
print(f"[push] {hf_dir} -> {args.repo} (private); already pushed: {sorted(pushed)}", flush=True)
|
| 69 |
+
|
| 70 |
+
while True:
|
| 71 |
+
for step in sorted(step_dirs(hf_dir)):
|
| 72 |
+
if step in pushed:
|
| 73 |
+
continue
|
| 74 |
+
if args.every > 1 and (step // 100) % args.every != 0 and step != args.until_step:
|
| 75 |
+
continue
|
| 76 |
+
d = step_dirs(hf_dir)[step]
|
| 77 |
+
if not args.once and not is_stable(d, args.stable_sec):
|
| 78 |
+
continue # still being written; try next poll
|
| 79 |
+
print(f"[push] uploading step-{step} ...", flush=True)
|
| 80 |
+
try:
|
| 81 |
+
api.upload_folder(folder_path=str(d), repo_id=args.repo, repo_type="model",
|
| 82 |
+
path_in_repo=f"step-{step}", commit_message=f"add step-{step}")
|
| 83 |
+
pushed.add(step)
|
| 84 |
+
marker.write_text(json.dumps(sorted(pushed)))
|
| 85 |
+
print(f"[push] DONE step-{step} -> {args.repo}/step-{step}", flush=True)
|
| 86 |
+
except Exception as e: # network / rate-limit / partial — retry next poll
|
| 87 |
+
print(f"[push] FAILED step-{step}: {type(e).__name__}: {e} (retry next poll)", flush=True)
|
| 88 |
+
if args.once:
|
| 89 |
+
print("[push] --once sweep complete; exiting.", flush=True)
|
| 90 |
+
return
|
| 91 |
+
if args.until_step and args.until_step in pushed:
|
| 92 |
+
print(f"[push] final step-{args.until_step} pushed; exiting.", flush=True)
|
| 93 |
+
return
|
| 94 |
+
time.sleep(args.poll)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
main()
|
gpu-sft/scripts/gpu_sft/run_sft.sh
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Copyright The Marin Authors
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
#
|
| 5 |
+
# Self-healing launcher for train_sft_qwen3.py on ONE 8xH100 node.
|
| 6 |
+
#
|
| 7 |
+
# bash run_sft.sh <exp_name> [num_train_steps]
|
| 8 |
+
#
|
| 9 |
+
# Loops torchrun: on a transient crash (e.g. an NCCL watchdog collective timeout)
|
| 10 |
+
# it waits 30s and relaunches with --resume auto (continues from checkpoints-temp)
|
| 11 |
+
# and a FIXED --wandb-id (continues the SAME W&B run instead of forking a new one).
|
| 12 |
+
# Exits 0 when training reaches the step target. Bails out if it fast-fails
|
| 13 |
+
# repeatedly -- that means a real error (bad config / OOM / disk), not a blip.
|
| 14 |
+
#
|
| 15 |
+
# Data must already be prepared at $DATA_ROOT/<exp_name> by prepare_sft_data.py.
|
| 16 |
+
# For a FRESH run, delete $RUNS_ROOT/<exp_name> first (else --resume auto continues
|
| 17 |
+
# the old checkpoints). Launch under nohup so it survives your shell:
|
| 18 |
+
#
|
| 19 |
+
# nohup bash gpu-sft/scripts/gpu_sft/run_sft.sh science_depth4v3_nofilter \
|
| 20 |
+
# > /opt/tiger/lmc_muon/gpu-sft-runs/science_depth4v3_nofilter/wrapper.log 2>&1 &
|
| 21 |
+
#
|
| 22 |
+
# NOTE: no `set -e` on purpose -- the whole point is to survive torchrun failures.
|
| 23 |
+
set -uo pipefail
|
| 24 |
+
|
| 25 |
+
EXP="${1:?usage: run_sft.sh <exp_name> [num_train_steps]}"
|
| 26 |
+
NUM_STEPS="${2:-2000}"
|
| 27 |
+
NPROC="${NPROC:-8}"
|
| 28 |
+
|
| 29 |
+
DATA_ROOT="${DATA_ROOT:-/opt/tiger/lmc_muon/gpu-sft-data}"
|
| 30 |
+
RUNS_ROOT="${RUNS_ROOT:-/opt/tiger/lmc_muon/gpu-sft-runs}"
|
| 31 |
+
REPO="${REPO:-/opt/tiger/lmc_muon/self-verified-distillation}"
|
| 32 |
+
WANDB_PROJECT="${WANDB_PROJECT:-marin-instill-gpu}"
|
| 33 |
+
WANDB_ID="${WANDB_ID:-$EXP}" # override to FORCE a fresh W&B run id (avoids reattaching to a prior partial run of the same name)
|
| 34 |
+
|
| 35 |
+
# Durability: async HDFS mirror of hf/ exports + native checkpoints/. Local NVMe stays the
|
| 36 |
+
# training --out (fast, no FUSE I/O stall that could trip the NCCL watchdog); this only
|
| 37 |
+
# copies in the background so a machine kill can't erase progress. Set MIRROR=0 to disable.
|
| 38 |
+
MIRROR="${MIRROR:-1}"
|
| 39 |
+
HDFS_ROOT="${HDFS_ROOT:-/mnt/hdfs/fangzhao_writable/marin-sft}"
|
| 40 |
+
MIRROR_EVERY="${MIRROR_EVERY:-300}"
|
| 41 |
+
|
| 42 |
+
DATA="$DATA_ROOT/$EXP"
|
| 43 |
+
OUT="$RUNS_ROOT/$EXP"
|
| 44 |
+
LOG="$OUT/train.log"
|
| 45 |
+
|
| 46 |
+
MAX_RESTARTS="${MAX_RESTARTS:-30}" # absolute cap on relaunches
|
| 47 |
+
FAST_FAIL_SEC="${FAST_FAIL_SEC:-180}" # a run dying sooner than this = a hard fail
|
| 48 |
+
MAX_FAST_FAILS="${MAX_FAST_FAILS:-3}" # bail after this many CONSECUTIVE fast fails
|
| 49 |
+
|
| 50 |
+
export HF_HOME="${HF_HOME:-/opt/tiger/lmc_muon/hf_cache}"
|
| 51 |
+
export TOKENIZERS_PARALLELISM=false
|
| 52 |
+
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
|
| 53 |
+
export WANDB_MODE="${WANDB_MODE:-online}"
|
| 54 |
+
export NCCL_DEBUG="${NCCL_DEBUG:-WARN}"
|
| 55 |
+
|
| 56 |
+
if [[ ! -d "$DATA" ]]; then
|
| 57 |
+
echo "[run_sft] ERROR: prepared data not found at $DATA -- run prepare_sft_data.py first" >&2
|
| 58 |
+
exit 2
|
| 59 |
+
fi
|
| 60 |
+
mkdir -p "$OUT"
|
| 61 |
+
cd "$REPO" || { echo "[run_sft] ERROR: cannot cd $REPO" >&2; exit 2; }
|
| 62 |
+
|
| 63 |
+
echo "[run_sft] exp=$EXP steps=$NUM_STEPS nproc=$NPROC wandb-id=$WANDB_ID out=$OUT" | tee -a "$LOG"
|
| 64 |
+
|
| 65 |
+
# --- durability: background HDFS mirror (hf/ exports + native checkpoints/; skip the
|
| 66 |
+
# rolling checkpoints-temp/ which is overwritten and not rsync-snapshot-safe) ----------
|
| 67 |
+
DEST="$HDFS_ROOT/$EXP"
|
| 68 |
+
mirror_once() { rsync -a --exclude 'checkpoints-temp' "$OUT/" "$DEST/" 2>>"$OUT/mirror.log"; }
|
| 69 |
+
mirror_pid=""; push_pid=""
|
| 70 |
+
if [ "$MIRROR" = "1" ] && [ -d "$(dirname "$HDFS_ROOT")" ]; then
|
| 71 |
+
mkdir -p "$DEST"
|
| 72 |
+
echo "[run_sft] HDFS mirror ON: $OUT -> $DEST every ${MIRROR_EVERY}s" | tee -a "$LOG"
|
| 73 |
+
( while true; do sleep "$MIRROR_EVERY"; mirror_once; done ) &
|
| 74 |
+
mirror_pid=$!
|
| 75 |
+
elif [ -n "${PUSH_HF_REPO:-}" ]; then
|
| 76 |
+
# No HDFS (e.g. A800 boxes): push each hf/step-N to a private HF repo instead.
|
| 77 |
+
echo "[run_sft] no HDFS -> HF push: hf/step-N -> $PUSH_HF_REPO" | tee -a "$LOG"
|
| 78 |
+
nohup python gpu-sft/scripts/gpu_sft/push_exports.py --out "$OUT" --repo "$PUSH_HF_REPO" >> "$OUT/push.log" 2>&1 &
|
| 79 |
+
push_pid=$!
|
| 80 |
+
else
|
| 81 |
+
echo "[run_sft] WARNING: NO durability -- no HDFS mount and no PUSH_HF_REPO; checkpoints on ephemeral disk only!" | tee -a "$LOG"
|
| 82 |
+
fi
|
| 83 |
+
trap '[ -n "${mirror_pid:-}" ] && kill "$mirror_pid" 2>/dev/null; [ -n "${push_pid:-}" ] && kill "$push_pid" 2>/dev/null' EXIT
|
| 84 |
+
|
| 85 |
+
fast_fails=0
|
| 86 |
+
for attempt in $(seq 1 "$MAX_RESTARTS"); do
|
| 87 |
+
echo "[run_sft] === attempt $attempt/$MAX_RESTARTS $(date -u) ===" | tee -a "$LOG"
|
| 88 |
+
start=$(date +%s)
|
| 89 |
+
|
| 90 |
+
torchrun --standalone --nproc_per_node="$NPROC" \
|
| 91 |
+
gpu-sft/scripts/gpu_sft/train_sft_qwen3.py \
|
| 92 |
+
--data "$DATA" --out "$OUT" --recipe qwen3-8b \
|
| 93 |
+
--train-batch-size 64 --per-device-batch 1 --loss-groups 2 \
|
| 94 |
+
--num-train-steps "$NUM_STEPS" \
|
| 95 |
+
--steps-per-hf-export 100 --steps-per-checkpoint 500 \
|
| 96 |
+
--loss-chunk-size 2048 --hf-export-dtype bfloat16 \
|
| 97 |
+
--init-mode all_ranks --resume auto \
|
| 98 |
+
--wandb-project "$WANDB_PROJECT" --wandb-name "$EXP" --wandb-id "$WANDB_ID" \
|
| 99 |
+
--log-every 1 >> "$LOG" 2>&1
|
| 100 |
+
rc=$?
|
| 101 |
+
dur=$(( $(date +%s) - start ))
|
| 102 |
+
|
| 103 |
+
if [[ $rc -eq 0 ]]; then
|
| 104 |
+
echo "[run_sft] SUCCESS: $EXP finished (rc=0) after $attempt attempt(s)" | tee -a "$LOG"
|
| 105 |
+
if [ -n "$mirror_pid" ]; then echo "[run_sft] final HDFS mirror..." | tee -a "$LOG"; mirror_once; fi
|
| 106 |
+
if [ -n "$push_pid" ]; then
|
| 107 |
+
echo "[run_sft] final HF push..." | tee -a "$LOG"
|
| 108 |
+
kill "$push_pid" 2>/dev/null
|
| 109 |
+
python gpu-sft/scripts/gpu_sft/push_exports.py --out "$OUT" --repo "$PUSH_HF_REPO" --once >> "$OUT/push.log" 2>&1
|
| 110 |
+
fi
|
| 111 |
+
exit 0
|
| 112 |
+
fi
|
| 113 |
+
|
| 114 |
+
echo "[run_sft] exit rc=$rc after ${dur}s" | tee -a "$LOG"
|
| 115 |
+
if [[ $dur -lt $FAST_FAIL_SEC ]]; then
|
| 116 |
+
fast_fails=$((fast_fails + 1))
|
| 117 |
+
echo "[run_sft] fast-fail $fast_fails/$MAX_FAST_FAILS (died <${FAST_FAIL_SEC}s -> likely a real error, not transient)" | tee -a "$LOG"
|
| 118 |
+
if [[ $fast_fails -ge $MAX_FAST_FAILS ]]; then
|
| 119 |
+
echo "[run_sft] ABORT: $MAX_FAST_FAILS consecutive fast fails -- inspect $LOG (config/OOM/disk, not a blip)" | tee -a "$LOG"
|
| 120 |
+
exit 1
|
| 121 |
+
fi
|
| 122 |
+
else
|
| 123 |
+
fast_fails=0 # ran a while then died -> treat as transient, reset the counter
|
| 124 |
+
fi
|
| 125 |
+
|
| 126 |
+
echo "[run_sft] relaunching in 30s (will --resume auto from checkpoints-temp)..." | tee -a "$LOG"
|
| 127 |
+
sleep 30
|
| 128 |
+
done
|
| 129 |
+
|
| 130 |
+
echo "[run_sft] ABORT: hit MAX_RESTARTS=$MAX_RESTARTS without finishing" | tee -a "$LOG"
|
| 131 |
+
exit 1
|
gpu-sft/scripts/gpu_sft/train_sft_qwen3.py
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# ruff: noqa: E501
|
| 3 |
+
"""Stage 2: single-node/multi-node NVIDIA GPU port of the Marin/Levanter Qwen3 SFT recipe.
|
| 4 |
+
|
| 5 |
+
PyTorch + HF transformers + torch FSDP. No JAX, no GCS, no Iris, no marin credentials.
|
| 6 |
+
Reads the packed dataset produced by prepare_sft_data.py, writes native (resumable)
|
| 7 |
+
checkpoints and HF-format exports to local disk on the same cadence as the TPU run, so
|
| 8 |
+
the existing evalchemy tooling can consume `<out>/hf/step-N` unchanged.
|
| 9 |
+
|
| 10 |
+
Launch (8 GPUs, one node):
|
| 11 |
+
torchrun --standalone --nproc_per_node=8 train_sft_qwen3.py --data /data/sft/foo --out /data/runs/foo
|
| 12 |
+
|
| 13 |
+
Plan only, no GPUs needed:
|
| 14 |
+
python train_sft_qwen3.py --data /data/sft/foo --out /tmp/x --dry-run
|
| 15 |
+
|
| 16 |
+
WHAT IS REPRODUCED EXACTLY
|
| 17 |
+
--------------------------
|
| 18 |
+
* optimiser chain order: global-norm clip -> Adam(b1,b2,eps) -> decoupled wd -> scale(-lr)
|
| 19 |
+
* weight-decay mask: no decay on *norm*, embeddings and biases; lm_head IS decayed
|
| 20 |
+
* LR: linear warmup 0->lr over int(warmup*T) steps, constant, then cosine to lr*min_lr_ratio
|
| 21 |
+
over int(decay*T) steps (clamped). LR at optimiser step 0 is exactly 0.0.
|
| 22 |
+
* mixed precision: fp32 master params / bf16 compute / fp32 gradient reduction
|
| 23 |
+
* loss: per-microbatch token-weighted mean over assistant tokens, then arithmetic mean
|
| 24 |
+
across the `--loss-groups` microbatches of an optimiser step (levanter grad_accum uses
|
| 25 |
+
ReductionType.MEAN over 2 microbatches of 32 on v5p-64). No z-loss.
|
| 26 |
+
* RoPE positions are CONTIGUOUS 0..L-1 across a packed example (levanter passes
|
| 27 |
+
`pos_ids = arange(Pos)`; it does NOT reset per document), while attention is still
|
| 28 |
+
blocked across documents with varlen cu_seq_lens. Most HF/TRL packing implementations
|
| 29 |
+
reset position_ids per document -- that is a real divergence and we do not do it.
|
| 30 |
+
* data order: sequential over packed examples with wraparound (shuffle is a silent no-op
|
| 31 |
+
in the TPU config), so `--data-seed` only matters if you pass --shuffle.
|
| 32 |
+
* checkpoint/export naming: levanter's StepInfo.step == completed_steps - 1, and hooks
|
| 33 |
+
fire when `step > 1 and step % every == 0`, plus a forced final hook. A 2000-step run
|
| 34 |
+
therefore writes hf/step-100 ... hf/step-1900 and a final hf/step-1999.
|
| 35 |
+
|
| 36 |
+
WHAT DIFFERS (and why) -- see also the handoff doc:
|
| 37 |
+
* per-device microbatch is 1 packed example (not 32-across-32-chips); grouping into
|
| 38 |
+
`--loss-groups` groups restores the exact loss normalisation, but the *gradient*
|
| 39 |
+
accumulation order differs, so results are not bitwise identical.
|
| 40 |
+
* attention kernel is FlashAttention-2 (varlen), not the TPU splash/Pallas kernel.
|
| 41 |
+
* cross-entropy is a chunked torch implementation, not levanter's fused Pallas CE.
|
| 42 |
+
* HF export defaults to bf16 here (the TPU export was fp32; vLLM casts to bf16 anyway).
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
from __future__ import annotations
|
| 46 |
+
|
| 47 |
+
import argparse
|
| 48 |
+
import contextlib
|
| 49 |
+
import json
|
| 50 |
+
import math
|
| 51 |
+
import os
|
| 52 |
+
import shutil
|
| 53 |
+
import time
|
| 54 |
+
from dataclasses import dataclass, replace
|
| 55 |
+
|
| 56 |
+
import numpy as np
|
| 57 |
+
|
| 58 |
+
MODEL_VOCAB_SIZE = 151936 # Qwen3 config.vocab_size for 1.7B / 4B / 8B / 32B
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# ----------------------------------------------------------------------------------
|
| 62 |
+
# recipes (frozen per model size, copied from the marin experiment files)
|
| 63 |
+
# ----------------------------------------------------------------------------------
|
| 64 |
+
@dataclass
|
| 65 |
+
class Recipe:
|
| 66 |
+
model_id: str
|
| 67 |
+
learning_rate: float
|
| 68 |
+
weight_decay: float
|
| 69 |
+
warmup: float
|
| 70 |
+
decay: float
|
| 71 |
+
min_lr_ratio: float
|
| 72 |
+
max_grad_norm: float
|
| 73 |
+
beta1: float
|
| 74 |
+
beta2: float
|
| 75 |
+
eps: float
|
| 76 |
+
train_batch_size: int
|
| 77 |
+
num_train_steps: int
|
| 78 |
+
max_seq_len: int
|
| 79 |
+
steps_per_checkpoint: int
|
| 80 |
+
steps_per_hf_export: int
|
| 81 |
+
lr_schedule: str = "cosine"
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
RECIPES: dict[str, Recipe] = {
|
| 85 |
+
# experiments/exp_sft_qwen3_8b_selfinstill_*_2k_lr5e6_wd01.py
|
| 86 |
+
"qwen3-8b": Recipe(
|
| 87 |
+
model_id="Qwen/Qwen3-8B",
|
| 88 |
+
learning_rate=5e-6,
|
| 89 |
+
weight_decay=0.01,
|
| 90 |
+
warmup=0.05,
|
| 91 |
+
decay=0.9,
|
| 92 |
+
min_lr_ratio=0.1,
|
| 93 |
+
max_grad_norm=1.0,
|
| 94 |
+
beta1=0.9,
|
| 95 |
+
beta2=0.999,
|
| 96 |
+
eps=1e-8,
|
| 97 |
+
train_batch_size=64,
|
| 98 |
+
num_train_steps=2000,
|
| 99 |
+
max_seq_len=32768,
|
| 100 |
+
steps_per_checkpoint=20,
|
| 101 |
+
steps_per_hf_export=100,
|
| 102 |
+
),
|
| 103 |
+
# experiments/exp_*_sft_qwen3_4b_* (OT4 recipe: higher LR, no wd, tight clip)
|
| 104 |
+
"qwen3-4b": Recipe(
|
| 105 |
+
model_id="Qwen/Qwen3-4B",
|
| 106 |
+
learning_rate=2e-5,
|
| 107 |
+
weight_decay=0.0,
|
| 108 |
+
warmup=0.03,
|
| 109 |
+
decay=0.9,
|
| 110 |
+
min_lr_ratio=0.1,
|
| 111 |
+
max_grad_norm=0.2,
|
| 112 |
+
beta1=0.9,
|
| 113 |
+
beta2=0.999,
|
| 114 |
+
eps=1e-8,
|
| 115 |
+
train_batch_size=128,
|
| 116 |
+
num_train_steps=0, # 0 => ceil(8 * n_packs / batch); override with --num-train-steps
|
| 117 |
+
max_seq_len=32768,
|
| 118 |
+
steps_per_checkpoint=20,
|
| 119 |
+
steps_per_hf_export=100,
|
| 120 |
+
),
|
| 121 |
+
"qwen3-1.7b": Recipe(
|
| 122 |
+
model_id="Qwen/Qwen3-1.7B",
|
| 123 |
+
learning_rate=2e-5,
|
| 124 |
+
weight_decay=0.0,
|
| 125 |
+
warmup=0.03,
|
| 126 |
+
decay=0.9,
|
| 127 |
+
min_lr_ratio=0.1,
|
| 128 |
+
max_grad_norm=0.2,
|
| 129 |
+
beta1=0.9,
|
| 130 |
+
beta2=0.999,
|
| 131 |
+
eps=1e-8,
|
| 132 |
+
train_batch_size=128,
|
| 133 |
+
num_train_steps=0,
|
| 134 |
+
max_seq_len=32768,
|
| 135 |
+
steps_per_checkpoint=20,
|
| 136 |
+
steps_per_hf_export=100,
|
| 137 |
+
),
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ----------------------------------------------------------------------------------
|
| 142 |
+
# LR schedule (port of levanter OptimizerConfig.lr_scheduler for a single cycle)
|
| 143 |
+
# ----------------------------------------------------------------------------------
|
| 144 |
+
def _frac_or_steps(v: float, total: int) -> int:
|
| 145 |
+
if v < 0.0 or (v > 1.0 and v % 1 != 0):
|
| 146 |
+
raise ValueError(f"Invalid fraction {v}")
|
| 147 |
+
return int(v * total) if v <= 1.0 else int(v)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def make_lr_multiplier(recipe: Recipe, num_train_steps: int):
|
| 151 |
+
warmup_steps = min(_frac_or_steps(recipe.warmup, num_train_steps), num_train_steps)
|
| 152 |
+
max_decay = max(num_train_steps - warmup_steps, 0)
|
| 153 |
+
decay_steps = min(max(_frac_or_steps(recipe.decay, num_train_steps), 0), max_decay)
|
| 154 |
+
stable_steps = num_train_steps - warmup_steps - decay_steps
|
| 155 |
+
alpha = recipe.min_lr_ratio
|
| 156 |
+
sched = recipe.lr_schedule
|
| 157 |
+
|
| 158 |
+
def fn(step: int) -> float:
|
| 159 |
+
if warmup_steps > 0 and step < warmup_steps:
|
| 160 |
+
return step / warmup_steps # optax.linear_schedule(0.0, lr, warmup_steps)
|
| 161 |
+
if step < warmup_steps + stable_steps:
|
| 162 |
+
return 1.0
|
| 163 |
+
if decay_steps == 0:
|
| 164 |
+
return 1.0
|
| 165 |
+
t = min(step - warmup_steps - stable_steps, decay_steps)
|
| 166 |
+
if sched == "cosine":
|
| 167 |
+
cos = 0.5 * (1.0 + math.cos(math.pi * t / decay_steps))
|
| 168 |
+
return (1.0 - alpha) * cos + alpha
|
| 169 |
+
if sched == "linear":
|
| 170 |
+
return 1.0 + (alpha - 1.0) * (t / decay_steps)
|
| 171 |
+
if sched == "constant":
|
| 172 |
+
return 1.0
|
| 173 |
+
raise ValueError(f"unsupported lr_schedule {sched}")
|
| 174 |
+
|
| 175 |
+
return fn, (warmup_steps, stable_steps, decay_steps)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
# ----------------------------------------------------------------------------------
|
| 179 |
+
# weight decay mask (port of AdamConfig.build_weight_decay_mask reasonable_default)
|
| 180 |
+
# ----------------------------------------------------------------------------------
|
| 181 |
+
def is_no_decay(name: str) -> bool:
|
| 182 |
+
"""levanter excludes LayerNorm/RMSNorm/RmsNorm/Embedding modules and *.bias.
|
| 183 |
+
|
| 184 |
+
lm_head is an ordinary Linear in levanter, so it IS decayed. With tied embeddings
|
| 185 |
+
(1.7B/4B) the single shared tensor is named model.embed_tokens.weight and is excluded,
|
| 186 |
+
which matches levanter (lm_head is None when tie_word_embeddings=True).
|
| 187 |
+
"""
|
| 188 |
+
if name.endswith(".bias"):
|
| 189 |
+
return True
|
| 190 |
+
if "norm" in name.lower(): # input_layernorm, post_attention_layernorm, model.norm, q_norm, k_norm
|
| 191 |
+
return True
|
| 192 |
+
if name.endswith("embed_tokens.weight"):
|
| 193 |
+
return True
|
| 194 |
+
return False
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# ----------------------------------------------------------------------------------
|
| 198 |
+
# packed dataset
|
| 199 |
+
# ----------------------------------------------------------------------------------
|
| 200 |
+
class PackedSFTData:
|
| 201 |
+
def __init__(self, path: str):
|
| 202 |
+
with open(os.path.join(path, "manifest.json")) as f:
|
| 203 |
+
self.manifest = json.load(f)
|
| 204 |
+
self.tokens = np.load(os.path.join(path, "tokens.npy"), mmap_mode="r")
|
| 205 |
+
self.mask = np.load(os.path.join(path, "assistant_mask.npy"), mmap_mode="r")
|
| 206 |
+
self.doc_lens = np.load(os.path.join(path, "doc_lens.npy"))
|
| 207 |
+
self.pack_offsets = np.load(os.path.join(path, "pack_offsets.npy"))
|
| 208 |
+
self.assistant_counts = np.load(os.path.join(path, "assistant_counts.npy"))
|
| 209 |
+
self.max_seq_len = int(self.manifest["max_seq_len"])
|
| 210 |
+
self.n_packs = int(self.tokens.shape[0])
|
| 211 |
+
assert self.tokens.shape[1] == self.max_seq_len
|
| 212 |
+
|
| 213 |
+
def doc_lengths(self, i: int) -> list[int]:
|
| 214 |
+
s, e = int(self.pack_offsets[i]), int(self.pack_offsets[i + 1])
|
| 215 |
+
return [int(x) for x in self.doc_lens[s:e]]
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def collate(data: PackedSFTData, indices, device):
|
| 219 |
+
"""Flatten `indices` packed examples into one padding-free row of B*L tokens.
|
| 220 |
+
|
| 221 |
+
Returns tensors shaped [1, B*L] plus varlen metadata. batch dim is always 1 so the
|
| 222 |
+
HF flash-attention path uses flash_attn_varlen_func with our explicit cu_seq_lens.
|
| 223 |
+
"""
|
| 224 |
+
import torch
|
| 225 |
+
|
| 226 |
+
L = data.max_seq_len
|
| 227 |
+
ids = np.concatenate([np.asarray(data.tokens[i], dtype=np.int64) for i in indices])
|
| 228 |
+
msk = np.concatenate([np.asarray(data.mask[i], dtype=np.int64) for i in indices])
|
| 229 |
+
labels = np.where(msk == 1, ids, -100)
|
| 230 |
+
for b in range(len(indices)):
|
| 231 |
+
labels[b * L] = -100 # levanter's not_last_mask: position L-1 of the PREVIOUS example
|
| 232 |
+
targets = np.empty_like(labels)
|
| 233 |
+
targets[:-1] = labels[1:]
|
| 234 |
+
targets[-1] = -100
|
| 235 |
+
|
| 236 |
+
seg_lens: list[int] = []
|
| 237 |
+
for i in indices:
|
| 238 |
+
dl = data.doc_lengths(i)
|
| 239 |
+
seg_lens.extend(dl)
|
| 240 |
+
pad = L - int(sum(dl))
|
| 241 |
+
if pad > 0:
|
| 242 |
+
seg_lens.append(pad) # padding is its own attention segment
|
| 243 |
+
cu = np.concatenate([[0], np.cumsum(np.asarray(seg_lens, dtype=np.int64))]).astype(np.int32)
|
| 244 |
+
|
| 245 |
+
pos = np.tile(np.arange(L, dtype=np.int64), len(indices)) # contiguous per example, like levanter
|
| 246 |
+
n_targets = int((targets != -100).sum())
|
| 247 |
+
return {
|
| 248 |
+
"input_ids": torch.from_numpy(ids).unsqueeze(0).to(device, non_blocking=True),
|
| 249 |
+
"position_ids": torch.from_numpy(pos).unsqueeze(0).to(device, non_blocking=True),
|
| 250 |
+
"targets": torch.from_numpy(targets).unsqueeze(0).to(device, non_blocking=True),
|
| 251 |
+
"cu_seq_lens": torch.from_numpy(cu).to(device, non_blocking=True),
|
| 252 |
+
"max_length": int(max(seg_lens)),
|
| 253 |
+
}, n_targets
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
# ----------------------------------------------------------------------------------
|
| 257 |
+
# model wrapper with chunked (memory-bounded) cross entropy
|
| 258 |
+
# ----------------------------------------------------------------------------------
|
| 259 |
+
def _build_sft_module():
|
| 260 |
+
import torch
|
| 261 |
+
import torch.nn as nn
|
| 262 |
+
import torch.nn.functional as F
|
| 263 |
+
from torch.utils.checkpoint import checkpoint
|
| 264 |
+
|
| 265 |
+
def _ce_chunk(hidden_chunk, target_chunk, lm_head):
|
| 266 |
+
logits = lm_head(hidden_chunk).float()
|
| 267 |
+
return F.cross_entropy(logits, target_chunk, ignore_index=-100, reduction="sum")
|
| 268 |
+
|
| 269 |
+
class SFTModule(nn.Module):
|
| 270 |
+
"""hf_model + a chunked lm_head/CE that never materialises [L, 151936] logits."""
|
| 271 |
+
|
| 272 |
+
def __init__(self, hf_model, loss_chunk_size: int, checkpoint_loss: bool = True):
|
| 273 |
+
super().__init__()
|
| 274 |
+
self.hf_model = hf_model
|
| 275 |
+
self.loss_chunk_size = loss_chunk_size
|
| 276 |
+
self.checkpoint_loss = checkpoint_loss
|
| 277 |
+
|
| 278 |
+
def forward(self, input_ids, position_ids, targets, cu_seq_lens, max_length, return_hidden: bool = False):
|
| 279 |
+
out = self.hf_model.model(
|
| 280 |
+
input_ids=input_ids,
|
| 281 |
+
position_ids=position_ids,
|
| 282 |
+
use_cache=False,
|
| 283 |
+
cu_seq_lens_q=cu_seq_lens,
|
| 284 |
+
cu_seq_lens_k=cu_seq_lens,
|
| 285 |
+
max_length_q=max_length,
|
| 286 |
+
max_length_k=max_length,
|
| 287 |
+
)
|
| 288 |
+
hidden = out.last_hidden_state[0]
|
| 289 |
+
if return_hidden:
|
| 290 |
+
return hidden
|
| 291 |
+
tgt = targets[0]
|
| 292 |
+
total = torch.zeros((), device=hidden.device, dtype=torch.float32)
|
| 293 |
+
n = hidden.shape[0]
|
| 294 |
+
cs = self.loss_chunk_size or n
|
| 295 |
+
for s in range(0, n, cs):
|
| 296 |
+
e = min(s + cs, n)
|
| 297 |
+
if self.checkpoint_loss and self.training:
|
| 298 |
+
part = checkpoint(_ce_chunk, hidden[s:e], tgt[s:e], self.hf_model.lm_head, use_reentrant=False)
|
| 299 |
+
else:
|
| 300 |
+
part = _ce_chunk(hidden[s:e], tgt[s:e], self.hf_model.lm_head)
|
| 301 |
+
total = total + part
|
| 302 |
+
return total
|
| 303 |
+
|
| 304 |
+
return SFTModule
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# ----------------------------------------------------------------------------------
|
| 308 |
+
# distributed helpers
|
| 309 |
+
# ----------------------------------------------------------------------------------
|
| 310 |
+
def dist_info():
|
| 311 |
+
return (
|
| 312 |
+
int(os.environ.get("RANK", 0)),
|
| 313 |
+
int(os.environ.get("LOCAL_RANK", 0)),
|
| 314 |
+
int(os.environ.get("WORLD_SIZE", 1)),
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def log0(rank: int, msg: str) -> None:
|
| 319 |
+
if rank == 0:
|
| 320 |
+
print(msg, flush=True)
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def fmt_hms(seconds: float) -> str:
|
| 324 |
+
seconds = max(int(seconds), 0)
|
| 325 |
+
h, rem = divmod(seconds, 3600)
|
| 326 |
+
m, s = divmod(rem, 60)
|
| 327 |
+
return f"{h}:{m:02d}:{s:02d}"
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def detect_grad_reduce_factor(device, world_size: int, rank: int) -> float:
|
| 331 |
+
"""FSDP averages gradients over the DP group; loss scales must undo that.
|
| 332 |
+
|
| 333 |
+
Empirically determined with a 1-layer probe so a torch behaviour change cannot
|
| 334 |
+
silently rescale the effective learning rate by world_size.
|
| 335 |
+
"""
|
| 336 |
+
import torch
|
| 337 |
+
import torch.nn as nn
|
| 338 |
+
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
|
| 339 |
+
from torch.distributed.fsdp import ShardingStrategy
|
| 340 |
+
|
| 341 |
+
if world_size == 1:
|
| 342 |
+
return 1.0
|
| 343 |
+
lin = nn.Linear(64, 64, bias=False)
|
| 344 |
+
with torch.no_grad():
|
| 345 |
+
lin.weight.zero_()
|
| 346 |
+
probe = FSDP(
|
| 347 |
+
lin,
|
| 348 |
+
sharding_strategy=ShardingStrategy.FULL_SHARD,
|
| 349 |
+
device_id=device,
|
| 350 |
+
use_orig_params=True,
|
| 351 |
+
)
|
| 352 |
+
x = torch.ones(1, 64, device=device)
|
| 353 |
+
probe(x).sum().backward()
|
| 354 |
+
with FSDP.summon_full_params(probe, with_grads=True, writeback=False):
|
| 355 |
+
observed = float(lin.weight.grad.detach().float().mean().item())
|
| 356 |
+
del probe, lin
|
| 357 |
+
if abs(observed - 1.0) < 1e-3:
|
| 358 |
+
log0(rank, "[init] FSDP averages gradients across ranks -> loss scale multiplied by world_size")
|
| 359 |
+
return float(world_size)
|
| 360 |
+
if abs(observed - world_size) < 1e-3:
|
| 361 |
+
log0(rank, "[init] FSDP sums gradients across ranks -> no loss rescale")
|
| 362 |
+
return 1.0
|
| 363 |
+
raise RuntimeError(f"Unexpected FSDP gradient reduction: mean grad {observed}, world_size {world_size}")
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def verify_document_isolation(model, device, rank: int) -> None:
|
| 367 |
+
"""Prove cross-document attention is actually blocked before burning GPU-days.
|
| 368 |
+
|
| 369 |
+
If transformers stops forwarding cu_seq_lens_* to the attention kernel this test
|
| 370 |
+
fails loudly instead of silently training with cross-document leakage.
|
| 371 |
+
"""
|
| 372 |
+
import torch
|
| 373 |
+
|
| 374 |
+
L, d0 = 64, 32
|
| 375 |
+
ids = torch.randint(0, 1000, (1, L), device=device)
|
| 376 |
+
pos = torch.arange(L, device=device).unsqueeze(0)
|
| 377 |
+
cu = torch.tensor([0, d0, L], dtype=torch.int32, device=device)
|
| 378 |
+
tgt = torch.full((1, L), -100, dtype=torch.long, device=device)
|
| 379 |
+
was_training = model.training
|
| 380 |
+
model.eval()
|
| 381 |
+
with torch.no_grad():
|
| 382 |
+
h1 = model(ids, pos, tgt, cu, d0, return_hidden=True)[:d0].clone()
|
| 383 |
+
ids2 = ids.clone()
|
| 384 |
+
ids2[0, d0:] = torch.randint(0, 1000, (L - d0,), device=device)
|
| 385 |
+
h2 = model(ids2, pos, tgt, cu, d0, return_hidden=True)[:d0].clone()
|
| 386 |
+
if was_training:
|
| 387 |
+
model.train()
|
| 388 |
+
diff = (h1 - h2).abs().max().item()
|
| 389 |
+
if diff != 0.0:
|
| 390 |
+
raise RuntimeError(
|
| 391 |
+
"Cross-document attention is NOT blocked (max hidden-state delta "
|
| 392 |
+
f"{diff:.3e} after perturbing the second document). The varlen cu_seq_lens "
|
| 393 |
+
"kwargs are not reaching flash-attention. Check the transformers version "
|
| 394 |
+
"and that attn_implementation='flash_attention_2'."
|
| 395 |
+
)
|
| 396 |
+
log0(rank, "[init] cross-document attention isolation verified (delta == 0)")
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
# ----------------------------------------------------------------------------------
|
| 400 |
+
def build_model(args, recipe: Recipe, rank: int, local_rank: int, world_size: int):
|
| 401 |
+
import torch
|
| 402 |
+
from torch.distributed.fsdp import BackwardPrefetch, MixedPrecision, ShardingStrategy
|
| 403 |
+
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
|
| 404 |
+
from torch.distributed.fsdp.wrap import ModuleWrapPolicy
|
| 405 |
+
from transformers import AutoConfig, AutoModelForCausalLM
|
| 406 |
+
|
| 407 |
+
config = AutoConfig.from_pretrained(recipe.model_id)
|
| 408 |
+
config.use_cache = False
|
| 409 |
+
if config.vocab_size != MODEL_VOCAB_SIZE:
|
| 410 |
+
log0(rank, f"[init] WARNING model vocab_size={config.vocab_size}, expected {MODEL_VOCAB_SIZE}")
|
| 411 |
+
|
| 412 |
+
if args.init_mode == "rank0_meta" and world_size > 1:
|
| 413 |
+
if rank == 0:
|
| 414 |
+
hf_model = AutoModelForCausalLM.from_pretrained(
|
| 415 |
+
recipe.model_id, dtype=torch.float32, attn_implementation=args.attn_impl, low_cpu_mem_usage=True
|
| 416 |
+
)
|
| 417 |
+
else:
|
| 418 |
+
with torch.device("meta"):
|
| 419 |
+
hf_model = AutoModelForCausalLM.from_config(config, attn_implementation=args.attn_impl)
|
| 420 |
+
else:
|
| 421 |
+
hf_model = AutoModelForCausalLM.from_pretrained(
|
| 422 |
+
recipe.model_id, dtype=torch.float32, attn_implementation=args.attn_impl, low_cpu_mem_usage=True
|
| 423 |
+
)
|
| 424 |
+
hf_model.config.use_cache = False
|
| 425 |
+
hf_model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
|
| 426 |
+
|
| 427 |
+
sft = _build_sft_module()(hf_model, args.loss_chunk_size, checkpoint_loss=not args.no_loss_checkpoint)
|
| 428 |
+
|
| 429 |
+
layer_cls = type(hf_model.model.layers[0])
|
| 430 |
+
wrap_classes = {layer_cls}
|
| 431 |
+
if args.wrap_embeddings:
|
| 432 |
+
wrap_classes.add(torch.nn.Embedding)
|
| 433 |
+
|
| 434 |
+
mp = MixedPrecision(
|
| 435 |
+
param_dtype=torch.bfloat16,
|
| 436 |
+
reduce_dtype=torch.float32 if args.grad_reduce_dtype == "float32" else torch.bfloat16,
|
| 437 |
+
buffer_dtype=torch.float32,
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
def param_init_fn(module):
|
| 441 |
+
module.to_empty(device=torch.device("cuda", local_rank), recurse=False)
|
| 442 |
+
|
| 443 |
+
model = FSDP(
|
| 444 |
+
sft,
|
| 445 |
+
auto_wrap_policy=ModuleWrapPolicy(wrap_classes),
|
| 446 |
+
mixed_precision=mp,
|
| 447 |
+
sharding_strategy=ShardingStrategy.FULL_SHARD,
|
| 448 |
+
backward_prefetch=BackwardPrefetch.BACKWARD_PRE,
|
| 449 |
+
device_id=torch.device("cuda", local_rank),
|
| 450 |
+
use_orig_params=True,
|
| 451 |
+
limit_all_gathers=True,
|
| 452 |
+
sync_module_states=(args.init_mode == "rank0_meta" and world_size > 1),
|
| 453 |
+
param_init_fn=param_init_fn if (args.init_mode == "rank0_meta" and world_size > 1 and rank != 0) else None,
|
| 454 |
+
)
|
| 455 |
+
return model, hf_model
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def build_optimizer(model, recipe: Recipe, args):
|
| 459 |
+
import torch
|
| 460 |
+
|
| 461 |
+
decay, no_decay = [], []
|
| 462 |
+
dn, nn_ = [], []
|
| 463 |
+
for name, p in model.named_parameters():
|
| 464 |
+
if not p.requires_grad:
|
| 465 |
+
continue
|
| 466 |
+
clean = name.replace("_fsdp_wrapped_module.", "").replace("hf_model.", "")
|
| 467 |
+
(no_decay if is_no_decay(clean) else decay).append(p)
|
| 468 |
+
(nn_ if is_no_decay(clean) else dn).append(clean)
|
| 469 |
+
groups = [
|
| 470 |
+
{"params": decay, "weight_decay": recipe.weight_decay},
|
| 471 |
+
{"params": no_decay, "weight_decay": 0.0},
|
| 472 |
+
]
|
| 473 |
+
opt = torch.optim.AdamW(
|
| 474 |
+
groups,
|
| 475 |
+
lr=recipe.learning_rate,
|
| 476 |
+
betas=(recipe.beta1, recipe.beta2),
|
| 477 |
+
eps=recipe.eps,
|
| 478 |
+
fused=args.fused_adam,
|
| 479 |
+
foreach=not args.fused_adam,
|
| 480 |
+
)
|
| 481 |
+
return opt, dn, nn_
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
# ----------------------------------------------------------------------------------
|
| 485 |
+
# checkpoint / export
|
| 486 |
+
# ----------------------------------------------------------------------------------
|
| 487 |
+
def save_native(model, optimizer, step: int, path: str, extra: dict) -> None:
|
| 488 |
+
import torch.distributed as dist
|
| 489 |
+
import torch.distributed.checkpoint as dcp
|
| 490 |
+
from torch.distributed.checkpoint.state_dict import get_state_dict
|
| 491 |
+
|
| 492 |
+
msd, osd = get_state_dict(model, optimizer)
|
| 493 |
+
tmp = path + ".partial"
|
| 494 |
+
dcp.save({"model": msd, "optim": osd}, checkpoint_id=tmp)
|
| 495 |
+
dist.barrier()
|
| 496 |
+
if dist.get_rank() == 0:
|
| 497 |
+
with open(os.path.join(tmp, "meta.json"), "w") as f:
|
| 498 |
+
json.dump({"step": step, **extra}, f)
|
| 499 |
+
if os.path.exists(path):
|
| 500 |
+
shutil.rmtree(path)
|
| 501 |
+
os.replace(tmp, path)
|
| 502 |
+
dist.barrier()
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
def load_native(model, optimizer, path: str) -> int:
|
| 506 |
+
import torch.distributed.checkpoint as dcp
|
| 507 |
+
from torch.distributed.checkpoint.state_dict import get_state_dict, set_state_dict
|
| 508 |
+
|
| 509 |
+
msd, osd = get_state_dict(model, optimizer)
|
| 510 |
+
state = {"model": msd, "optim": osd}
|
| 511 |
+
dcp.load(state, checkpoint_id=path)
|
| 512 |
+
set_state_dict(model, optimizer, model_state_dict=state["model"], optim_state_dict=state["optim"])
|
| 513 |
+
with open(os.path.join(path, "meta.json")) as f:
|
| 514 |
+
return int(json.load(f)["step"])
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def export_hf(model, hf_model, tokenizer, out_dir: str, label: int, dtype_name: str, keep_gen_cfg: bool) -> None:
|
| 518 |
+
import torch
|
| 519 |
+
import torch.distributed as dist
|
| 520 |
+
from torch.distributed.fsdp import FullStateDictConfig, StateDictType
|
| 521 |
+
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
|
| 522 |
+
|
| 523 |
+
cfg = FullStateDictConfig(offload_to_cpu=True, rank0_only=True)
|
| 524 |
+
with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT, cfg):
|
| 525 |
+
sd = model.state_dict()
|
| 526 |
+
if dist.get_rank() == 0:
|
| 527 |
+
dtype = {"bfloat16": torch.bfloat16, "float32": torch.float32, "float16": torch.float16}[dtype_name]
|
| 528 |
+
clean = {}
|
| 529 |
+
for k, v in sd.items():
|
| 530 |
+
k = k[len("hf_model.") :] if k.startswith("hf_model.") else k
|
| 531 |
+
clean[k] = v.to(dtype)
|
| 532 |
+
target = os.path.join(out_dir, "hf", f"step-{label}")
|
| 533 |
+
tmp = target + ".partial"
|
| 534 |
+
if os.path.exists(tmp):
|
| 535 |
+
shutil.rmtree(tmp)
|
| 536 |
+
os.makedirs(tmp, exist_ok=True)
|
| 537 |
+
hf_model.save_pretrained(tmp, state_dict=clean, safe_serialization=True, max_shard_size="5GB")
|
| 538 |
+
tokenizer.save_pretrained(tmp)
|
| 539 |
+
cfg_path = os.path.join(tmp, "config.json")
|
| 540 |
+
with open(cfg_path) as f:
|
| 541 |
+
cfg_json = json.load(f)
|
| 542 |
+
cfg_json["torch_dtype"] = dtype_name # vLLM reads this; keep it honest
|
| 543 |
+
cfg_json["dtype"] = dtype_name
|
| 544 |
+
with open(cfg_path, "w") as f:
|
| 545 |
+
json.dump(cfg_json, f, indent=2)
|
| 546 |
+
gen_cfg = os.path.join(tmp, "generation_config.json")
|
| 547 |
+
if not keep_gen_cfg and os.path.exists(gen_cfg):
|
| 548 |
+
os.remove(gen_cfg) # the levanter export writes no generation_config.json
|
| 549 |
+
if os.path.exists(target):
|
| 550 |
+
shutil.rmtree(target)
|
| 551 |
+
os.replace(tmp, target)
|
| 552 |
+
print(f"[export] wrote {target}", flush=True)
|
| 553 |
+
del sd
|
| 554 |
+
dist.barrier()
|
| 555 |
+
|
| 556 |
+
|
| 557 |
+
def export_tokenizer(recipe: Recipe):
|
| 558 |
+
"""267 <|padding_i|> tokens so len(tokenizer) == model vocab, exactly like
|
| 559 |
+
HFCheckpointConverter.with_tokenizer_padded_to_match_model()."""
|
| 560 |
+
from transformers import AutoTokenizer
|
| 561 |
+
|
| 562 |
+
tok = AutoTokenizer.from_pretrained(recipe.model_id)
|
| 563 |
+
missing = MODEL_VOCAB_SIZE - len(tok)
|
| 564 |
+
if missing > 0:
|
| 565 |
+
tok.add_tokens([f"<|padding_{i}|>" for i in range(missing)])
|
| 566 |
+
return tok, missing
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
# ----------------------------------------------------------------------------------
|
| 570 |
+
def dry_run(args, recipe: Recipe, data: PackedSFTData, num_train_steps: int) -> None:
|
| 571 |
+
lr_fn, (w, s, d) = make_lr_multiplier(recipe, num_train_steps)
|
| 572 |
+
L = recipe.max_seq_len
|
| 573 |
+
tokens_per_step = recipe.train_batch_size * L
|
| 574 |
+
print("=" * 78)
|
| 575 |
+
print(f"model {recipe.model_id}")
|
| 576 |
+
print(f"packed examples {data.n_packs} (seq len {L})")
|
| 577 |
+
print(f"global batch {recipe.train_batch_size} examples = {tokens_per_step:,} tokens/step")
|
| 578 |
+
print(f"steps {num_train_steps} => {tokens_per_step * num_train_steps:,} tokens presented")
|
| 579 |
+
print(f"epochs over the data {num_train_steps * recipe.train_batch_size / data.n_packs:.2f}")
|
| 580 |
+
print(f"lr schedule warmup {w} / stable {s} / cosine {d} to {recipe.learning_rate * recipe.min_lr_ratio:.3e}")
|
| 581 |
+
for st in [0, 1, w - 1, w, w + s - 1, w + s, num_train_steps // 2, num_train_steps - 1]:
|
| 582 |
+
if 0 <= st < num_train_steps:
|
| 583 |
+
print(f" step {st:>5} lr = {recipe.learning_rate * lr_fn(st):.6e}")
|
| 584 |
+
print(f"weight decay {recipe.weight_decay} (excluded: *norm*, embed_tokens, *.bias; lm_head decayed)")
|
| 585 |
+
print(f"grad clip / betas {recipe.max_grad_norm} / ({recipe.beta1}, {recipe.beta2}) eps {recipe.eps}")
|
| 586 |
+
print(f"loss groups per step {args.loss_groups} (TPU used 2 microbatches of 32)")
|
| 587 |
+
print(f"assistant tokens {int(data.assistant_counts.sum()):,} over the dataset")
|
| 588 |
+
print(f"exports hf/step-N every {recipe.steps_per_hf_export}, native every {recipe.steps_per_checkpoint}")
|
| 589 |
+
print("=" * 78)
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
# ----------------------------------------------------------------------------------
|
| 593 |
+
def main() -> None:
|
| 594 |
+
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 595 |
+
p.add_argument("--data", required=True, help="output dir of prepare_sft_data.py")
|
| 596 |
+
p.add_argument("--out", required=True, help="run dir; checkpoints/, checkpoints-temp/, hf/ live here")
|
| 597 |
+
p.add_argument("--recipe", default="qwen3-8b", choices=sorted(RECIPES))
|
| 598 |
+
p.add_argument("--model-id", default=None, help="override the recipe's HF model id")
|
| 599 |
+
p.add_argument("--num-train-steps", type=int, default=0)
|
| 600 |
+
p.add_argument("--train-batch-size", type=int, default=0)
|
| 601 |
+
p.add_argument("--per-device-batch", type=int, default=1, help="packed examples per GPU per micro-step")
|
| 602 |
+
p.add_argument("--loss-groups", type=int, default=2, help="microbatches per optimiser step; TPU used 2")
|
| 603 |
+
p.add_argument("--steps-per-checkpoint", type=int, default=0)
|
| 604 |
+
p.add_argument("--steps-per-hf-export", type=int, default=0)
|
| 605 |
+
p.add_argument("--temp-checkpoint-minutes", type=float, default=10.0)
|
| 606 |
+
p.add_argument("--loss-chunk-size", type=int, default=2048)
|
| 607 |
+
p.add_argument("--no-loss-checkpoint", action="store_true")
|
| 608 |
+
p.add_argument("--attn-impl", default="flash_attention_2")
|
| 609 |
+
p.add_argument("--grad-reduce-dtype", default="float32", choices=("float32", "bfloat16"))
|
| 610 |
+
p.add_argument("--init-mode", default="rank0_meta", choices=("rank0_meta", "all_ranks"))
|
| 611 |
+
p.add_argument("--wrap-embeddings", action="store_true")
|
| 612 |
+
p.add_argument("--fused-adam", action="store_true")
|
| 613 |
+
p.add_argument("--hf-export-dtype", default="bfloat16", choices=("bfloat16", "float32", "float16"))
|
| 614 |
+
p.add_argument("--keep-generation-config", action="store_true")
|
| 615 |
+
p.add_argument("--trainer-seed", type=int, default=0, help="levanter TrainerConfig.seed (stays 0 on TPU)")
|
| 616 |
+
p.add_argument("--data-seed", type=int, default=42, help="only used when --shuffle is set")
|
| 617 |
+
p.add_argument("--shuffle", action="store_true", help="NOT what the TPU run did; shuffle is a no-op there")
|
| 618 |
+
p.add_argument("--resume", default="auto", help="auto | none | <path>")
|
| 619 |
+
p.add_argument("--log-every", type=int, default=1)
|
| 620 |
+
p.add_argument("--skip-checks", action="store_true")
|
| 621 |
+
p.add_argument("--wandb-project", default=None)
|
| 622 |
+
p.add_argument("--wandb-name", default=None)
|
| 623 |
+
p.add_argument("--wandb-id", default=None,
|
| 624 |
+
help="fixed W&B run id; reused across restarts (with resume=allow) so an "
|
| 625 |
+
"auto-resumed run continues ONE W&B run instead of forking a new one")
|
| 626 |
+
p.add_argument("--dry-run", action="store_true")
|
| 627 |
+
args = p.parse_args()
|
| 628 |
+
|
| 629 |
+
recipe = RECIPES[args.recipe]
|
| 630 |
+
if args.model_id:
|
| 631 |
+
recipe = replace(recipe, model_id=args.model_id)
|
| 632 |
+
if args.train_batch_size:
|
| 633 |
+
recipe = replace(recipe, train_batch_size=args.train_batch_size)
|
| 634 |
+
if args.steps_per_checkpoint:
|
| 635 |
+
recipe = replace(recipe, steps_per_checkpoint=args.steps_per_checkpoint)
|
| 636 |
+
if args.steps_per_hf_export:
|
| 637 |
+
recipe = replace(recipe, steps_per_hf_export=args.steps_per_hf_export)
|
| 638 |
+
|
| 639 |
+
data = PackedSFTData(args.data)
|
| 640 |
+
recipe = replace(recipe, max_seq_len=data.max_seq_len)
|
| 641 |
+
num_train_steps = args.num_train_steps or recipe.num_train_steps
|
| 642 |
+
if not num_train_steps:
|
| 643 |
+
num_train_steps = math.ceil(8 * data.n_packs / recipe.train_batch_size)
|
| 644 |
+
|
| 645 |
+
if args.dry_run:
|
| 646 |
+
dry_run(args, recipe, data, num_train_steps)
|
| 647 |
+
return
|
| 648 |
+
|
| 649 |
+
import torch
|
| 650 |
+
import torch.distributed as dist
|
| 651 |
+
|
| 652 |
+
rank, local_rank, world_size = dist_info()
|
| 653 |
+
torch.cuda.set_device(local_rank)
|
| 654 |
+
dist.init_process_group("nccl")
|
| 655 |
+
device = torch.device("cuda", local_rank)
|
| 656 |
+
torch.manual_seed(args.trainer_seed)
|
| 657 |
+
np.random.seed(args.trainer_seed)
|
| 658 |
+
|
| 659 |
+
B = recipe.train_batch_size
|
| 660 |
+
G = args.loss_groups
|
| 661 |
+
per_group = B // G
|
| 662 |
+
micro_per_group = per_group // (world_size * args.per_device_batch)
|
| 663 |
+
if B % G or per_group % (world_size * args.per_device_batch):
|
| 664 |
+
raise ValueError(
|
| 665 |
+
f"train_batch_size={B} must divide by loss_groups={G} and then by "
|
| 666 |
+
f"world_size*per_device_batch={world_size * args.per_device_batch}"
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
if recipe.steps_per_hf_export <= 0 or recipe.steps_per_checkpoint <= 0:
|
| 670 |
+
raise ValueError("steps_per_hf_export and steps_per_checkpoint must be > 0")
|
| 671 |
+
log0(rank, f"[init] world_size={world_size} batch={B} groups={G} micro/group={micro_per_group} seq={recipe.max_seq_len}")
|
| 672 |
+
grad_factor = 1.0 if args.skip_checks else detect_grad_reduce_factor(device, world_size, rank)
|
| 673 |
+
|
| 674 |
+
model, hf_model = build_model(args, recipe, rank, local_rank, world_size)
|
| 675 |
+
if not args.skip_checks:
|
| 676 |
+
verify_document_isolation(model, device, rank)
|
| 677 |
+
optimizer, decayed, undecayed = build_optimizer(model, recipe, args)
|
| 678 |
+
log0(rank, f"[init] weight decay applied to {len(decayed)} tensors, excluded {len(undecayed)}")
|
| 679 |
+
log0(rank, f"[init] excluded sample: {undecayed[:4]} ... lm_head decayed: {any(n.endswith('lm_head.weight') for n in decayed)}")
|
| 680 |
+
|
| 681 |
+
lr_fn, (w_steps, s_steps, d_steps) = make_lr_multiplier(recipe, num_train_steps)
|
| 682 |
+
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lr_fn)
|
| 683 |
+
tokenizer, n_pad = export_tokenizer(recipe)
|
| 684 |
+
log0(rank, f"[init] export tokenizer padded with {n_pad} tokens -> len={len(tokenizer)}")
|
| 685 |
+
|
| 686 |
+
ckpt_dir = os.path.join(args.out, "checkpoints")
|
| 687 |
+
temp_dir = os.path.join(args.out, "checkpoints-temp")
|
| 688 |
+
if rank == 0:
|
| 689 |
+
os.makedirs(ckpt_dir, exist_ok=True)
|
| 690 |
+
os.makedirs(os.path.join(args.out, "hf"), exist_ok=True)
|
| 691 |
+
dist.barrier()
|
| 692 |
+
|
| 693 |
+
start_step = 0
|
| 694 |
+
resume_path = None
|
| 695 |
+
if args.resume == "auto":
|
| 696 |
+
cands = []
|
| 697 |
+
if os.path.exists(os.path.join(temp_dir, "meta.json")):
|
| 698 |
+
cands.append(temp_dir)
|
| 699 |
+
if os.path.isdir(ckpt_dir):
|
| 700 |
+
cands += [os.path.join(ckpt_dir, d) for d in os.listdir(ckpt_dir) if d.startswith("step-")]
|
| 701 |
+
cands = [c for c in cands if os.path.exists(os.path.join(c, "meta.json"))]
|
| 702 |
+
if cands:
|
| 703 |
+
resume_path = max(cands, key=lambda c: json.load(open(os.path.join(c, "meta.json")))["step"])
|
| 704 |
+
elif args.resume != "none":
|
| 705 |
+
resume_path = args.resume
|
| 706 |
+
if resume_path:
|
| 707 |
+
start_step = load_native(model, optimizer, resume_path)
|
| 708 |
+
for _ in range(start_step):
|
| 709 |
+
scheduler.step()
|
| 710 |
+
log0(rank, f"[init] resumed from {resume_path} at step {start_step}")
|
| 711 |
+
|
| 712 |
+
order = np.arange(data.n_packs)
|
| 713 |
+
if args.shuffle:
|
| 714 |
+
np.random.default_rng(args.data_seed).shuffle(order)
|
| 715 |
+
|
| 716 |
+
run = None
|
| 717 |
+
if args.wandb_project and rank == 0:
|
| 718 |
+
import wandb
|
| 719 |
+
|
| 720 |
+
run = wandb.init(
|
| 721 |
+
project=args.wandb_project,
|
| 722 |
+
name=args.wandb_name or os.path.basename(args.out),
|
| 723 |
+
id=args.wandb_id,
|
| 724 |
+
resume="allow" if args.wandb_id else None,
|
| 725 |
+
)
|
| 726 |
+
run.config.update({**vars(args), **recipe.__dict__, "num_train_steps": num_train_steps},
|
| 727 |
+
allow_val_change=True)
|
| 728 |
+
|
| 729 |
+
model.train()
|
| 730 |
+
t_start = time.time()
|
| 731 |
+
last_temp = time.time()
|
| 732 |
+
log0(rank, "First batch loaded, starting first train step (includes CUDA graph/kernel warmup)...")
|
| 733 |
+
|
| 734 |
+
for step in range(start_step, num_train_steps):
|
| 735 |
+
t_step = time.time()
|
| 736 |
+
base = step * B
|
| 737 |
+
batch_idx = [int(order[(base + i) % data.n_packs]) for i in range(B)]
|
| 738 |
+
optimizer.zero_grad(set_to_none=True)
|
| 739 |
+
|
| 740 |
+
group_loss_sums = torch.zeros(G, device=device, dtype=torch.float32)
|
| 741 |
+
group_tokens = torch.zeros(G, device=device, dtype=torch.float32)
|
| 742 |
+
for g in range(G):
|
| 743 |
+
gidx = batch_idx[g * per_group : (g + 1) * per_group]
|
| 744 |
+
denom = float(sum(int(data.assistant_counts[i]) for i in gidx))
|
| 745 |
+
if denom <= 0:
|
| 746 |
+
raise RuntimeError(f"group {g} of step {step} has zero assistant tokens")
|
| 747 |
+
scale = grad_factor / (G * denom)
|
| 748 |
+
for m in range(micro_per_group):
|
| 749 |
+
off = (m * world_size + rank) * args.per_device_batch
|
| 750 |
+
mine = gidx[off : off + args.per_device_batch]
|
| 751 |
+
batch, n_tok = collate(data, mine, device)
|
| 752 |
+
loss_sum = model(
|
| 753 |
+
batch["input_ids"],
|
| 754 |
+
batch["position_ids"],
|
| 755 |
+
batch["targets"],
|
| 756 |
+
batch["cu_seq_lens"],
|
| 757 |
+
batch["max_length"],
|
| 758 |
+
)
|
| 759 |
+
(loss_sum * scale).backward()
|
| 760 |
+
group_loss_sums[g] += loss_sum.detach()
|
| 761 |
+
group_tokens[g] += n_tok
|
| 762 |
+
|
| 763 |
+
dist.all_reduce(group_loss_sums)
|
| 764 |
+
dist.all_reduce(group_tokens)
|
| 765 |
+
if step == start_step:
|
| 766 |
+
expect = [float(sum(int(data.assistant_counts[i]) for i in batch_idx[g * per_group : (g + 1) * per_group])) for g in range(G)]
|
| 767 |
+
got = [float(x) for x in group_tokens.tolist()]
|
| 768 |
+
if any(abs(a - b) > 0.5 for a, b in zip(expect, got)):
|
| 769 |
+
raise RuntimeError(f"loss denominator mismatch: manifest={expect} observed={got}")
|
| 770 |
+
log0(rank, f"[init] loss denominators verified: {got}")
|
| 771 |
+
loss_value = float((group_loss_sums / group_tokens.clamp(min=1)).mean().item())
|
| 772 |
+
|
| 773 |
+
gnorm = model.clip_grad_norm_(recipe.max_grad_norm)
|
| 774 |
+
optimizer.step()
|
| 775 |
+
scheduler.step()
|
| 776 |
+
|
| 777 |
+
completed = step + 1
|
| 778 |
+
label = completed - 1 # levanter StepInfo.step
|
| 779 |
+
dt = time.time() - t_step
|
| 780 |
+
if rank == 0 and (completed % args.log_every == 0 or completed == num_train_steps):
|
| 781 |
+
done = completed - start_step
|
| 782 |
+
total = num_train_steps - start_step
|
| 783 |
+
rate = (time.time() - t_start) / max(done, 1)
|
| 784 |
+
print(
|
| 785 |
+
f"Progress on:train {completed}it/{num_train_steps / 1000:.2f}kit "
|
| 786 |
+
f"rate:{rate:.1f}s/it remaining:{fmt_hms(rate * (total - done))} "
|
| 787 |
+
f"elapsed:{fmt_hms(time.time() - t_start)} postfix:loss={loss_value:.3f}",
|
| 788 |
+
flush=True,
|
| 789 |
+
)
|
| 790 |
+
if run is not None:
|
| 791 |
+
run.log(
|
| 792 |
+
{
|
| 793 |
+
"train/loss": loss_value,
|
| 794 |
+
"train/lr": scheduler.get_last_lr()[0],
|
| 795 |
+
"train/grad_norm": float(gnorm),
|
| 796 |
+
"train/tokens": int(group_tokens.sum().item()),
|
| 797 |
+
"train/step_time": dt,
|
| 798 |
+
},
|
| 799 |
+
step=completed,
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
is_final = completed == num_train_steps
|
| 803 |
+
if (label > 1 and label % int(recipe.steps_per_hf_export) == 0) or is_final:
|
| 804 |
+
export_hf(model, hf_model, tokenizer, args.out, label, args.hf_export_dtype, args.keep_generation_config)
|
| 805 |
+
if (label > 1 and label % int(recipe.steps_per_checkpoint) == 0) or is_final:
|
| 806 |
+
save_native(model, optimizer, completed, os.path.join(ckpt_dir, f"step-{label}"), {"label": label})
|
| 807 |
+
elif (time.time() - last_temp) > args.temp_checkpoint_minutes * 60:
|
| 808 |
+
save_native(model, optimizer, completed, temp_dir, {"label": label})
|
| 809 |
+
last_temp = time.time()
|
| 810 |
+
|
| 811 |
+
log0(rank, f"[done] {num_train_steps} steps in {fmt_hms(time.time() - t_start)}")
|
| 812 |
+
if run is not None:
|
| 813 |
+
run.finish()
|
| 814 |
+
dist.barrier()
|
| 815 |
+
dist.destroy_process_group()
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
if __name__ == "__main__":
|
| 819 |
+
with contextlib.suppress(KeyboardInterrupt):
|
| 820 |
+
main()
|
scripts/combine_and_publish.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Wait for all workers to upload, gather, combine, and publish the final dataset.
|
| 2 |
+
|
| 3 |
+
Aggregates stats with the FULL launcher.py schema (`summary`, `checks`,
|
| 4 |
+
`failure_breakdown`, `failure_breakdown_examples`, `num_shards`) so the published
|
| 5 |
+
`stats.json` matches the original `sdg/launcher.py` output structure.
|
| 6 |
+
|
| 7 |
+
Env vars:
|
| 8 |
+
HF_USER — HuggingFace username (default: fzzhang)
|
| 9 |
+
RUN_NAME — experiment name (default: math53K run name)
|
| 10 |
+
NUM_WORKERS — total worker count (default: 4)
|
| 11 |
+
NUM_SHARDS — total shards (default: 32)
|
| 12 |
+
"""
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import tempfile
|
| 16 |
+
import time
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
from huggingface_hub import HfApi, snapshot_download
|
| 20 |
+
|
| 21 |
+
HF_USER = os.environ.get("HF_USER", "fzzhang")
|
| 22 |
+
RUN_NAME = os.environ.get(
|
| 23 |
+
"RUN_NAME", "qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1"
|
| 24 |
+
)
|
| 25 |
+
NUM_WORKERS = int(os.environ.get("NUM_WORKERS", "4"))
|
| 26 |
+
NUM_SHARDS = int(os.environ.get("NUM_SHARDS", "32"))
|
| 27 |
+
STAGING_REPO = f"{HF_USER}/{RUN_NAME}-staging"
|
| 28 |
+
FINAL_REPO = f"{HF_USER}/{RUN_NAME}"
|
| 29 |
+
|
| 30 |
+
api = HfApi()
|
| 31 |
+
|
| 32 |
+
print(f"waiting for {NUM_WORKERS} workers to upload...", flush=True)
|
| 33 |
+
while True:
|
| 34 |
+
try:
|
| 35 |
+
files = api.list_repo_files(STAGING_REPO, repo_type="dataset")
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f" list error (staging repo may not exist yet): {e}", flush=True)
|
| 38 |
+
files = []
|
| 39 |
+
done = [
|
| 40 |
+
f for f in files
|
| 41 |
+
if f.startswith("workers/worker_") and f.endswith(".done")
|
| 42 |
+
]
|
| 43 |
+
print(
|
| 44 |
+
f" [{time.strftime('%H:%M:%S')}] {len(done)}/{NUM_WORKERS} workers done",
|
| 45 |
+
flush=True,
|
| 46 |
+
)
|
| 47 |
+
if len(done) >= NUM_WORKERS:
|
| 48 |
+
break
|
| 49 |
+
time.sleep(120)
|
| 50 |
+
|
| 51 |
+
print("downloading staging repo...", flush=True)
|
| 52 |
+
local = snapshot_download(
|
| 53 |
+
repo_id=STAGING_REPO, repo_type="dataset", local_dir="./gathered"
|
| 54 |
+
)
|
| 55 |
+
shards_dir = Path(local) / "shards"
|
| 56 |
+
present = sorted(shards_dir.glob("shard_*"))
|
| 57 |
+
print(f" {len(present)} shard dirs present", flush=True)
|
| 58 |
+
|
| 59 |
+
# Concatenate output.jsonl files in shard order
|
| 60 |
+
combined = Path("combined_output.jsonl")
|
| 61 |
+
total = 0
|
| 62 |
+
with open(combined, "w") as out:
|
| 63 |
+
for sid in range(NUM_SHARDS):
|
| 64 |
+
f = shards_dir / f"shard_{sid:03d}" / "output.jsonl"
|
| 65 |
+
if not f.exists():
|
| 66 |
+
print(f" WARN: missing {f}")
|
| 67 |
+
continue
|
| 68 |
+
with open(f) as src:
|
| 69 |
+
for line in src:
|
| 70 |
+
out.write(line)
|
| 71 |
+
total += 1
|
| 72 |
+
print(f"combined {total} rows", flush=True)
|
| 73 |
+
|
| 74 |
+
# Aggregate stats with the FULL launcher.py schema
|
| 75 |
+
agg = {
|
| 76 |
+
"summary": {
|
| 77 |
+
"total_examples": 0,
|
| 78 |
+
"total_passed": 0,
|
| 79 |
+
"total_passed_samples": 0,
|
| 80 |
+
"total_failed": 0,
|
| 81 |
+
"pass_rate_pct": 0.0,
|
| 82 |
+
},
|
| 83 |
+
"checks": {
|
| 84 |
+
"static_check": {"passed": 0, "failed": 0},
|
| 85 |
+
"cycle_consistency": {"passed": 0, "failed": 0},
|
| 86 |
+
"factual_accuracy": {"passed": 0, "failed": 0},
|
| 87 |
+
"correctness": {"passed": 0, "failed": 0},
|
| 88 |
+
},
|
| 89 |
+
"failure_breakdown": {
|
| 90 |
+
"failed_static_check": 0,
|
| 91 |
+
"failed_cycle_consistency": 0,
|
| 92 |
+
"failed_factual_accuracy": 0,
|
| 93 |
+
"failed_correctness": 0,
|
| 94 |
+
"exhausted_samples": 0,
|
| 95 |
+
"total_failed": 0,
|
| 96 |
+
},
|
| 97 |
+
"failure_breakdown_examples": {
|
| 98 |
+
"failed_static_check": 0,
|
| 99 |
+
"failed_cycle_consistency": 0,
|
| 100 |
+
"failed_factual_accuracy": 0,
|
| 101 |
+
"failed_correctness": 0,
|
| 102 |
+
"total_failed": 0,
|
| 103 |
+
},
|
| 104 |
+
"num_shards": NUM_SHARDS,
|
| 105 |
+
}
|
| 106 |
+
for sid in range(NUM_SHARDS):
|
| 107 |
+
sf = shards_dir / f"shard_{sid:03d}" / "stats.json"
|
| 108 |
+
if not sf.exists():
|
| 109 |
+
continue
|
| 110 |
+
s = json.load(open(sf))
|
| 111 |
+
for k in ["total_examples", "total_passed", "total_passed_samples", "total_failed"]:
|
| 112 |
+
agg["summary"][k] += s["summary"].get(k, 0)
|
| 113 |
+
for cn in agg["checks"]:
|
| 114 |
+
if cn in s.get("checks", {}):
|
| 115 |
+
for k in ["passed", "failed"]:
|
| 116 |
+
agg["checks"][cn][k] += s["checks"][cn][k]
|
| 117 |
+
for k in agg["failure_breakdown"]:
|
| 118 |
+
if k in s.get("failure_breakdown", {}):
|
| 119 |
+
agg["failure_breakdown"][k] += s["failure_breakdown"][k]
|
| 120 |
+
for k in agg["failure_breakdown_examples"]:
|
| 121 |
+
if k in s.get("failure_breakdown_examples", {}):
|
| 122 |
+
agg["failure_breakdown_examples"][k] += s["failure_breakdown_examples"][k]
|
| 123 |
+
te = agg["summary"]["total_examples"]
|
| 124 |
+
tp = agg["summary"]["total_passed"]
|
| 125 |
+
agg["summary"]["pass_rate_pct"] = round(tp / te * 100, 2) if te else 0.0
|
| 126 |
+
|
| 127 |
+
stats_path = Path("combined_stats.json")
|
| 128 |
+
json.dump(agg, open(stats_path, "w"), indent=2)
|
| 129 |
+
print(f"stats: {agg['summary']}", flush=True)
|
| 130 |
+
|
| 131 |
+
# Sanity: row count should match total_passed (first_valid policy)
|
| 132 |
+
expected = agg["summary"].get("total_passed_samples") or agg["summary"]["total_passed"]
|
| 133 |
+
if total != expected:
|
| 134 |
+
raise RuntimeError(
|
| 135 |
+
f"INTEGRITY: combined output.jsonl has {total} rows but stats expect "
|
| 136 |
+
f"{expected}. Shard outputs may be corrupted."
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
print(f"publishing to {FINAL_REPO}", flush=True)
|
| 140 |
+
api.create_repo(FINAL_REPO, repo_type="dataset", exist_ok=True)
|
| 141 |
+
readme = (
|
| 142 |
+
"---\n"
|
| 143 |
+
"configs:\n"
|
| 144 |
+
"- config_name: default\n"
|
| 145 |
+
" data_files:\n"
|
| 146 |
+
" - split: train\n"
|
| 147 |
+
" path: output.jsonl\n"
|
| 148 |
+
"---\n"
|
| 149 |
+
)
|
| 150 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
|
| 151 |
+
f.write(readme)
|
| 152 |
+
rp = f.name
|
| 153 |
+
api.upload_file(
|
| 154 |
+
path_or_fileobj=rp,
|
| 155 |
+
path_in_repo="README.md",
|
| 156 |
+
repo_id=FINAL_REPO,
|
| 157 |
+
repo_type="dataset",
|
| 158 |
+
)
|
| 159 |
+
api.upload_file(
|
| 160 |
+
path_or_fileobj=str(combined),
|
| 161 |
+
path_in_repo="output.jsonl",
|
| 162 |
+
repo_id=FINAL_REPO,
|
| 163 |
+
repo_type="dataset",
|
| 164 |
+
)
|
| 165 |
+
api.upload_file(
|
| 166 |
+
path_or_fileobj=str(stats_path),
|
| 167 |
+
path_in_repo="stats.json",
|
| 168 |
+
repo_id=FINAL_REPO,
|
| 169 |
+
repo_type="dataset",
|
| 170 |
+
)
|
| 171 |
+
print(f"DONE: https://huggingface.co/datasets/{FINAL_REPO}", flush=True)
|
scripts/launch_shards.sh
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Launch 8 shards on this worker, one per GPU.
|
| 3 |
+
# Worker index is derived from $MY_POD_NAME (Arnold pod name ends with worker-N).
|
| 4 |
+
#
|
| 5 |
+
# Env vars:
|
| 6 |
+
# CONFIG — path to the SDG YAML config (default: math53K config)
|
| 7 |
+
# LIMIT — optional --limit value for smoke tests (e.g. LIMIT=128)
|
| 8 |
+
# NUM_SHARDS — total shards across all workers (default: 32 = 4 nodes × 8 GPU)
|
| 9 |
+
#
|
| 10 |
+
# Outputs:
|
| 11 |
+
# logs/worker_${WORKER_IDX}/shard_NNN.log
|
| 12 |
+
set -e
|
| 13 |
+
|
| 14 |
+
WORKER_IDX=$(echo "$MY_POD_NAME" | grep -oE 'worker-[0-9]+$' | grep -oE '[0-9]+')
|
| 15 |
+
if [ -z "$WORKER_IDX" ]; then
|
| 16 |
+
echo "ERROR: could not infer worker index from MY_POD_NAME=$MY_POD_NAME" >&2
|
| 17 |
+
exit 1
|
| 18 |
+
fi
|
| 19 |
+
|
| 20 |
+
CONFIG="${CONFIG:-sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml}"
|
| 21 |
+
NUM_SHARDS="${NUM_SHARDS:-32}"
|
| 22 |
+
GPUS_PER_WORKER=8
|
| 23 |
+
START=$((WORKER_IDX * GPUS_PER_WORKER))
|
| 24 |
+
|
| 25 |
+
mkdir -p "logs/worker_${WORKER_IDX}"
|
| 26 |
+
LIMIT_FLAG=""
|
| 27 |
+
if [ -n "$LIMIT" ]; then LIMIT_FLAG="--limit $LIMIT"; fi
|
| 28 |
+
|
| 29 |
+
echo "worker $WORKER_IDX launching shards $START..$((START+GPUS_PER_WORKER-1)) (config=$CONFIG, limit=${LIMIT:-none})"
|
| 30 |
+
for i in $(seq 0 $((GPUS_PER_WORKER - 1))); do
|
| 31 |
+
SHARD_ID=$((START + i))
|
| 32 |
+
CUDA_VISIBLE_DEVICES=$i nohup python -m sdg.generate \
|
| 33 |
+
--config "$CONFIG" --shard-id "$SHARD_ID" --num-shards "$NUM_SHARDS" $LIMIT_FLAG \
|
| 34 |
+
> "logs/worker_${WORKER_IDX}/shard_${SHARD_ID}.log" 2>&1 &
|
| 35 |
+
done
|
| 36 |
+
echo "launched 8 shards; PIDs: $(jobs -p)"
|
scripts/monitor.sh
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Per-worker monitor: GPU util, current tqdm state for each shard, completion counts,
|
| 3 |
+
# and the cluster-wide MongoDB cache size.
|
| 4 |
+
#
|
| 5 |
+
# Works on any worker (uses $ARNOLD_WORKER_0_HOST for cross-worker mongo reachability,
|
| 6 |
+
# so it doesn't matter whether you're on worker_0 or not).
|
| 7 |
+
#
|
| 8 |
+
# Usage:
|
| 9 |
+
# ./scripts/monitor.sh # one shot
|
| 10 |
+
# watch -n 30 ./scripts/monitor.sh # auto-refresh
|
| 11 |
+
# while true; do clear; ./scripts/monitor.sh; sleep 30; done # if `watch` unavailable
|
| 12 |
+
|
| 13 |
+
RUN_NAME="${RUN_NAME:-qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1}"
|
| 14 |
+
|
| 15 |
+
echo "=== $(hostname) $(date +%H:%M:%S) ==="
|
| 16 |
+
nvidia-smi --query-gpu=index,utilization.gpu --format=csv,noheader
|
| 17 |
+
|
| 18 |
+
echo
|
| 19 |
+
echo "=== Shards on this worker ==="
|
| 20 |
+
# Show the most recent tqdm update on each shard's log.
|
| 21 |
+
# tqdm uses \r to overwrite a single line — naive `tail -1` returns the entire
|
| 22 |
+
# line and `cut -cN` truncates from the wrong end. Convert \r → \n and take
|
| 23 |
+
# the actual most-recent state.
|
| 24 |
+
for f in logs/worker_*/shard_*.log; do
|
| 25 |
+
[ -e "$f" ] || continue
|
| 26 |
+
recent=$(tail -c 500 "$f" 2>/dev/null | tr '\r' '\n' | tail -1)
|
| 27 |
+
echo "$(basename "$f"): $recent"
|
| 28 |
+
done
|
| 29 |
+
|
| 30 |
+
echo
|
| 31 |
+
STATS=$(ls "output/${RUN_NAME}/shards"/*/stats.json 2>/dev/null | wc -l)
|
| 32 |
+
COMPLETE=$(grep -l "PIPELINE COMPLETE" logs/worker_*/shard_*.log 2>/dev/null | wc -l)
|
| 33 |
+
|
| 34 |
+
# Mongo cache count — use ARNOLD_WORKER_0_HOST (IPv6) so this works from any worker,
|
| 35 |
+
# not just worker_0. 127.0.0.1 would only work on worker_0.
|
| 36 |
+
MONGO=$(python3 -c "
|
| 37 |
+
from pymongo import MongoClient
|
| 38 |
+
import os
|
| 39 |
+
host = os.environ['ARNOLD_WORKER_0_HOST']
|
| 40 |
+
port = os.environ['ARNOLD_WORKER_0_PORT']
|
| 41 |
+
print(MongoClient(f'mongodb://[{host}]:{port}', serverSelectionTimeoutMS=3000).sdg_cache.inference_cache.count_documents({}))
|
| 42 |
+
" 2>/dev/null)
|
| 43 |
+
|
| 44 |
+
echo "stats.json: $STATS/8 | PIPELINE COMPLETE: $COMPLETE/8 | mongo cache: ${MONGO:-<unreachable>}"
|
scripts/upload_my_shards.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Wait for this worker's 8 shards to complete, then upload to the staging HF repo.
|
| 2 |
+
|
| 3 |
+
Env vars:
|
| 4 |
+
HF_USER — HuggingFace username (default: fzzhang)
|
| 5 |
+
RUN_NAME — experiment name (default: math53K run name)
|
| 6 |
+
|
| 7 |
+
Polls every 2 minutes until all 8 stats.json files appear, then uploads each
|
| 8 |
+
shard folder to `<HF_USER>/<RUN_NAME>-staging` and writes a `workers/worker_N.done`
|
| 9 |
+
marker that the combiner watches for.
|
| 10 |
+
"""
|
| 11 |
+
import os
|
| 12 |
+
import re
|
| 13 |
+
import time
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from huggingface_hub import HfApi
|
| 17 |
+
|
| 18 |
+
HF_USER = os.environ.get("HF_USER", "fzzhang")
|
| 19 |
+
RUN_NAME = os.environ.get(
|
| 20 |
+
"RUN_NAME", "qwen3_4b_openthoughts3_math53K_instill_n8_valredundancy5_round1"
|
| 21 |
+
)
|
| 22 |
+
GPUS_PER_WORKER = 8
|
| 23 |
+
SHARDS_DIR = Path(f"output/{RUN_NAME}/shards")
|
| 24 |
+
STAGING_REPO = f"{HF_USER}/{RUN_NAME}-staging"
|
| 25 |
+
|
| 26 |
+
m = re.search(r"worker-(\d+)$", os.environ.get("MY_POD_NAME", ""))
|
| 27 |
+
if not m:
|
| 28 |
+
raise SystemExit("cannot infer worker idx from MY_POD_NAME")
|
| 29 |
+
widx = int(m.group(1))
|
| 30 |
+
my_shards = list(range(widx * GPUS_PER_WORKER, (widx + 1) * GPUS_PER_WORKER))
|
| 31 |
+
print(f"worker {widx} owns shards {my_shards}", flush=True)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def done(sid: int) -> bool:
|
| 35 |
+
return (SHARDS_DIR / f"shard_{sid:03d}" / "stats.json").exists()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
while True:
|
| 39 |
+
d = [s for s in my_shards if done(s)]
|
| 40 |
+
print(f"[wait] {len(d)}/{len(my_shards)} shards complete", flush=True)
|
| 41 |
+
if len(d) == len(my_shards):
|
| 42 |
+
break
|
| 43 |
+
time.sleep(120)
|
| 44 |
+
|
| 45 |
+
api = HfApi()
|
| 46 |
+
api.create_repo(STAGING_REPO, repo_type="dataset", exist_ok=True, private=True)
|
| 47 |
+
print(f"uploading to {STAGING_REPO}", flush=True)
|
| 48 |
+
|
| 49 |
+
for sid in my_shards:
|
| 50 |
+
sp = SHARDS_DIR / f"shard_{sid:03d}"
|
| 51 |
+
api.upload_folder(
|
| 52 |
+
folder_path=str(sp),
|
| 53 |
+
path_in_repo=f"shards/shard_{sid:03d}",
|
| 54 |
+
repo_id=STAGING_REPO,
|
| 55 |
+
repo_type="dataset",
|
| 56 |
+
commit_message=f"upload shard {sid:03d}",
|
| 57 |
+
)
|
| 58 |
+
print(f" shard_{sid:03d} done", flush=True)
|
| 59 |
+
|
| 60 |
+
api.upload_file(
|
| 61 |
+
path_or_fileobj=f"worker {widx} done\n".encode(),
|
| 62 |
+
path_in_repo=f"workers/worker_{widx}.done",
|
| 63 |
+
repo_id=STAGING_REPO,
|
| 64 |
+
repo_type="dataset",
|
| 65 |
+
)
|
| 66 |
+
print(f"worker {widx} fully uploaded", flush=True)
|
sdg/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__version__ = "0.1.0"
|
sdg/__main__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Allow ``python -m sdg`` as an alias for ``python -m sdg.generate``."""
|
| 2 |
+
from sdg.generate import main
|
| 3 |
+
|
| 4 |
+
main()
|
sdg/config.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SDGConfig dataclass and YAML serialization.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field, fields, asdict
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class SDGConfig:
|
| 13 |
+
# Model
|
| 14 |
+
model_name: str = "Qwen/Qwen3-4B"
|
| 15 |
+
is_instruction_tuned: bool = True
|
| 16 |
+
tensor_parallel_size: int = 1
|
| 17 |
+
max_model_len: int = 32768
|
| 18 |
+
gpu_memory_utilization: float = 0.95
|
| 19 |
+
|
| 20 |
+
# Dataset
|
| 21 |
+
dataset_hf_id: str = "open-thoughts/OpenThoughts-114k-math"
|
| 22 |
+
dataset_hf_config: str | None = None # HF dataset config/subset name
|
| 23 |
+
dataset_hf_revision: str | None = None # HF dataset revision (commit hash)
|
| 24 |
+
dataset_split: str = "train"
|
| 25 |
+
dataset_format: str = "openthoughts4" # "openthoughts4" | "messages"
|
| 26 |
+
check_boxed: bool = True
|
| 27 |
+
limit: int | None = None
|
| 28 |
+
|
| 29 |
+
# Generation
|
| 30 |
+
num_generations: int = 4
|
| 31 |
+
gen_temperature: float = 0.8
|
| 32 |
+
gen_top_p: float = 0.95
|
| 33 |
+
gen_max_tokens: int = 32768
|
| 34 |
+
gen_batch_size: int = 64
|
| 35 |
+
|
| 36 |
+
# Validation
|
| 37 |
+
validation_type: str | None = "uq" # "uq" | "simple" | null (None = skip validation)
|
| 38 |
+
selection_policy: str = "first_valid" # "first_valid" | "all_valid"
|
| 39 |
+
num_validation_votes: int = 3
|
| 40 |
+
val_temperature: float = 0.6
|
| 41 |
+
val_top_p: float = 0.95
|
| 42 |
+
val_max_tokens: int = 32768
|
| 43 |
+
val_batch_size: int = 64
|
| 44 |
+
|
| 45 |
+
# Output
|
| 46 |
+
experiment_name: str = ""
|
| 47 |
+
output_dir: str = "output"
|
| 48 |
+
|
| 49 |
+
# MongoDB cache
|
| 50 |
+
mongo_uri: str = ""
|
| 51 |
+
|
| 52 |
+
# Sharding
|
| 53 |
+
num_shards: int = 1
|
| 54 |
+
|
| 55 |
+
# Machines to exclude when launching Slurm jobs
|
| 56 |
+
machines_to_exclude: list[str] = field(default_factory=list)
|
| 57 |
+
|
| 58 |
+
# ── derived properties ──────────────────────────────────────────────
|
| 59 |
+
|
| 60 |
+
@property
|
| 61 |
+
def run_name(self) -> str:
|
| 62 |
+
if not self.experiment_name:
|
| 63 |
+
raise ValueError("experiment_name must be set in the config YAML")
|
| 64 |
+
return self.experiment_name
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def mongo_db_name(self) -> str:
|
| 68 |
+
return "sdg_cache"
|
| 69 |
+
|
| 70 |
+
@property
|
| 71 |
+
def output_path(self) -> Path:
|
| 72 |
+
return Path(self.output_dir) / self.run_name
|
| 73 |
+
|
| 74 |
+
# ── YAML I/O ────────────────────────────────────────────────────────
|
| 75 |
+
|
| 76 |
+
@classmethod
|
| 77 |
+
def from_yaml(cls, path: str | Path) -> "SDGConfig":
|
| 78 |
+
with open(path) as f:
|
| 79 |
+
data = yaml.safe_load(f) or {}
|
| 80 |
+
valid_keys = {f.name for f in fields(cls)}
|
| 81 |
+
filtered = {k: v for k, v in data.items() if k in valid_keys}
|
| 82 |
+
config = cls(**filtered)
|
| 83 |
+
if not config.mongo_uri:
|
| 84 |
+
raise ValueError(
|
| 85 |
+
f"mongo_uri is not set in {path}. "
|
| 86 |
+
f"Add a line like `mongo_uri: \"mongodb://HOST:PORT\"` to the config. "
|
| 87 |
+
f"(Common mistake: the key must be `mongo_uri` with an underscore, not `mongo-uri`.)"
|
| 88 |
+
)
|
| 89 |
+
return config
|
| 90 |
+
|
| 91 |
+
def to_yaml(self, path: str | Path) -> None:
|
| 92 |
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
| 93 |
+
with open(path, "w") as f:
|
| 94 |
+
yaml.dump(asdict(self), f, default_flow_style=False, sort_keys=False)
|
sdg/configs/qwen3_0.6b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_0.6b_openthoughts3_math53K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-0.6B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 8
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 5
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_0.6b_openthoughts4_code9K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_0.6b_openthoughts4_code9K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-0.6B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/ot4-code-deduped"
|
| 10 |
+
dataset_hf_revision: "840633b"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_0.6b_openthoughts4_science26K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_0.6b_openthoughts4_science26K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-0.6B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/ot4-science-deduped"
|
| 10 |
+
dataset_hf_revision: "ddae07c"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_0.6b_rstarcoder_seedsft_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_0.6b_rstarcoder_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-0.6B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/rstarcoder-seedsft-deduped"
|
| 10 |
+
dataset_hf_revision: "1242238"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://jagupard39.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_1.7b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_1.7b_openthoughts3_math53K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-1.7B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 8
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 5
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_1.7b_openthoughts4_code9K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_1.7b_openthoughts4_code9K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-1.7B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/ot4-code-deduped"
|
| 10 |
+
dataset_hf_revision: "840633b"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_1.7b_openthoughts4_science26K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_1.7b_openthoughts4_science26K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-1.7B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/ot4-science-deduped"
|
| 10 |
+
dataset_hf_revision: "ddae07c"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_32b_openthoughts3_math53K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_32b_openthoughts3_math53K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-32B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 8
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 5
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://jagupard39.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_32b_openthoughts4_code9K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_32b_openthoughts4_code9K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-32B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/ot4-code-deduped"
|
| 10 |
+
dataset_hf_revision: "840633b"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_32b_openthoughts4_science26K_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_32b_openthoughts4_science26K_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-32B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/ot4-science-deduped"
|
| 10 |
+
dataset_hf_revision: "ddae07c"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_32b_rstarcoder_seedsft_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_32b_rstarcoder_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-32B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/rstarcoder-seedsft-deduped"
|
| 10 |
+
dataset_hf_revision: "1242238"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://jagupard39.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_4b_mot_math_instill_n4_valredundancy3_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_name: "Qwen/Qwen3-4B"
|
| 2 |
+
is_instruction_tuned: true
|
| 3 |
+
tensor_parallel_size: 1
|
| 4 |
+
max_model_len: 32768
|
| 5 |
+
gpu_memory_utilization: 0.95
|
| 6 |
+
|
| 7 |
+
dataset_hf_id: "open-r1/Mixture-of-Thoughts"
|
| 8 |
+
dataset_hf_config: "math"
|
| 9 |
+
dataset_split: "train"
|
| 10 |
+
dataset_format: "messages"
|
| 11 |
+
check_boxed: true
|
| 12 |
+
limit: null
|
| 13 |
+
|
| 14 |
+
num_generations: 4
|
| 15 |
+
gen_temperature: 0.8
|
| 16 |
+
gen_top_p: 0.95
|
| 17 |
+
gen_max_tokens: 32768
|
| 18 |
+
gen_batch_size: 64
|
| 19 |
+
|
| 20 |
+
validation_type: uq
|
| 21 |
+
selection_policy: first_valid
|
| 22 |
+
num_validation_votes: 3
|
| 23 |
+
val_temperature: 0.8
|
| 24 |
+
val_top_p: 0.95
|
| 25 |
+
val_max_tokens: 32768
|
| 26 |
+
val_batch_size: 64
|
| 27 |
+
|
| 28 |
+
experiment_name: "qwen3_4b_mot_math_instill_n4_valredundancy3_round1"
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
|
| 31 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_4b_nemotron_cascade2_science100k_instill_n8_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_nemotron_cascade2_science100k_instill_n8_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/nemotron-cascade-2-science-deduped-100k"
|
| 10 |
+
dataset_hf_revision: "adb996f"
|
| 11 |
+
dataset_split: "train"
|
| 12 |
+
dataset_format: "openthoughts4"
|
| 13 |
+
check_boxed: false
|
| 14 |
+
limit: null
|
| 15 |
+
|
| 16 |
+
num_generations: 8
|
| 17 |
+
gen_temperature: 0.8
|
| 18 |
+
gen_top_p: 0.95
|
| 19 |
+
gen_max_tokens: 32768
|
| 20 |
+
gen_batch_size: 64
|
| 21 |
+
|
| 22 |
+
validation_type: uq
|
| 23 |
+
selection_policy: first_valid
|
| 24 |
+
num_validation_votes: 5
|
| 25 |
+
val_temperature: 0.8
|
| 26 |
+
val_top_p: 0.95
|
| 27 |
+
val_max_tokens: 32768
|
| 28 |
+
val_batch_size: 64
|
| 29 |
+
|
| 30 |
+
output_dir: "output"
|
| 31 |
+
mongo_uri: "mongodb://jagupard39.stanford.edu:27017"
|
| 32 |
+
num_shards: 30
|
| 33 |
+
|
| 34 |
+
machines_to_exclude:
|
| 35 |
+
- sphinx1
|
| 36 |
+
- sphinx2
|
| 37 |
+
- sphinx3
|
| 38 |
+
- sphinx4
|
| 39 |
+
- sphinx5
|
| 40 |
+
- sphinx6
|
| 41 |
+
- sphinx7
|
| 42 |
+
- sphinx8
|
sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy1_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy1_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 1
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 1
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy3_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy3_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 1
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 3
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_openthoughts3_math53K_instill_n1_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 1
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 5
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy1_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy1_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 4
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 1
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy3_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy3_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 4
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 3
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_openthoughts3_math53K_instill_n4_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 4
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: uq
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 5
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_no_filter_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_openthoughts3_math53K_instill_n8_no_filter_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 8
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: null
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 0
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|
sdg/configs/qwen3_4b_openthoughts3_math53K_instill_n8_simple_valredundancy5_round1.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_name: "qwen3_4b_openthoughts3_math53K_instill_n8_simple_valredundancy5_round1"
|
| 2 |
+
|
| 3 |
+
model_name: "Qwen/Qwen3-4B"
|
| 4 |
+
is_instruction_tuned: true
|
| 5 |
+
tensor_parallel_size: 1
|
| 6 |
+
max_model_len: 32768
|
| 7 |
+
gpu_memory_utilization: 0.95
|
| 8 |
+
|
| 9 |
+
dataset_hf_id: "teetone/openthoughts3-math-deduped-53K"
|
| 10 |
+
dataset_split: "train"
|
| 11 |
+
dataset_format: "openthoughts4"
|
| 12 |
+
check_boxed: true
|
| 13 |
+
limit: null
|
| 14 |
+
|
| 15 |
+
num_generations: 8
|
| 16 |
+
gen_temperature: 0.8
|
| 17 |
+
gen_top_p: 0.95
|
| 18 |
+
gen_max_tokens: 32768
|
| 19 |
+
gen_batch_size: 64
|
| 20 |
+
|
| 21 |
+
validation_type: simple
|
| 22 |
+
selection_policy: first_valid
|
| 23 |
+
num_validation_votes: 5
|
| 24 |
+
val_temperature: 0.8
|
| 25 |
+
val_top_p: 0.95
|
| 26 |
+
val_max_tokens: 32768
|
| 27 |
+
val_batch_size: 64
|
| 28 |
+
|
| 29 |
+
output_dir: "output"
|
| 30 |
+
mongo_uri: "mongodb://john3.stanford.edu:27017"
|
| 31 |
+
num_shards: 30
|
| 32 |
+
|
| 33 |
+
machines_to_exclude:
|
| 34 |
+
- sphinx1
|
| 35 |
+
- sphinx2
|
| 36 |
+
- sphinx3
|
| 37 |
+
- sphinx4
|
| 38 |
+
- sphinx5
|
| 39 |
+
- sphinx6
|
| 40 |
+
- sphinx7
|
| 41 |
+
- sphinx8
|