Spaces:
Sleeping
Sleeping
MSG msgencrypted-auto commited on
Commit ·
28543d3
1
Parent(s): aac5f23
Feat/last sprintos (#23)
Browse files* skills
* fix readme
* readme track
* readme track
* experiment
* experiment and finetuning fix
* experiment and finetuning fix
* server finetuning common
* fix
* fix
---------
Co-authored-by: msgencrypted-auto <msgencrypted.auto@gmail.com>
- README.md +19 -0
- modal_math_gate_publish_result.json +53 -0
- modal_science_gate_publish_result.json +55 -0
- research/modal/README.md +15 -7
- research/modal/_common.py +209 -13
- research/modal/experiments.yaml +36 -3
- research/modal/finetune_app.py +108 -18
- research/modal/server_app.py +118 -39
- research/modal/tests/test_modal_common.py +107 -0
README.md
CHANGED
|
@@ -10,8 +10,21 @@ python_version: "3.12"
|
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
tags:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
- build-small-hackathon
|
| 14 |
- backyard-ai
|
|
|
|
| 15 |
- tiny-titan
|
| 16 |
- best-agent
|
| 17 |
- best-demo
|
|
@@ -19,8 +32,14 @@ tags:
|
|
| 19 |
- sharing-is-caring
|
| 20 |
- off-the-grid
|
| 21 |
- off-brand
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
---
|
| 23 |
|
|
|
|
|
|
|
| 24 |
# Lesson Agent
|
| 25 |
|
| 26 |
**Backyard AI** Gradio Space for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
|
|
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
tags:
|
| 13 |
+
- track:backyard
|
| 14 |
+
- track:wood
|
| 15 |
+
- sponsor:openbmb
|
| 16 |
+
- sponsor:openai
|
| 17 |
+
- sponsor:nvidia
|
| 18 |
+
- sponsor:modal
|
| 19 |
+
- achievement:offgrid
|
| 20 |
+
- achievement:welltuned
|
| 21 |
+
- achievement:offbrand
|
| 22 |
+
- achievement:llama
|
| 23 |
+
- achievement:sharing
|
| 24 |
+
- achievement:fieldnotes
|
| 25 |
- build-small-hackathon
|
| 26 |
- backyard-ai
|
| 27 |
+
- modal
|
| 28 |
- tiny-titan
|
| 29 |
- best-agent
|
| 30 |
- best-demo
|
|
|
|
| 32 |
- sharing-is-caring
|
| 33 |
- off-the-grid
|
| 34 |
- off-brand
|
| 35 |
+
- field-notes
|
| 36 |
+
- well-tuned
|
| 37 |
+
- llama-champion
|
| 38 |
+
|
| 39 |
---
|
| 40 |
|
| 41 |
+
|
| 42 |
+
|
| 43 |
# Lesson Agent
|
| 44 |
|
| 45 |
**Backyard AI** Gradio Space for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
modal_math_gate_publish_result.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"job": "math-lora",
|
| 3 |
+
"baseline_results": "/vol/finetuned/results/lm_eval/minicpm5-1b__baseline__math/results.json",
|
| 4 |
+
"candidate_results": "/vol/finetuned/results/lm_eval/math-lora__math/results.json",
|
| 5 |
+
"gate": {
|
| 6 |
+
"passed": true,
|
| 7 |
+
"checks": [
|
| 8 |
+
{
|
| 9 |
+
"check": "gsm8k >= 0.05",
|
| 10 |
+
"value": 0.4,
|
| 11 |
+
"ok": true
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"check": "gsm8k improve >= 0.02",
|
| 15 |
+
"value": 0.07,
|
| 16 |
+
"ok": true
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"check": "arc_challenge regress <= 0.03",
|
| 20 |
+
"value": -0.04999999999999999,
|
| 21 |
+
"ok": true
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"check": "hellaswag regress <= 0.03",
|
| 25 |
+
"value": 0.0,
|
| 26 |
+
"ok": true
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"check": "piqa regress <= 0.03",
|
| 30 |
+
"value": 0.020000000000000018,
|
| 31 |
+
"ok": true
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"task": "gsm8k",
|
| 35 |
+
"candidate_score": 0.4,
|
| 36 |
+
"baseline_score": 0.33
|
| 37 |
+
},
|
| 38 |
+
"publish": {
|
| 39 |
+
"published": true,
|
| 40 |
+
"repo_id": "MSGEncrypted/minicpm5-1b-math-lora",
|
| 41 |
+
"url": "https://huggingface.co/MSGEncrypted/minicpm5-1b-math-lora",
|
| 42 |
+
"uploads": [
|
| 43 |
+
{
|
| 44 |
+
"repo_id": "MSGEncrypted/minicpm5-1b-math-lora",
|
| 45 |
+
"url": "https://huggingface.co/MSGEncrypted/minicpm5-1b-math-lora"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"repo_id": "build-small-hackathon/minicpm5-1b-math-lora",
|
| 49 |
+
"url": "https://huggingface.co/build-small-hackathon/minicpm5-1b-math-lora"
|
| 50 |
+
}
|
| 51 |
+
]
|
| 52 |
+
}
|
| 53 |
+
}
|
modal_science_gate_publish_result.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"job": "science-lora",
|
| 3 |
+
"baseline_results": "/vol/finetuned/results/lm_eval/minicpm5-1b__baseline__science/results.json",
|
| 4 |
+
"candidate_results": "/vol/finetuned/results/lm_eval/science-lora__science/results.json",
|
| 5 |
+
"gate": {
|
| 6 |
+
"passed": false,
|
| 7 |
+
"checks": [
|
| 8 |
+
{
|
| 9 |
+
"check": "sciq >= 0.5",
|
| 10 |
+
"value": 0.94,
|
| 11 |
+
"ok": true
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"check": "sciq improve >= 0.02",
|
| 15 |
+
"value": -0.010000000000000009,
|
| 16 |
+
"ok": false
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"check": "arc_challenge regress <= 0.03",
|
| 20 |
+
"value": -0.019999999999999962,
|
| 21 |
+
"ok": true
|
| 22 |
+
}
|
| 23 |
+
],
|
| 24 |
+
"task": "sciq",
|
| 25 |
+
"candidate_score": 0.94,
|
| 26 |
+
"baseline_score": 0.95
|
| 27 |
+
},
|
| 28 |
+
"publish": {
|
| 29 |
+
"published": false,
|
| 30 |
+
"reason": "gate failed",
|
| 31 |
+
"gate": {
|
| 32 |
+
"passed": false,
|
| 33 |
+
"checks": [
|
| 34 |
+
{
|
| 35 |
+
"check": "sciq >= 0.5",
|
| 36 |
+
"value": 0.94,
|
| 37 |
+
"ok": true
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"check": "sciq improve >= 0.02",
|
| 41 |
+
"value": -0.010000000000000009,
|
| 42 |
+
"ok": false
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"check": "arc_challenge regress <= 0.03",
|
| 46 |
+
"value": -0.019999999999999962,
|
| 47 |
+
"ok": true
|
| 48 |
+
}
|
| 49 |
+
],
|
| 50 |
+
"task": "sciq",
|
| 51 |
+
"candidate_score": 0.94,
|
| 52 |
+
"baseline_score": 0.95
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
}
|
research/modal/README.md
CHANGED
|
@@ -60,7 +60,7 @@ uv sync --group modal # local orchestration only
|
|
| 60 |
## Run training + benchmarks
|
| 61 |
|
| 62 |
All commands from **repo root**. `finetune_app.py` runs the full **skill-matrix
|
| 63 |
-
pipeline**: per-profile baseline lm-eval → finetune each job's QLoRA adapter →
|
| 64 |
post-train lm-eval vs. that baseline → check `goals` (gate) → publish to the
|
| 65 |
Hugging Face Hub if the gate passes → pull adapter + results to your laptop.
|
| 66 |
|
|
@@ -119,7 +119,7 @@ for the `goals`/`publish` schema.
|
|
| 119 |
| Flag | Default | Meaning |
|
| 120 |
| ---- | ------- | ------- |
|
| 121 |
| `--train` / `--no-train` | train on | Run finetune jobs |
|
| 122 |
-
| `--eval-only` | off | Skip train
|
| 123 |
| `--parallel` | off | `finetune_one.spawn()` per job instead of sequential |
|
| 124 |
| `--job` | all jobs | Run one job name from `experiments.yaml` |
|
| 125 |
| `--category` | all categories | Run all jobs with this `category` |
|
|
@@ -208,7 +208,7 @@ Task flags (`--job`, `--category`, `--cmd`, `--pipeline`, `--eval-only`, `--publ
|
|
| 208 |
| `--category` | — | Run the skill-matrix pipeline for all jobs in a category |
|
| 209 |
| `--pipeline` | off | Run the skill-matrix pipeline for all jobs |
|
| 210 |
| `--max-steps` | from YAML | Override training steps |
|
| 211 |
-
| `--eval-only` | off | Pipeline eval/gate/publish
|
| 212 |
| `--publish` / `--no-publish` | publish on | Push to `publish.hub_repo` if the gate passes |
|
| 213 |
| `--publish-only` | off | Re-check the gate against existing results and publish (requires `--job`) |
|
| 214 |
| `--pull` / `--no-pull` | pull on | `modal volume get` adapter + results after the pipeline |
|
|
@@ -469,6 +469,11 @@ goals:
|
|
| 469 |
max_regress: 0.03
|
| 470 |
```
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
A job with no `goals` (e.g. `alpaca-lora`) is never gated and never published —
|
| 473 |
it's local-only (still trained, evaluated, and pulled to your laptop).
|
| 474 |
|
|
@@ -482,10 +487,13 @@ publish:
|
|
| 482 |
|
| 483 |
### What happens on a passing gate
|
| 484 |
|
| 485 |
-
1. `run_lm_eval` writes `results/lm_eval/<job>__<profile>/results.json`.
|
| 486 |
-
2.
|
| 487 |
-
|
| 488 |
-
3.
|
|
|
|
|
|
|
|
|
|
| 489 |
- renders a model card (`README.md`) into the adapter directory — base model,
|
| 490 |
gate checks table, full lm-eval baseline-vs-candidate-vs-delta table,
|
| 491 |
training stats, and a PEFT load snippet
|
|
|
|
| 60 |
## Run training + benchmarks
|
| 61 |
|
| 62 |
All commands from **repo root**. `finetune_app.py` runs the full **skill-matrix
|
| 63 |
+
pipeline**: per-profile **base-model** baseline lm-eval (no adapter) → finetune each job's QLoRA adapter →
|
| 64 |
post-train lm-eval vs. that baseline → check `goals` (gate) → publish to the
|
| 65 |
Hugging Face Hub if the gate passes → pull adapter + results to your laptop.
|
| 66 |
|
|
|
|
| 119 |
| Flag | Default | Meaning |
|
| 120 |
| ---- | ------- | ------- |
|
| 121 |
| `--train` / `--no-train` | train on | Run finetune jobs |
|
| 122 |
+
| `--eval-only` | off | Skip train; eval existing Volume checkpoints (still runs missing base-model baselines) |
|
| 123 |
| `--parallel` | off | `finetune_one.spawn()` per job instead of sequential |
|
| 124 |
| `--job` | all jobs | Run one job name from `experiments.yaml` |
|
| 125 |
| `--category` | all categories | Run all jobs with this `category` |
|
|
|
|
| 208 |
| `--category` | — | Run the skill-matrix pipeline for all jobs in a category |
|
| 209 |
| `--pipeline` | off | Run the skill-matrix pipeline for all jobs |
|
| 210 |
| `--max-steps` | from YAML | Override training steps |
|
| 211 |
+
| `--eval-only` | off | Pipeline eval/gate/publish only (skip train; still runs missing base-model baselines) |
|
| 212 |
| `--publish` / `--no-publish` | publish on | Push to `publish.hub_repo` if the gate passes |
|
| 213 |
| `--publish-only` | off | Re-check the gate against existing results and publish (requires `--job`) |
|
| 214 |
| `--pull` / `--no-pull` | pull on | `modal volume get` adapter + results after the pipeline |
|
|
|
|
| 469 |
max_regress: 0.03
|
| 470 |
```
|
| 471 |
|
| 472 |
+
Publishable jobs also run a **general** eval (`defaults.general_eval_profile`, default
|
| 473 |
+
`compare_study`: arc_easy, arc_challenge, hellaswag, piqa, boolq, gsm8k) and must pass
|
| 474 |
+
`defaults.general_goals` regression guards so skill tuning does not wash out general
|
| 475 |
+
capability. The publish gate requires **both** skill `goals` and `general_goals` to pass.
|
| 476 |
+
|
| 477 |
A job with no `goals` (e.g. `alpaca-lora`) is never gated and never published —
|
| 478 |
it's local-only (still trained, evaluated, and pulled to your laptop).
|
| 479 |
|
|
|
|
| 487 |
|
| 488 |
### What happens on a passing gate
|
| 489 |
|
| 490 |
+
1. `run_lm_eval` writes skill results to `results/lm_eval/<job>__<profile>/results.json`.
|
| 491 |
+
2. For publishable jobs, a second run writes general results to
|
| 492 |
+
`results/lm_eval/<job>__<general_eval_profile>/results.json`.
|
| 493 |
+
3. `check_gate` compares skill results against `results/lm_eval/<preset>__baseline__<profile>/results.json`
|
| 494 |
+
and general results against `results/lm_eval/<preset>__baseline__<general_eval_profile>/results.json`
|
| 495 |
+
using `goals` + `general_goals` → `{"passed": bool, "skill": {...}, "general": {...}, "checks": [...]}`.
|
| 496 |
+
4. If `passed` and `publish` is set, `publish_adapter`:
|
| 497 |
- renders a model card (`README.md`) into the adapter directory — base model,
|
| 498 |
gate checks table, full lm-eval baseline-vs-candidate-vs-delta table,
|
| 499 |
training stats, and a PEFT load snippet
|
research/modal/_common.py
CHANGED
|
@@ -36,6 +36,8 @@ BASE_MODEL_ID = "openbmb/MiniCPM5-1B"
|
|
| 36 |
|
| 37 |
BASELINE_EXPERIMENT = "minicpm5-1b__modal-baseline"
|
| 38 |
BASELINE_RESULTS_JSON = f"{LM_EVAL_OUTPUT}/{BASELINE_EXPERIMENT}/results.json"
|
|
|
|
|
|
|
| 39 |
|
| 40 |
# Metric keys to prefer when picking a task's "primary" score, in priority
|
| 41 |
# order. Covers lm-eval-harness multiple-choice (acc), generation (exact_match),
|
|
@@ -87,6 +89,9 @@ image = (
|
|
| 87 |
COMMON_ENV = {
|
| 88 |
"TRUST_REMOTE_CODE": "true",
|
| 89 |
"HF_HOME": HF_CACHE_PATH,
|
|
|
|
|
|
|
|
|
|
| 90 |
"PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:True",
|
| 91 |
}
|
| 92 |
|
|
@@ -100,9 +105,24 @@ def repo_env() -> dict[str, str]:
|
|
| 100 |
return {**os.environ, **COMMON_ENV}
|
| 101 |
|
| 102 |
|
| 103 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
finetune_vol.reload()
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
|
| 108 |
def commit_volumes() -> None:
|
|
@@ -385,6 +405,105 @@ def job_gpu(job: dict[str, Any]) -> str:
|
|
| 385 |
return job.get("gpu") or DEFAULT_GPU
|
| 386 |
|
| 387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
def config_for_profile(profile: str) -> str:
|
| 389 |
"""Map an eval_profiles.yaml profile name to its config path (relative to repo root)."""
|
| 390 |
with EVAL_PROFILES_PATH.open() as f:
|
|
@@ -462,7 +581,7 @@ def evaluate_gate(
|
|
| 462 |
"""Check a candidate's lm-eval results dict against `goals` (Hub publish gate).
|
| 463 |
|
| 464 |
`goals` schema:
|
| 465 |
-
task: <lm-eval task name
|
| 466 |
min_score: <float, optional> # candidate score must be >= this
|
| 467 |
min_improve: <float, optional> # candidate - baseline must be >= this
|
| 468 |
guard_tasks: # optional regression guards
|
|
@@ -482,9 +601,11 @@ def evaluate_gate(
|
|
| 482 |
checks: list[dict[str, Any]] = []
|
| 483 |
passed = True
|
| 484 |
|
| 485 |
-
task = goals
|
| 486 |
-
cand_score =
|
| 487 |
-
|
|
|
|
|
|
|
| 488 |
|
| 489 |
# Tolerance so a score landing exactly on a threshold (e.g. a clean +0.02
|
| 490 |
# improvement stored as 0.0199999996) is not rejected by float epsilon.
|
|
@@ -576,6 +697,61 @@ def check_gate_files(
|
|
| 576 |
return evaluate_gate(candidate=candidate, baseline=baseline, goals=goals)
|
| 577 |
|
| 578 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
def render_model_card(
|
| 580 |
*,
|
| 581 |
job: dict[str, Any],
|
|
@@ -624,16 +800,36 @@ def render_model_card(
|
|
| 624 |
"",
|
| 625 |
"## Benchmark gate",
|
| 626 |
"",
|
| 627 |
-
f"- eval profile: `{job.get('eval_profile')}`",
|
| 628 |
f"- gate: {'**PASSED**' if gate_result.get('passed') else '**FAILED**'}",
|
| 629 |
"",
|
| 630 |
-
"| check | value | result |",
|
| 631 |
-
"| --- | ---: | --- |",
|
| 632 |
]
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
|
| 638 |
lines.extend(
|
| 639 |
[
|
|
|
|
| 36 |
|
| 37 |
BASELINE_EXPERIMENT = "minicpm5-1b__modal-baseline"
|
| 38 |
BASELINE_RESULTS_JSON = f"{LM_EVAL_OUTPUT}/{BASELINE_EXPERIMENT}/results.json"
|
| 39 |
+
# Shared general-capability profile for publish gates (limit 100; see compare_study).
|
| 40 |
+
GENERAL_EVAL_PROFILE = "compare_study"
|
| 41 |
|
| 42 |
# Metric keys to prefer when picking a task's "primary" score, in priority
|
| 43 |
# order. Covers lm-eval-harness multiple-choice (acc), generation (exact_match),
|
|
|
|
| 89 |
COMMON_ENV = {
|
| 90 |
"TRUST_REMOTE_CODE": "true",
|
| 91 |
"HF_HOME": HF_CACHE_PATH,
|
| 92 |
+
# Keep hf-xet logs off the HF cache Volume mount so volume.reload() is not
|
| 93 |
+
# blocked by open log file handles on warm containers.
|
| 94 |
+
"HF_XET_LOG_DEST": "/tmp/xet-logs/",
|
| 95 |
"PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:True",
|
| 96 |
}
|
| 97 |
|
|
|
|
| 105 |
return {**os.environ, **COMMON_ENV}
|
| 106 |
|
| 107 |
|
| 108 |
+
def _reload_volume_safe(vol: modal.Volume, *, label: str) -> None:
|
| 109 |
+
"""Reload a Volume; skip (with warning) when open files block the operation."""
|
| 110 |
+
try:
|
| 111 |
+
vol.reload()
|
| 112 |
+
except (RuntimeError, modal.exception.ConflictError) as exc:
|
| 113 |
+
if "open files preventing the operation" in str(exc):
|
| 114 |
+
print(f"warning: skipping {label} volume reload ({exc})")
|
| 115 |
+
return
|
| 116 |
+
raise
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def reload_finetune_volume() -> None:
|
| 120 |
finetune_vol.reload()
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def reload_volumes() -> None:
|
| 124 |
+
reload_finetune_volume()
|
| 125 |
+
_reload_volume_safe(hf_cache_vol, label="hf-cache")
|
| 126 |
|
| 127 |
|
| 128 |
def commit_volumes() -> None:
|
|
|
|
| 405 |
return job.get("gpu") or DEFAULT_GPU
|
| 406 |
|
| 407 |
|
| 408 |
+
def job_needs_general_gate(job: dict[str, Any]) -> bool:
|
| 409 |
+
"""Publishable jobs run a second general eval and must pass `general_goals`."""
|
| 410 |
+
return bool(job.get("goals") and job.get("publish"))
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def general_eval_profile(defaults: dict[str, Any]) -> str:
|
| 414 |
+
return defaults.get("general_eval_profile", GENERAL_EVAL_PROFILE)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def general_goals_for_job(
|
| 418 |
+
job: dict[str, Any], defaults: dict[str, Any]
|
| 419 |
+
) -> dict[str, Any] | None:
|
| 420 |
+
if not job_needs_general_gate(job):
|
| 421 |
+
return None
|
| 422 |
+
goals = job.get("general_goals") or defaults.get("general_goals")
|
| 423 |
+
return goals if goals else None
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def baseline_profiles_for_jobs(
|
| 427 |
+
jobs: list[dict[str, Any]], defaults: dict[str, Any]
|
| 428 |
+
) -> list[str]:
|
| 429 |
+
profiles = {j.get("eval_profile", "compare_study") for j in jobs}
|
| 430 |
+
if any(job_needs_general_gate(j) for j in jobs):
|
| 431 |
+
profiles.add(general_eval_profile(defaults))
|
| 432 |
+
return sorted(profiles)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def baseline_experiment_name(preset: str, profile: str) -> str:
|
| 436 |
+
"""Volume path key for the unfine-tuned base model on a given eval profile."""
|
| 437 |
+
return f"{preset}__baseline__{profile}"
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def _load_models_registry() -> dict[str, Any]:
|
| 441 |
+
path = REPO_ROOT / "models.yaml"
|
| 442 |
+
if not path.is_file():
|
| 443 |
+
path = Path("/repo") / "models.yaml"
|
| 444 |
+
if not path.is_file():
|
| 445 |
+
return {}
|
| 446 |
+
with path.open() as f:
|
| 447 |
+
return yaml.safe_load(f) or {}
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def resolve_base_model_id(job: dict[str, Any], defaults: dict[str, Any]) -> str:
|
| 451 |
+
"""Hub/path id of the base model this job fine-tunes — used as the eval baseline."""
|
| 452 |
+
explicit = job.get("model") or (job.get("args") or {}).get("model")
|
| 453 |
+
if explicit:
|
| 454 |
+
return str(explicit)
|
| 455 |
+
preset = job.get("preset", defaults.get("preset", "minicpm5-1b"))
|
| 456 |
+
entry = (_load_models_registry().get("models") or {}).get(preset) or {}
|
| 457 |
+
return entry.get("model_id") or BASE_MODEL_ID
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def discover_cached_baselines(
|
| 461 |
+
profile_names: list[str],
|
| 462 |
+
*,
|
| 463 |
+
preset: str,
|
| 464 |
+
eval_tasks: list[str] | None = None,
|
| 465 |
+
eval_limit: int | None = None,
|
| 466 |
+
eval_num_fewshot: int | None = None,
|
| 467 |
+
eval_seed: int | None = None,
|
| 468 |
+
) -> dict[str, bool]:
|
| 469 |
+
"""True per profile when base-model baseline results already exist on the Volume."""
|
| 470 |
+
cached: dict[str, bool] = {}
|
| 471 |
+
for profile in profile_names:
|
| 472 |
+
cached[profile] = baseline_is_cached(
|
| 473 |
+
baseline_experiment_name(preset, profile),
|
| 474 |
+
config_for_profile(profile),
|
| 475 |
+
tasks=eval_tasks,
|
| 476 |
+
limit=eval_limit,
|
| 477 |
+
num_fewshot=eval_num_fewshot,
|
| 478 |
+
seed=eval_seed,
|
| 479 |
+
)
|
| 480 |
+
return cached
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def profiles_needing_baseline_run(
|
| 484 |
+
profile_names: list[str],
|
| 485 |
+
cached: dict[str, bool],
|
| 486 |
+
*,
|
| 487 |
+
skip_baseline: bool,
|
| 488 |
+
) -> list[str]:
|
| 489 |
+
if skip_baseline:
|
| 490 |
+
return []
|
| 491 |
+
return [profile for profile in profile_names if not cached.get(profile)]
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def eval_paths(
|
| 495 |
+
*,
|
| 496 |
+
job_name: str,
|
| 497 |
+
preset: str,
|
| 498 |
+
profile: str,
|
| 499 |
+
) -> tuple[str, str, str]:
|
| 500 |
+
"""Return (candidate_results_path, baseline_results_path, experiment_name)."""
|
| 501 |
+
exp_name = f"{job_name}__{profile}"
|
| 502 |
+
candidate = f"{LM_EVAL_OUTPUT}/{exp_name}/results.json"
|
| 503 |
+
baseline = f"{LM_EVAL_OUTPUT}/{baseline_experiment_name(preset, profile)}/results.json"
|
| 504 |
+
return candidate, baseline, exp_name
|
| 505 |
+
|
| 506 |
+
|
| 507 |
def config_for_profile(profile: str) -> str:
|
| 508 |
"""Map an eval_profiles.yaml profile name to its config path (relative to repo root)."""
|
| 509 |
with EVAL_PROFILES_PATH.open() as f:
|
|
|
|
| 581 |
"""Check a candidate's lm-eval results dict against `goals` (Hub publish gate).
|
| 582 |
|
| 583 |
`goals` schema:
|
| 584 |
+
task: <lm-eval task name, optional when only guard_tasks are set>
|
| 585 |
min_score: <float, optional> # candidate score must be >= this
|
| 586 |
min_improve: <float, optional> # candidate - baseline must be >= this
|
| 587 |
guard_tasks: # optional regression guards
|
|
|
|
| 601 |
checks: list[dict[str, Any]] = []
|
| 602 |
passed = True
|
| 603 |
|
| 604 |
+
task = goals.get("task")
|
| 605 |
+
cand_score = base_score = None
|
| 606 |
+
if task:
|
| 607 |
+
cand_score = _score(cand_tasks, task)
|
| 608 |
+
base_score = _score(base_tasks, task)
|
| 609 |
|
| 610 |
# Tolerance so a score landing exactly on a threshold (e.g. a clean +0.02
|
| 611 |
# improvement stored as 0.0199999996) is not rejected by float epsilon.
|
|
|
|
| 697 |
return evaluate_gate(candidate=candidate, baseline=baseline, goals=goals)
|
| 698 |
|
| 699 |
|
| 700 |
+
def check_publish_gate_files(
|
| 701 |
+
*,
|
| 702 |
+
skill_candidate_path: str,
|
| 703 |
+
skill_baseline_path: str | None,
|
| 704 |
+
skill_goals: dict[str, Any],
|
| 705 |
+
general_candidate_path: str | None = None,
|
| 706 |
+
general_baseline_path: str | None = None,
|
| 707 |
+
general_goals: dict[str, Any] | None = None,
|
| 708 |
+
) -> dict[str, Any]:
|
| 709 |
+
"""Gate on skill-specific eval plus optional general-capability eval."""
|
| 710 |
+
skill_gate = check_gate_files(
|
| 711 |
+
candidate_results_path=skill_candidate_path,
|
| 712 |
+
baseline_results_path=skill_baseline_path,
|
| 713 |
+
goals=skill_goals,
|
| 714 |
+
)
|
| 715 |
+
general_gate: dict[str, Any] | None = None
|
| 716 |
+
if general_goals:
|
| 717 |
+
if not general_candidate_path:
|
| 718 |
+
general_gate = {
|
| 719 |
+
"passed": False,
|
| 720 |
+
"checks": [
|
| 721 |
+
{
|
| 722 |
+
"check": "general eval results missing",
|
| 723 |
+
"value": None,
|
| 724 |
+
"ok": False,
|
| 725 |
+
}
|
| 726 |
+
],
|
| 727 |
+
"reason": "general candidate results path not provided",
|
| 728 |
+
}
|
| 729 |
+
else:
|
| 730 |
+
general_gate = check_gate_files(
|
| 731 |
+
candidate_results_path=general_candidate_path,
|
| 732 |
+
baseline_results_path=general_baseline_path,
|
| 733 |
+
goals=general_goals,
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
passed = skill_gate.get("passed") and (
|
| 737 |
+
general_gate is None or general_gate.get("passed")
|
| 738 |
+
)
|
| 739 |
+
checks = list(skill_gate.get("checks", []))
|
| 740 |
+
if general_gate:
|
| 741 |
+
for check in general_gate.get("checks", []):
|
| 742 |
+
checks.append({**check, "check": f"general: {check['check']}"})
|
| 743 |
+
|
| 744 |
+
return {
|
| 745 |
+
"passed": passed,
|
| 746 |
+
"checks": checks,
|
| 747 |
+
"skill": skill_gate,
|
| 748 |
+
"general": general_gate,
|
| 749 |
+
"task": skill_gate.get("task"),
|
| 750 |
+
"candidate_score": skill_gate.get("candidate_score"),
|
| 751 |
+
"baseline_score": skill_gate.get("baseline_score"),
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
|
| 755 |
def render_model_card(
|
| 756 |
*,
|
| 757 |
job: dict[str, Any],
|
|
|
|
| 800 |
"",
|
| 801 |
"## Benchmark gate",
|
| 802 |
"",
|
| 803 |
+
f"- skill eval profile: `{job.get('eval_profile')}`",
|
| 804 |
f"- gate: {'**PASSED**' if gate_result.get('passed') else '**FAILED**'}",
|
| 805 |
"",
|
|
|
|
|
|
|
| 806 |
]
|
| 807 |
+
|
| 808 |
+
def _gate_table(section: dict[str, Any] | None, *, prefix: str = "") -> list[str]:
|
| 809 |
+
if not section:
|
| 810 |
+
return []
|
| 811 |
+
out = [
|
| 812 |
+
f"### {prefix}checks".strip(),
|
| 813 |
+
"",
|
| 814 |
+
"| check | value | result |",
|
| 815 |
+
"| --- | ---: | --- |",
|
| 816 |
+
]
|
| 817 |
+
for c in section.get("checks", []):
|
| 818 |
+
out.append(
|
| 819 |
+
f"| {c['check']} | {_fmt(c['value'])} | {'pass' if c['ok'] else 'fail'} |"
|
| 820 |
+
)
|
| 821 |
+
if not section.get("checks"):
|
| 822 |
+
out.append("| — | — | — |")
|
| 823 |
+
out.append("")
|
| 824 |
+
return out
|
| 825 |
+
|
| 826 |
+
skill_section = gate_result.get("skill") or gate_result
|
| 827 |
+
lines.extend(_gate_table(skill_section, prefix="Skill "))
|
| 828 |
+
if gate_result.get("general"):
|
| 829 |
+
gen_profile = job.get("general_eval_profile") or GENERAL_EVAL_PROFILE
|
| 830 |
+
lines.append(f"- general eval profile: `{gen_profile}`")
|
| 831 |
+
lines.append("")
|
| 832 |
+
lines.extend(_gate_table(gate_result["general"], prefix="General "))
|
| 833 |
|
| 834 |
lines.extend(
|
| 835 |
[
|
research/modal/experiments.yaml
CHANGED
|
@@ -5,6 +5,10 @@
|
|
| 5 |
# checks the result against `goals`, and — only if the gate passes —
|
| 6 |
# publishes the adapter to `publish.hub_repo` on the Hugging Face Hub.
|
| 7 |
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
# Smoke limits (max_steps, max_samples, eval `limit` in the profile configs)
|
| 9 |
# keep hackathon runs affordable; bump them for full runs.
|
| 10 |
#
|
|
@@ -27,6 +31,22 @@ defaults:
|
|
| 27 |
max_steps: 100
|
| 28 |
# Hugging Face namespace for published adapters.
|
| 29 |
hub_org: MSGEncrypted
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
finetune:
|
| 32 |
# --- teaching: lesson-planning agent chat data (Well-Tuned primary) ---
|
|
@@ -62,14 +82,27 @@ finetune:
|
|
| 62 |
private: false
|
| 63 |
|
| 64 |
# --- science: factual + explanatory science tutoring ---
|
|
|
|
|
|
|
|
|
|
| 65 |
- name: science-lora
|
| 66 |
category: science
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
args:
|
|
|
|
|
|
|
|
|
|
| 70 |
early_stopping_patience: 2 # keep best eval_loss checkpoint, not the last
|
| 71 |
val_split: 0.05
|
| 72 |
-
description: Science tutor Q&A chat
|
| 73 |
eval_profile: science
|
| 74 |
goals:
|
| 75 |
task: sciq
|
|
|
|
| 5 |
# checks the result against `goals`, and — only if the gate passes —
|
| 6 |
# publishes the adapter to `publish.hub_repo` on the Hugging Face Hub.
|
| 7 |
#
|
| 8 |
+
# Baselines are always the unfine-tuned base model (`defaults.preset` →
|
| 9 |
+
# models.yaml model_id, e.g. openbmb/MiniCPM5-1B), stored as
|
| 10 |
+
# results/lm_eval/<preset>__baseline__<profile>/ and reused across jobs.
|
| 11 |
+
#
|
| 12 |
# Smoke limits (max_steps, max_samples, eval `limit` in the profile configs)
|
| 13 |
# keep hackathon runs affordable; bump them for full runs.
|
| 14 |
#
|
|
|
|
| 31 |
max_steps: 100
|
| 32 |
# Hugging Face namespace for published adapters.
|
| 33 |
hub_org: MSGEncrypted
|
| 34 |
+
# Second eval pass for publish gates: balanced general SLM mix (limit 100).
|
| 35 |
+
general_eval_profile: compare_study
|
| 36 |
+
general_goals:
|
| 37 |
+
guard_tasks:
|
| 38 |
+
- task: arc_easy
|
| 39 |
+
max_regress: 0.03
|
| 40 |
+
- task: arc_challenge
|
| 41 |
+
max_regress: 0.03
|
| 42 |
+
- task: hellaswag
|
| 43 |
+
max_regress: 0.03
|
| 44 |
+
- task: piqa
|
| 45 |
+
max_regress: 0.03
|
| 46 |
+
- task: boolq
|
| 47 |
+
max_regress: 0.03
|
| 48 |
+
- task: gsm8k
|
| 49 |
+
max_regress: 0.03
|
| 50 |
|
| 51 |
finetune:
|
| 52 |
# --- teaching: lesson-planning agent chat data (Well-Tuned primary) ---
|
|
|
|
| 82 |
private: false
|
| 83 |
|
| 84 |
# --- science: factual + explanatory science tutoring ---
|
| 85 |
+
# 10 local science-tutor chats overfit easily on their own; mix in alpaca
|
| 86 |
+
# replay + NEFTune + bigger LoRA rank (same recipe as teaching/math/language)
|
| 87 |
+
# so sciq recall improves instead of regressing from narrow chat-format drift.
|
| 88 |
- name: science-lora
|
| 89 |
category: science
|
| 90 |
+
max_steps: 150
|
| 91 |
+
mix:
|
| 92 |
+
- dataset: research/data/science-tutor-chat.jsonl
|
| 93 |
+
format: chat
|
| 94 |
+
weight: 16 # ~10 samples -> ~160 examples
|
| 95 |
+
- dataset: tatsu-lab/alpaca # general replay: protect sciq/arc_challenge
|
| 96 |
+
format: alpaca
|
| 97 |
+
dataset_split: "train[:600]"
|
| 98 |
+
max_samples: 600
|
| 99 |
args:
|
| 100 |
+
lora_r: 32
|
| 101 |
+
lora_alpha: 64
|
| 102 |
+
neftune_noise_alpha: 5
|
| 103 |
early_stopping_patience: 2 # keep best eval_loss checkpoint, not the last
|
| 104 |
val_split: 0.05
|
| 105 |
+
description: Science tutor Q&A chat + alpaca replay, r=32 + NEFTune
|
| 106 |
eval_profile: science
|
| 107 |
goals:
|
| 108 |
task: sciq
|
research/modal/finetune_app.py
CHANGED
|
@@ -36,16 +36,21 @@ for _candidate in (Path(__file__).resolve().parent, Path("/repo/research/modal")
|
|
| 36 |
sys.path.insert(0, str(_candidate))
|
| 37 |
|
| 38 |
from _common import ( # noqa: E402
|
| 39 |
-
BASE_MODEL_ID,
|
| 40 |
FINETUNE_VOL_PATH,
|
| 41 |
HF_CACHE_PATH,
|
| 42 |
LM_EVAL_OUTPUT,
|
|
|
|
| 43 |
build_finetune_cmd,
|
| 44 |
build_lm_eval_cmd,
|
| 45 |
check_gate_files,
|
|
|
|
| 46 |
commit_volumes,
|
| 47 |
config_for_profile,
|
|
|
|
|
|
|
| 48 |
finetune_vol,
|
|
|
|
|
|
|
| 49 |
hf_cache_vol,
|
| 50 |
hf_secret,
|
| 51 |
image,
|
|
@@ -53,11 +58,15 @@ from _common import ( # noqa: E402
|
|
| 53 |
job_plan_rows,
|
| 54 |
parse_json_object,
|
| 55 |
prepare_jobs,
|
|
|
|
|
|
|
| 56 |
split_csv,
|
| 57 |
publish_adapter_files,
|
| 58 |
pull_artifacts,
|
|
|
|
| 59 |
reload_volumes,
|
| 60 |
repo_env,
|
|
|
|
| 61 |
)
|
| 62 |
|
| 63 |
APP_NAME = "slm-finetune-benchmark"
|
|
@@ -118,7 +127,7 @@ def run_lm_eval(
|
|
| 118 |
seed: int | None = None,
|
| 119 |
) -> dict[str, Any]:
|
| 120 |
"""Run slm-lm-eval on base model or finetuned checkpoint."""
|
| 121 |
-
|
| 122 |
|
| 123 |
if adapter_path:
|
| 124 |
adapter_dir = Path(adapter_path)
|
|
@@ -183,9 +192,21 @@ def check_gate(
|
|
| 183 |
candidate_results_path: str,
|
| 184 |
baseline_results_path: str | None,
|
| 185 |
goals: dict[str, Any],
|
|
|
|
|
|
|
|
|
|
| 186 |
) -> dict[str, Any]:
|
| 187 |
-
"""Check
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
return check_gate_files(
|
| 190 |
candidate_results_path=candidate_results_path,
|
| 191 |
baseline_results_path=baseline_results_path,
|
|
@@ -207,7 +228,7 @@ def publish_adapter(
|
|
| 207 |
baseline_results_path: str | None,
|
| 208 |
) -> dict[str, Any]:
|
| 209 |
"""Write a model card and push the adapter to the Hub, but only if the gate passed."""
|
| 210 |
-
|
| 211 |
return publish_adapter_files(
|
| 212 |
job=job,
|
| 213 |
adapter_dir=adapter_dir,
|
|
@@ -284,17 +305,29 @@ def main(
|
|
| 284 |
print(json.dumps({"preset": preset, "jobs": plan_rows}, indent=2))
|
| 285 |
return
|
| 286 |
|
| 287 |
-
profile_names =
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
result = run_lm_eval.remote(
|
| 294 |
-
experiment_name=
|
| 295 |
config=config_for_profile(profile),
|
| 296 |
preset=preset,
|
| 297 |
-
tasks=
|
| 298 |
limit=eval_limit,
|
| 299 |
num_fewshot=eval_num_fewshot,
|
| 300 |
batch_size=eval_batch_size,
|
|
@@ -304,6 +337,9 @@ def main(
|
|
| 304 |
)
|
| 305 |
print(json.dumps(result, indent=2))
|
| 306 |
baselines_ok[profile] = bool(result.get("ok"))
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
train_results: dict[str, dict[str, Any]] = {}
|
| 309 |
if train and not eval_only:
|
|
@@ -325,6 +361,7 @@ def main(
|
|
| 325 |
|
| 326 |
print("--- post-train lm-eval / gate / publish ---")
|
| 327 |
summary: list[dict[str, Any]] = []
|
|
|
|
| 328 |
for j in prepared:
|
| 329 |
job_name = j["name"]
|
| 330 |
profile = j.get("eval_profile", "compare_study")
|
|
@@ -333,17 +370,18 @@ def main(
|
|
| 333 |
train_payload["output_dir"] if train_payload else f"{FINETUNE_VOL_PATH}/{job_name}"
|
| 334 |
)
|
| 335 |
|
| 336 |
-
baseline_path = f"{LM_EVAL_OUTPUT}/{preset
|
| 337 |
compare_to = baseline_path if baselines_ok.get(profile) else None
|
|
|
|
| 338 |
|
| 339 |
exp_name = f"{job_name}__{profile}"
|
| 340 |
eval_result = run_lm_eval.remote(
|
| 341 |
experiment_name=exp_name,
|
| 342 |
config=config_for_profile(profile),
|
| 343 |
-
model_path=
|
| 344 |
adapter_path=adapter_path,
|
| 345 |
compare_to=compare_to,
|
| 346 |
-
tasks=
|
| 347 |
limit=eval_limit,
|
| 348 |
num_fewshot=eval_num_fewshot,
|
| 349 |
batch_size=eval_batch_size,
|
|
@@ -353,20 +391,58 @@ def main(
|
|
| 353 |
)
|
| 354 |
print(json.dumps(eval_result, indent=2))
|
| 355 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
row: dict[str, Any] = {
|
| 357 |
"name": job_name,
|
| 358 |
"category": j.get("category"),
|
| 359 |
"profile": profile,
|
|
|
|
| 360 |
"plan": next((p for p in plan_rows if p["name"] == job_name), None),
|
| 361 |
}
|
| 362 |
|
| 363 |
gate_result: dict[str, Any] | None = None
|
| 364 |
if j.get("goals"):
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
gate_result = check_gate.remote(
|
| 367 |
candidate_results_path=eval_result["results_json"],
|
| 368 |
baseline_results_path=baseline_path,
|
| 369 |
goals=j["goals"],
|
|
|
|
|
|
|
|
|
|
| 370 |
)
|
| 371 |
print(json.dumps(gate_result, indent=2))
|
| 372 |
row["gate_passed"] = bool(gate_result and gate_result.get("passed"))
|
|
@@ -388,6 +464,8 @@ def main(
|
|
| 388 |
|
| 389 |
if pull:
|
| 390 |
pull_artifacts(job_name, exp_name)
|
|
|
|
|
|
|
| 391 |
|
| 392 |
_print_summary(summary)
|
| 393 |
|
|
@@ -404,14 +482,26 @@ def publish_only(job: str):
|
|
| 404 |
|
| 405 |
preset = defaults.get("preset", "minicpm5-1b")
|
| 406 |
profile = j.get("eval_profile", "compare_study")
|
|
|
|
|
|
|
| 407 |
adapter_path = f"{FINETUNE_VOL_PATH}/{job}"
|
| 408 |
-
candidate_results_path =
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
gate_result = check_gate.remote(
|
| 412 |
candidate_results_path=candidate_results_path,
|
| 413 |
baseline_results_path=baseline_results_path,
|
| 414 |
goals=j["goals"],
|
|
|
|
|
|
|
|
|
|
| 415 |
)
|
| 416 |
print(json.dumps(gate_result, indent=2))
|
| 417 |
|
|
|
|
| 36 |
sys.path.insert(0, str(_candidate))
|
| 37 |
|
| 38 |
from _common import ( # noqa: E402
|
|
|
|
| 39 |
FINETUNE_VOL_PATH,
|
| 40 |
HF_CACHE_PATH,
|
| 41 |
LM_EVAL_OUTPUT,
|
| 42 |
+
baseline_profiles_for_jobs,
|
| 43 |
build_finetune_cmd,
|
| 44 |
build_lm_eval_cmd,
|
| 45 |
check_gate_files,
|
| 46 |
+
check_publish_gate_files,
|
| 47 |
commit_volumes,
|
| 48 |
config_for_profile,
|
| 49 |
+
discover_cached_baselines,
|
| 50 |
+
eval_paths,
|
| 51 |
finetune_vol,
|
| 52 |
+
general_eval_profile,
|
| 53 |
+
general_goals_for_job,
|
| 54 |
hf_cache_vol,
|
| 55 |
hf_secret,
|
| 56 |
image,
|
|
|
|
| 58 |
job_plan_rows,
|
| 59 |
parse_json_object,
|
| 60 |
prepare_jobs,
|
| 61 |
+
profiles_needing_baseline_run,
|
| 62 |
+
resolve_base_model_id,
|
| 63 |
split_csv,
|
| 64 |
publish_adapter_files,
|
| 65 |
pull_artifacts,
|
| 66 |
+
reload_finetune_volume,
|
| 67 |
reload_volumes,
|
| 68 |
repo_env,
|
| 69 |
+
baseline_experiment_name,
|
| 70 |
)
|
| 71 |
|
| 72 |
APP_NAME = "slm-finetune-benchmark"
|
|
|
|
| 127 |
seed: int | None = None,
|
| 128 |
) -> dict[str, Any]:
|
| 129 |
"""Run slm-lm-eval on base model or finetuned checkpoint."""
|
| 130 |
+
reload_finetune_volume()
|
| 131 |
|
| 132 |
if adapter_path:
|
| 133 |
adapter_dir = Path(adapter_path)
|
|
|
|
| 192 |
candidate_results_path: str,
|
| 193 |
baseline_results_path: str | None,
|
| 194 |
goals: dict[str, Any],
|
| 195 |
+
general_candidate_results_path: str | None = None,
|
| 196 |
+
general_baseline_results_path: str | None = None,
|
| 197 |
+
general_goals: dict[str, Any] | None = None,
|
| 198 |
) -> dict[str, Any]:
|
| 199 |
+
"""Check skill + general lm-eval results against publish goals."""
|
| 200 |
+
reload_finetune_volume()
|
| 201 |
+
if general_goals:
|
| 202 |
+
return check_publish_gate_files(
|
| 203 |
+
skill_candidate_path=candidate_results_path,
|
| 204 |
+
skill_baseline_path=baseline_results_path,
|
| 205 |
+
skill_goals=goals,
|
| 206 |
+
general_candidate_path=general_candidate_results_path,
|
| 207 |
+
general_baseline_path=general_baseline_results_path,
|
| 208 |
+
general_goals=general_goals,
|
| 209 |
+
)
|
| 210 |
return check_gate_files(
|
| 211 |
candidate_results_path=candidate_results_path,
|
| 212 |
baseline_results_path=baseline_results_path,
|
|
|
|
| 228 |
baseline_results_path: str | None,
|
| 229 |
) -> dict[str, Any]:
|
| 230 |
"""Write a model card and push the adapter to the Hub, but only if the gate passed."""
|
| 231 |
+
reload_finetune_volume()
|
| 232 |
return publish_adapter_files(
|
| 233 |
job=job,
|
| 234 |
adapter_dir=adapter_dir,
|
|
|
|
| 305 |
print(json.dumps({"preset": preset, "jobs": plan_rows}, indent=2))
|
| 306 |
return
|
| 307 |
|
| 308 |
+
profile_names = baseline_profiles_for_jobs(prepared, defaults)
|
| 309 |
|
| 310 |
+
eval_task_list = split_csv(eval_tasks)
|
| 311 |
+
baselines_ok = discover_cached_baselines(
|
| 312 |
+
profile_names,
|
| 313 |
+
preset=preset,
|
| 314 |
+
eval_tasks=eval_task_list,
|
| 315 |
+
eval_limit=eval_limit,
|
| 316 |
+
eval_num_fewshot=eval_num_fewshot,
|
| 317 |
+
eval_seed=eval_seed,
|
| 318 |
+
)
|
| 319 |
+
missing_baselines = profiles_needing_baseline_run(
|
| 320 |
+
profile_names, baselines_ok, skip_baseline=skip_baseline
|
| 321 |
+
)
|
| 322 |
+
if missing_baselines:
|
| 323 |
+
print(f"--- base-model baselines ({', '.join(missing_baselines)}) ---")
|
| 324 |
+
for profile in missing_baselines:
|
| 325 |
+
exp = baseline_experiment_name(preset, profile)
|
| 326 |
result = run_lm_eval.remote(
|
| 327 |
+
experiment_name=exp,
|
| 328 |
config=config_for_profile(profile),
|
| 329 |
preset=preset,
|
| 330 |
+
tasks=eval_task_list,
|
| 331 |
limit=eval_limit,
|
| 332 |
num_fewshot=eval_num_fewshot,
|
| 333 |
batch_size=eval_batch_size,
|
|
|
|
| 337 |
)
|
| 338 |
print(json.dumps(result, indent=2))
|
| 339 |
baselines_ok[profile] = bool(result.get("ok"))
|
| 340 |
+
elif any(baselines_ok.values()):
|
| 341 |
+
cached = [p for p in profile_names if baselines_ok.get(p)]
|
| 342 |
+
print(f"--- base-model baselines: reusing cached ({', '.join(cached)}) ---")
|
| 343 |
|
| 344 |
train_results: dict[str, dict[str, Any]] = {}
|
| 345 |
if train and not eval_only:
|
|
|
|
| 361 |
|
| 362 |
print("--- post-train lm-eval / gate / publish ---")
|
| 363 |
summary: list[dict[str, Any]] = []
|
| 364 |
+
gen_profile = general_eval_profile(defaults)
|
| 365 |
for j in prepared:
|
| 366 |
job_name = j["name"]
|
| 367 |
profile = j.get("eval_profile", "compare_study")
|
|
|
|
| 370 |
train_payload["output_dir"] if train_payload else f"{FINETUNE_VOL_PATH}/{job_name}"
|
| 371 |
)
|
| 372 |
|
| 373 |
+
baseline_path = f"{LM_EVAL_OUTPUT}/{baseline_experiment_name(preset, profile)}/results.json"
|
| 374 |
compare_to = baseline_path if baselines_ok.get(profile) else None
|
| 375 |
+
base_model_id = resolve_base_model_id(j, defaults)
|
| 376 |
|
| 377 |
exp_name = f"{job_name}__{profile}"
|
| 378 |
eval_result = run_lm_eval.remote(
|
| 379 |
experiment_name=exp_name,
|
| 380 |
config=config_for_profile(profile),
|
| 381 |
+
model_path=base_model_id,
|
| 382 |
adapter_path=adapter_path,
|
| 383 |
compare_to=compare_to,
|
| 384 |
+
tasks=eval_task_list,
|
| 385 |
limit=eval_limit,
|
| 386 |
num_fewshot=eval_num_fewshot,
|
| 387 |
batch_size=eval_batch_size,
|
|
|
|
| 391 |
)
|
| 392 |
print(json.dumps(eval_result, indent=2))
|
| 393 |
|
| 394 |
+
general_goals = general_goals_for_job(j, defaults)
|
| 395 |
+
general_eval_result: dict[str, Any] | None = None
|
| 396 |
+
general_candidate_path: str | None = None
|
| 397 |
+
general_baseline_path: str | None = None
|
| 398 |
+
if general_goals:
|
| 399 |
+
general_baseline_path = (
|
| 400 |
+
f"{LM_EVAL_OUTPUT}/{baseline_experiment_name(preset, gen_profile)}/results.json"
|
| 401 |
+
)
|
| 402 |
+
gen_compare_to = (
|
| 403 |
+
general_baseline_path if baselines_ok.get(gen_profile) else None
|
| 404 |
+
)
|
| 405 |
+
gen_exp_name = f"{job_name}__{gen_profile}"
|
| 406 |
+
general_eval_result = run_lm_eval.remote(
|
| 407 |
+
experiment_name=gen_exp_name,
|
| 408 |
+
config=config_for_profile(gen_profile),
|
| 409 |
+
model_path=base_model_id,
|
| 410 |
+
adapter_path=adapter_path,
|
| 411 |
+
compare_to=gen_compare_to,
|
| 412 |
+
tasks=eval_task_list,
|
| 413 |
+
limit=eval_limit,
|
| 414 |
+
num_fewshot=eval_num_fewshot,
|
| 415 |
+
batch_size=eval_batch_size,
|
| 416 |
+
device=eval_device,
|
| 417 |
+
dtype=eval_dtype,
|
| 418 |
+
seed=eval_seed,
|
| 419 |
+
)
|
| 420 |
+
print(json.dumps(general_eval_result, indent=2))
|
| 421 |
+
general_candidate_path = general_eval_result["results_json"]
|
| 422 |
+
|
| 423 |
row: dict[str, Any] = {
|
| 424 |
"name": job_name,
|
| 425 |
"category": j.get("category"),
|
| 426 |
"profile": profile,
|
| 427 |
+
"general_profile": gen_profile if general_goals else None,
|
| 428 |
"plan": next((p for p in plan_rows if p["name"] == job_name), None),
|
| 429 |
}
|
| 430 |
|
| 431 |
gate_result: dict[str, Any] | None = None
|
| 432 |
if j.get("goals"):
|
| 433 |
+
skill_ok = bool(eval_result.get("ok"))
|
| 434 |
+
general_ok = (
|
| 435 |
+
not general_goals
|
| 436 |
+
or bool(general_eval_result and general_eval_result.get("ok"))
|
| 437 |
+
)
|
| 438 |
+
if skill_ok and general_ok:
|
| 439 |
gate_result = check_gate.remote(
|
| 440 |
candidate_results_path=eval_result["results_json"],
|
| 441 |
baseline_results_path=baseline_path,
|
| 442 |
goals=j["goals"],
|
| 443 |
+
general_candidate_results_path=general_candidate_path,
|
| 444 |
+
general_baseline_results_path=general_baseline_path,
|
| 445 |
+
general_goals=general_goals,
|
| 446 |
)
|
| 447 |
print(json.dumps(gate_result, indent=2))
|
| 448 |
row["gate_passed"] = bool(gate_result and gate_result.get("passed"))
|
|
|
|
| 464 |
|
| 465 |
if pull:
|
| 466 |
pull_artifacts(job_name, exp_name)
|
| 467 |
+
if general_goals and general_eval_result:
|
| 468 |
+
pull_artifacts(job_name, f"{job_name}__{gen_profile}", dest="models/finetuned")
|
| 469 |
|
| 470 |
_print_summary(summary)
|
| 471 |
|
|
|
|
| 482 |
|
| 483 |
preset = defaults.get("preset", "minicpm5-1b")
|
| 484 |
profile = j.get("eval_profile", "compare_study")
|
| 485 |
+
gen_profile = general_eval_profile(defaults)
|
| 486 |
+
general_goals = general_goals_for_job(j, defaults)
|
| 487 |
adapter_path = f"{FINETUNE_VOL_PATH}/{job}"
|
| 488 |
+
candidate_results_path, baseline_results_path, _ = eval_paths(
|
| 489 |
+
job_name=job, preset=preset, profile=profile
|
| 490 |
+
)
|
| 491 |
+
general_candidate_path = None
|
| 492 |
+
general_baseline_path = None
|
| 493 |
+
if general_goals:
|
| 494 |
+
general_candidate_path, general_baseline_path, _ = eval_paths(
|
| 495 |
+
job_name=job, preset=preset, profile=gen_profile
|
| 496 |
+
)
|
| 497 |
|
| 498 |
gate_result = check_gate.remote(
|
| 499 |
candidate_results_path=candidate_results_path,
|
| 500 |
baseline_results_path=baseline_results_path,
|
| 501 |
goals=j["goals"],
|
| 502 |
+
general_candidate_results_path=general_candidate_path,
|
| 503 |
+
general_baseline_results_path=general_baseline_path,
|
| 504 |
+
general_goals=general_goals,
|
| 505 |
)
|
| 506 |
print(json.dumps(gate_result, indent=2))
|
| 507 |
|
research/modal/server_app.py
CHANGED
|
@@ -44,7 +44,6 @@ for _candidate in (Path(__file__).resolve().parent, Path("/repo/research/modal")
|
|
| 44 |
sys.path.insert(0, str(_candidate))
|
| 45 |
|
| 46 |
from _common import ( # noqa: E402
|
| 47 |
-
BASE_MODEL_ID,
|
| 48 |
DEFAULT_GPU,
|
| 49 |
DEFAULT_KEEPALIVE_HOURS,
|
| 50 |
DEFAULT_SCALEDOWN_WINDOW,
|
|
@@ -52,22 +51,31 @@ from _common import ( # noqa: E402
|
|
| 52 |
FINETUNE_VOL_PATH,
|
| 53 |
HF_CACHE_PATH,
|
| 54 |
LM_EVAL_OUTPUT,
|
| 55 |
-
|
|
|
|
| 56 |
build_finetune_cmd,
|
| 57 |
build_lm_eval_cmd,
|
| 58 |
check_gate_files,
|
|
|
|
| 59 |
commit_volumes,
|
| 60 |
config_for_profile,
|
|
|
|
|
|
|
| 61 |
finetune_vol,
|
|
|
|
|
|
|
| 62 |
hf_cache_vol,
|
| 63 |
hf_secret,
|
| 64 |
image,
|
| 65 |
job_plan_rows,
|
| 66 |
parse_json_object,
|
| 67 |
prepare_jobs,
|
|
|
|
|
|
|
| 68 |
split_csv,
|
| 69 |
publish_adapter_files,
|
| 70 |
pull_artifacts,
|
|
|
|
| 71 |
reload_volumes,
|
| 72 |
repo_env,
|
| 73 |
)
|
|
@@ -177,7 +185,9 @@ class GpuWorker:
|
|
| 177 |
"""Run slm-lm-eval on base model or finetuned checkpoint."""
|
| 178 |
# Pick up adapters committed by another container (e.g. a separate
|
| 179 |
# eval-only invocation) — the warm container's mount may predate them.
|
| 180 |
-
|
|
|
|
|
|
|
| 181 |
|
| 182 |
if adapter_path:
|
| 183 |
adapter_dir = Path(adapter_path)
|
|
@@ -241,8 +251,20 @@ class GpuWorker:
|
|
| 241 |
candidate_results_path: str,
|
| 242 |
baseline_results_path: str | None,
|
| 243 |
goals: dict[str, Any],
|
|
|
|
|
|
|
|
|
|
| 244 |
) -> dict[str, Any]:
|
| 245 |
-
"""Check
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
return check_gate_files(
|
| 247 |
candidate_results_path=candidate_results_path,
|
| 248 |
baseline_results_path=baseline_results_path,
|
|
@@ -313,40 +335,38 @@ class GpuWorker:
|
|
| 313 |
raise ValueError("No jobs matched the requested filters")
|
| 314 |
|
| 315 |
preset = defaults.get("preset", "minicpm5-1b")
|
| 316 |
-
profile_names =
|
| 317 |
plan = job_plan_rows(prepared)
|
| 318 |
if plan_only:
|
| 319 |
return {"preset": preset, "jobs": plan}
|
| 320 |
|
| 321 |
-
baselines_ok
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
)
|
| 349 |
-
baselines_ok[profile] = bool(result.get("ok"))
|
| 350 |
|
| 351 |
train_results: dict[str, dict[str, Any]] = {}
|
| 352 |
if train and not eval_only:
|
|
@@ -354,6 +374,7 @@ class GpuWorker:
|
|
| 354 |
train_results[j["name"]] = self.finetune.local(j)
|
| 355 |
|
| 356 |
rows: list[dict[str, Any]] = []
|
|
|
|
| 357 |
for j in prepared:
|
| 358 |
job_name = j["name"]
|
| 359 |
profile = j.get("eval_profile", "compare_study")
|
|
@@ -364,14 +385,15 @@ class GpuWorker:
|
|
| 364 |
else f"{FINETUNE_VOL_PATH}/{job_name}"
|
| 365 |
)
|
| 366 |
|
| 367 |
-
baseline_path = f"{LM_EVAL_OUTPUT}/{preset
|
| 368 |
compare_to = baseline_path if baselines_ok.get(profile) else None
|
|
|
|
| 369 |
|
| 370 |
exp_name = f"{job_name}__{profile}"
|
| 371 |
eval_result = self.lm_eval.local(
|
| 372 |
experiment_name=exp_name,
|
| 373 |
config=config_for_profile(profile),
|
| 374 |
-
model_path=
|
| 375 |
adapter_path=adapter_path,
|
| 376 |
compare_to=compare_to,
|
| 377 |
tasks=eval_tasks,
|
|
@@ -383,21 +405,60 @@ class GpuWorker:
|
|
| 383 |
seed=eval_seed,
|
| 384 |
)
|
| 385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
row: dict[str, Any] = {
|
| 387 |
"name": job_name,
|
| 388 |
"category": j.get("category"),
|
| 389 |
"profile": profile,
|
|
|
|
| 390 |
"plan": next((p for p in plan if p["name"] == job_name), None),
|
| 391 |
"eval": eval_result,
|
| 392 |
}
|
|
|
|
|
|
|
| 393 |
|
| 394 |
gate_result: dict[str, Any] | None = None
|
| 395 |
if j.get("goals"):
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
gate_result = self.check_gate.local(
|
| 398 |
candidate_results_path=eval_result["results_json"],
|
| 399 |
baseline_results_path=baseline_path,
|
| 400 |
goals=j["goals"],
|
|
|
|
|
|
|
|
|
|
| 401 |
)
|
| 402 |
row["gate"] = gate_result
|
| 403 |
|
|
@@ -515,14 +576,26 @@ def main(
|
|
| 515 |
|
| 516 |
preset = defaults.get("preset", "minicpm5-1b")
|
| 517 |
profile = j.get("eval_profile", "compare_study")
|
|
|
|
|
|
|
| 518 |
adapter_path = f"{FINETUNE_VOL_PATH}/{job}"
|
| 519 |
-
candidate_results_path =
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
|
| 522 |
gate_result = worker.check_gate.remote(
|
| 523 |
candidate_results_path=candidate_results_path,
|
| 524 |
baseline_results_path=baseline_results_path,
|
| 525 |
goals=j["goals"],
|
|
|
|
|
|
|
|
|
|
| 526 |
)
|
| 527 |
print(json.dumps(gate_result, indent=2))
|
| 528 |
|
|
@@ -570,6 +643,12 @@ def main(
|
|
| 570 |
if pull:
|
| 571 |
for row in result.get("jobs", []):
|
| 572 |
pull_artifacts(row["name"], f"{row['name']}__{row['profile']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
return
|
| 574 |
|
| 575 |
if serve:
|
|
|
|
| 44 |
sys.path.insert(0, str(_candidate))
|
| 45 |
|
| 46 |
from _common import ( # noqa: E402
|
|
|
|
| 47 |
DEFAULT_GPU,
|
| 48 |
DEFAULT_KEEPALIVE_HOURS,
|
| 49 |
DEFAULT_SCALEDOWN_WINDOW,
|
|
|
|
| 51 |
FINETUNE_VOL_PATH,
|
| 52 |
HF_CACHE_PATH,
|
| 53 |
LM_EVAL_OUTPUT,
|
| 54 |
+
baseline_experiment_name,
|
| 55 |
+
baseline_profiles_for_jobs,
|
| 56 |
build_finetune_cmd,
|
| 57 |
build_lm_eval_cmd,
|
| 58 |
check_gate_files,
|
| 59 |
+
check_publish_gate_files,
|
| 60 |
commit_volumes,
|
| 61 |
config_for_profile,
|
| 62 |
+
discover_cached_baselines,
|
| 63 |
+
eval_paths,
|
| 64 |
finetune_vol,
|
| 65 |
+
general_eval_profile,
|
| 66 |
+
general_goals_for_job,
|
| 67 |
hf_cache_vol,
|
| 68 |
hf_secret,
|
| 69 |
image,
|
| 70 |
job_plan_rows,
|
| 71 |
parse_json_object,
|
| 72 |
prepare_jobs,
|
| 73 |
+
profiles_needing_baseline_run,
|
| 74 |
+
resolve_base_model_id,
|
| 75 |
split_csv,
|
| 76 |
publish_adapter_files,
|
| 77 |
pull_artifacts,
|
| 78 |
+
reload_finetune_volume,
|
| 79 |
reload_volumes,
|
| 80 |
repo_env,
|
| 81 |
)
|
|
|
|
| 185 |
"""Run slm-lm-eval on base model or finetuned checkpoint."""
|
| 186 |
# Pick up adapters committed by another container (e.g. a separate
|
| 187 |
# eval-only invocation) — the warm container's mount may predate them.
|
| 188 |
+
# Only finetune_vol is needed here; reloading hf-cache can fail when
|
| 189 |
+
# hf-xet keeps log files open on the warm container's HF cache mount.
|
| 190 |
+
reload_finetune_volume()
|
| 191 |
|
| 192 |
if adapter_path:
|
| 193 |
adapter_dir = Path(adapter_path)
|
|
|
|
| 251 |
candidate_results_path: str,
|
| 252 |
baseline_results_path: str | None,
|
| 253 |
goals: dict[str, Any],
|
| 254 |
+
general_candidate_results_path: str | None = None,
|
| 255 |
+
general_baseline_results_path: str | None = None,
|
| 256 |
+
general_goals: dict[str, Any] | None = None,
|
| 257 |
) -> dict[str, Any]:
|
| 258 |
+
"""Check skill + general lm-eval results against publish goals."""
|
| 259 |
+
if general_goals:
|
| 260 |
+
return check_publish_gate_files(
|
| 261 |
+
skill_candidate_path=candidate_results_path,
|
| 262 |
+
skill_baseline_path=baseline_results_path,
|
| 263 |
+
skill_goals=goals,
|
| 264 |
+
general_candidate_path=general_candidate_results_path,
|
| 265 |
+
general_baseline_path=general_baseline_results_path,
|
| 266 |
+
general_goals=general_goals,
|
| 267 |
+
)
|
| 268 |
return check_gate_files(
|
| 269 |
candidate_results_path=candidate_results_path,
|
| 270 |
baseline_results_path=baseline_results_path,
|
|
|
|
| 335 |
raise ValueError("No jobs matched the requested filters")
|
| 336 |
|
| 337 |
preset = defaults.get("preset", "minicpm5-1b")
|
| 338 |
+
profile_names = baseline_profiles_for_jobs(prepared, defaults)
|
| 339 |
plan = job_plan_rows(prepared)
|
| 340 |
if plan_only:
|
| 341 |
return {"preset": preset, "jobs": plan}
|
| 342 |
|
| 343 |
+
baselines_ok = discover_cached_baselines(
|
| 344 |
+
profile_names,
|
| 345 |
+
preset=preset,
|
| 346 |
+
eval_tasks=eval_tasks,
|
| 347 |
+
eval_limit=eval_limit,
|
| 348 |
+
eval_num_fewshot=eval_num_fewshot,
|
| 349 |
+
eval_seed=eval_seed,
|
| 350 |
+
)
|
| 351 |
+
missing_baselines = profiles_needing_baseline_run(
|
| 352 |
+
profile_names, baselines_ok, skip_baseline=skip_baseline
|
| 353 |
+
)
|
| 354 |
+
for profile in missing_baselines:
|
| 355 |
+
exp = baseline_experiment_name(preset, profile)
|
| 356 |
+
cfg_path = config_for_profile(profile)
|
| 357 |
+
result = self.lm_eval.local(
|
| 358 |
+
experiment_name=exp,
|
| 359 |
+
config=cfg_path,
|
| 360 |
+
preset=preset,
|
| 361 |
+
tasks=eval_tasks,
|
| 362 |
+
limit=eval_limit,
|
| 363 |
+
num_fewshot=eval_num_fewshot,
|
| 364 |
+
batch_size=eval_batch_size,
|
| 365 |
+
device=eval_device,
|
| 366 |
+
dtype=eval_dtype,
|
| 367 |
+
seed=eval_seed,
|
| 368 |
+
)
|
| 369 |
+
baselines_ok[profile] = bool(result.get("ok"))
|
|
|
|
|
|
|
| 370 |
|
| 371 |
train_results: dict[str, dict[str, Any]] = {}
|
| 372 |
if train and not eval_only:
|
|
|
|
| 374 |
train_results[j["name"]] = self.finetune.local(j)
|
| 375 |
|
| 376 |
rows: list[dict[str, Any]] = []
|
| 377 |
+
gen_profile = general_eval_profile(defaults)
|
| 378 |
for j in prepared:
|
| 379 |
job_name = j["name"]
|
| 380 |
profile = j.get("eval_profile", "compare_study")
|
|
|
|
| 385 |
else f"{FINETUNE_VOL_PATH}/{job_name}"
|
| 386 |
)
|
| 387 |
|
| 388 |
+
baseline_path = f"{LM_EVAL_OUTPUT}/{baseline_experiment_name(preset, profile)}/results.json"
|
| 389 |
compare_to = baseline_path if baselines_ok.get(profile) else None
|
| 390 |
+
base_model_id = resolve_base_model_id(j, defaults)
|
| 391 |
|
| 392 |
exp_name = f"{job_name}__{profile}"
|
| 393 |
eval_result = self.lm_eval.local(
|
| 394 |
experiment_name=exp_name,
|
| 395 |
config=config_for_profile(profile),
|
| 396 |
+
model_path=base_model_id,
|
| 397 |
adapter_path=adapter_path,
|
| 398 |
compare_to=compare_to,
|
| 399 |
tasks=eval_tasks,
|
|
|
|
| 405 |
seed=eval_seed,
|
| 406 |
)
|
| 407 |
|
| 408 |
+
general_goals = general_goals_for_job(j, defaults)
|
| 409 |
+
general_eval_result: dict[str, Any] | None = None
|
| 410 |
+
general_candidate_path: str | None = None
|
| 411 |
+
general_baseline_path: str | None = None
|
| 412 |
+
if general_goals:
|
| 413 |
+
general_baseline_path = (
|
| 414 |
+
f"{LM_EVAL_OUTPUT}/{baseline_experiment_name(preset, gen_profile)}/results.json"
|
| 415 |
+
)
|
| 416 |
+
gen_compare_to = (
|
| 417 |
+
general_baseline_path if baselines_ok.get(gen_profile) else None
|
| 418 |
+
)
|
| 419 |
+
gen_exp_name = f"{job_name}__{gen_profile}"
|
| 420 |
+
general_eval_result = self.lm_eval.local(
|
| 421 |
+
experiment_name=gen_exp_name,
|
| 422 |
+
config=config_for_profile(gen_profile),
|
| 423 |
+
model_path=base_model_id,
|
| 424 |
+
adapter_path=adapter_path,
|
| 425 |
+
compare_to=gen_compare_to,
|
| 426 |
+
tasks=eval_tasks,
|
| 427 |
+
limit=eval_limit,
|
| 428 |
+
num_fewshot=eval_num_fewshot,
|
| 429 |
+
batch_size=eval_batch_size,
|
| 430 |
+
device=eval_device,
|
| 431 |
+
dtype=eval_dtype,
|
| 432 |
+
seed=eval_seed,
|
| 433 |
+
)
|
| 434 |
+
general_candidate_path = general_eval_result["results_json"]
|
| 435 |
+
|
| 436 |
row: dict[str, Any] = {
|
| 437 |
"name": job_name,
|
| 438 |
"category": j.get("category"),
|
| 439 |
"profile": profile,
|
| 440 |
+
"general_profile": gen_profile if general_goals else None,
|
| 441 |
"plan": next((p for p in plan if p["name"] == job_name), None),
|
| 442 |
"eval": eval_result,
|
| 443 |
}
|
| 444 |
+
if general_eval_result:
|
| 445 |
+
row["general_eval"] = general_eval_result
|
| 446 |
|
| 447 |
gate_result: dict[str, Any] | None = None
|
| 448 |
if j.get("goals"):
|
| 449 |
+
skill_ok = bool(eval_result.get("ok"))
|
| 450 |
+
general_ok = (
|
| 451 |
+
not general_goals
|
| 452 |
+
or bool(general_eval_result and general_eval_result.get("ok"))
|
| 453 |
+
)
|
| 454 |
+
if skill_ok and general_ok:
|
| 455 |
gate_result = self.check_gate.local(
|
| 456 |
candidate_results_path=eval_result["results_json"],
|
| 457 |
baseline_results_path=baseline_path,
|
| 458 |
goals=j["goals"],
|
| 459 |
+
general_candidate_results_path=general_candidate_path,
|
| 460 |
+
general_baseline_results_path=general_baseline_path,
|
| 461 |
+
general_goals=general_goals,
|
| 462 |
)
|
| 463 |
row["gate"] = gate_result
|
| 464 |
|
|
|
|
| 576 |
|
| 577 |
preset = defaults.get("preset", "minicpm5-1b")
|
| 578 |
profile = j.get("eval_profile", "compare_study")
|
| 579 |
+
gen_profile = general_eval_profile(defaults)
|
| 580 |
+
general_goals = general_goals_for_job(j, defaults)
|
| 581 |
adapter_path = f"{FINETUNE_VOL_PATH}/{job}"
|
| 582 |
+
candidate_results_path, baseline_results_path, _ = eval_paths(
|
| 583 |
+
job_name=job, preset=preset, profile=profile
|
| 584 |
+
)
|
| 585 |
+
general_candidate_path = None
|
| 586 |
+
general_baseline_path = None
|
| 587 |
+
if general_goals:
|
| 588 |
+
general_candidate_path, general_baseline_path, _ = eval_paths(
|
| 589 |
+
job_name=job, preset=preset, profile=gen_profile
|
| 590 |
+
)
|
| 591 |
|
| 592 |
gate_result = worker.check_gate.remote(
|
| 593 |
candidate_results_path=candidate_results_path,
|
| 594 |
baseline_results_path=baseline_results_path,
|
| 595 |
goals=j["goals"],
|
| 596 |
+
general_candidate_results_path=general_candidate_path,
|
| 597 |
+
general_baseline_results_path=general_baseline_path,
|
| 598 |
+
general_goals=general_goals,
|
| 599 |
)
|
| 600 |
print(json.dumps(gate_result, indent=2))
|
| 601 |
|
|
|
|
| 643 |
if pull:
|
| 644 |
for row in result.get("jobs", []):
|
| 645 |
pull_artifacts(row["name"], f"{row['name']}__{row['profile']}")
|
| 646 |
+
if row.get("general_profile"):
|
| 647 |
+
pull_artifacts(
|
| 648 |
+
row["name"],
|
| 649 |
+
f"{row['name']}__{row['general_profile']}",
|
| 650 |
+
dest="models/finetuned",
|
| 651 |
+
)
|
| 652 |
return
|
| 653 |
|
| 654 |
if serve:
|
research/modal/tests/test_modal_common.py
CHANGED
|
@@ -1,13 +1,23 @@
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
|
|
|
| 3 |
import sys
|
| 4 |
|
| 5 |
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
|
| 6 |
|
| 7 |
from research.modal._common import ( # noqa: E402
|
|
|
|
|
|
|
|
|
|
| 8 |
build_finetune_cmd,
|
| 9 |
build_lm_eval_cmd,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
prepare_jobs,
|
|
|
|
|
|
|
| 11 |
split_csv,
|
| 12 |
)
|
| 13 |
|
|
@@ -74,3 +84,100 @@ def test_prepare_jobs_filters_and_applies_finetune_overrides():
|
|
| 74 |
def test_split_csv_trims_empty_values():
|
| 75 |
assert split_csv(" math, science ,,code ") == ["math", "science", "code"]
|
| 76 |
assert split_csv(None) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
| 3 |
+
import json
|
| 4 |
import sys
|
| 5 |
|
| 6 |
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
|
| 7 |
|
| 8 |
from research.modal._common import ( # noqa: E402
|
| 9 |
+
COMMON_ENV,
|
| 10 |
+
baseline_experiment_name,
|
| 11 |
+
baseline_profiles_for_jobs,
|
| 12 |
build_finetune_cmd,
|
| 13 |
build_lm_eval_cmd,
|
| 14 |
+
check_publish_gate_files,
|
| 15 |
+
discover_cached_baselines,
|
| 16 |
+
evaluate_gate,
|
| 17 |
+
general_goals_for_job,
|
| 18 |
prepare_jobs,
|
| 19 |
+
profiles_needing_baseline_run,
|
| 20 |
+
resolve_base_model_id,
|
| 21 |
split_csv,
|
| 22 |
)
|
| 23 |
|
|
|
|
| 84 |
def test_split_csv_trims_empty_values():
|
| 85 |
assert split_csv(" math, science ,,code ") == ["math", "science", "code"]
|
| 86 |
assert split_csv(None) is None
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _results(task_scores: dict[str, float]) -> dict:
|
| 90 |
+
return {
|
| 91 |
+
"results": {
|
| 92 |
+
task: {"acc,none": score, "acc_stderr,none": 0.01}
|
| 93 |
+
for task, score in task_scores.items()
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def test_evaluate_gate_guard_only_goals():
|
| 99 |
+
candidate = _results({"arc_easy": 0.5, "hellaswag": 0.4})
|
| 100 |
+
baseline = _results({"arc_easy": 0.52, "hellaswag": 0.41})
|
| 101 |
+
goals = {
|
| 102 |
+
"guard_tasks": [
|
| 103 |
+
{"task": "arc_easy", "max_regress": 0.03},
|
| 104 |
+
{"task": "hellaswag", "max_regress": 0.03},
|
| 105 |
+
]
|
| 106 |
+
}
|
| 107 |
+
gate = evaluate_gate(candidate=candidate, baseline=baseline, goals=goals)
|
| 108 |
+
assert gate["passed"] is True
|
| 109 |
+
assert len(gate["checks"]) == 2
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def test_check_publish_gate_requires_both_skill_and_general(tmp_path):
|
| 113 |
+
skill_cand = tmp_path / "skill_cand.json"
|
| 114 |
+
skill_base = tmp_path / "skill_base.json"
|
| 115 |
+
general_cand = tmp_path / "general_cand.json"
|
| 116 |
+
general_base = tmp_path / "general_base.json"
|
| 117 |
+
skill_cand.write_text(
|
| 118 |
+
json.dumps(_results({"gsm8k": 0.4}))
|
| 119 |
+
)
|
| 120 |
+
skill_base.write_text(
|
| 121 |
+
json.dumps(_results({"gsm8k": 0.33}))
|
| 122 |
+
)
|
| 123 |
+
general_cand.write_text(
|
| 124 |
+
json.dumps(_results({"arc_easy": 0.5, "hellaswag": 0.4}))
|
| 125 |
+
)
|
| 126 |
+
general_base.write_text(
|
| 127 |
+
json.dumps(_results({"arc_easy": 0.52, "hellaswag": 0.41}))
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
gate = check_publish_gate_files(
|
| 131 |
+
skill_candidate_path=str(skill_cand),
|
| 132 |
+
skill_baseline_path=str(skill_base),
|
| 133 |
+
skill_goals={"task": "gsm8k", "min_improve": 0.02},
|
| 134 |
+
general_candidate_path=str(general_cand),
|
| 135 |
+
general_baseline_path=str(general_base),
|
| 136 |
+
general_goals={
|
| 137 |
+
"guard_tasks": [{"task": "arc_easy", "max_regress": 0.03}]
|
| 138 |
+
},
|
| 139 |
+
)
|
| 140 |
+
assert gate["passed"] is True
|
| 141 |
+
assert gate["skill"]["passed"] is True
|
| 142 |
+
assert gate["general"]["passed"] is True
|
| 143 |
+
assert any(c["check"].startswith("general:") for c in gate["checks"])
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def test_baseline_profiles_include_general_for_publishable_jobs():
|
| 147 |
+
_, jobs = prepare_jobs(job="math-lora")
|
| 148 |
+
defaults = {"general_eval_profile": "compare_study", "general_goals": {"guard_tasks": []}}
|
| 149 |
+
profiles = baseline_profiles_for_jobs(jobs, defaults)
|
| 150 |
+
assert "math" in profiles
|
| 151 |
+
assert "compare_study" in profiles
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def test_general_goals_only_for_publishable_jobs():
|
| 155 |
+
_, math_jobs = prepare_jobs(job="math-lora")
|
| 156 |
+
_, local_jobs = prepare_jobs(job="alpaca-lora")
|
| 157 |
+
defaults = {"general_goals": {"guard_tasks": [{"task": "piqa", "max_regress": 0.03}]}}
|
| 158 |
+
assert general_goals_for_job(math_jobs[0], defaults) is not None
|
| 159 |
+
assert general_goals_for_job(local_jobs[0], defaults) is None
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def test_resolve_base_model_id_from_preset():
|
| 163 |
+
_, jobs = prepare_jobs(job="math-lora")
|
| 164 |
+
defaults, job = {}, jobs[0]
|
| 165 |
+
assert resolve_base_model_id(job, defaults) == "openbmb/MiniCPM5-1B"
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def test_profiles_needing_baseline_run_respects_skip_and_cache():
|
| 169 |
+
cached = {"math": True, "compare_study": False}
|
| 170 |
+
assert profiles_needing_baseline_run(
|
| 171 |
+
["math", "compare_study"], cached, skip_baseline=False
|
| 172 |
+
) == ["compare_study"]
|
| 173 |
+
assert profiles_needing_baseline_run(
|
| 174 |
+
["math", "compare_study"], cached, skip_baseline=True
|
| 175 |
+
) == []
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def test_baseline_experiment_name_uses_preset():
|
| 179 |
+
assert baseline_experiment_name("minicpm5-1b", "math") == "minicpm5-1b__baseline__math"
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def test_common_env_redirects_xet_logs_off_hf_cache_volume():
|
| 183 |
+
assert COMMON_ENV["HF_XET_LOG_DEST"] == "/tmp/xet-logs/"
|