File size: 6,303 Bytes
071ba6b
 
 
 
 
 
 
9e58a11
 
 
 
 
 
28d7ac6
 
eae16b1
 
 
071ba6b
 
 
 
 
 
 
 
 
8787bd3
 
 
fffaeea
 
 
 
 
 
 
 
8787bd3
 
071ba6b
8787bd3
071ba6b
8787bd3
071ba6b
 
707d9ee
 
 
 
 
 
 
7c1bf5c
 
 
071ba6b
 
 
 
 
 
 
 
 
9e58a11
 
83021ff
 
071ba6b
83021ff
071ba6b
 
83021ff
9e58a11
071ba6b
 
 
 
9e58a11
 
 
 
 
 
 
 
071ba6b
83021ff
071ba6b
 
 
83021ff
7ad981e
9e58a11
7ad981e
9e58a11
 
 
 
 
7ad981e
071ba6b
 
 
 
8787bd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/bin/bash
# Full SFT + GRPO pipeline inside HF Job. Repo cloned at /w.
# Requires secrets: HF_TOKEN, WANDB_API_KEY
set -e
cd /w
export PYTHONPATH="/w:${PYTHONPATH}"

# Force single-GPU training. On a100-large (1x A100-80GB) this is a no-op,
# but kept as a defensive guardrail in case we ever fall back to a 2x flavor:
# TRL's SFTTrainer entropy logging path on multi-GPU crashes with
# `RuntimeError: tensor a (2) must match tensor b (4) at non-singleton
# dimension 0` because per-rank logits get mixed with the full-batch
# attention mask. Hiding extra GPUs completely sidesteps this.
export CUDA_VISIBLE_DEVICES=0

# Install huggingface_hub first so we can download data files (parity with job_smoke.sh)
pip install -q 'huggingface_hub>=0.28'

# Fetch large data files from HF (uploaded separately via hf upload)
mkdir -p data
python -c "from huggingface_hub import hf_hub_download; [hf_hub_download(repo_id='Pratham-math/fathom-code', filename=f'data/{f}', local_dir='/w', token='${HF_TOKEN}') for f in ['train.jsonl','eval.jsonl','sft_traces.jsonl']]"
ls -la data/

# Base runtime deps (these don't conflict)
pip install -q openenv-core fastapi 'uvicorn[standard]' pydantic RestrictedPython tiktoken httpx
pip install -q hydra-core omegaconf wandb 'huggingface_hub>=0.28' tyro

# Core ML deps.
# NOTE: trl 1.2 imports `is_trackio_available` from transformers, which is not
# present in 4.49.0. Use a newer transformers line in jobs.
# bitsandbytes pin notes:
#  - 0.45.1: ships cu124 only β€” breaks under vllm's cu128 torch.
#  - 0.47.x: cu128 + cu124 dual-wheel, but vllm's BitsAndBytesLinearMethod
#    gates on `bnb >= 0.48.1` (vllm/model_executor/layers/quantization/
#    bitsandbytes.py:_check_bitsandbytes_version) and raises ImportError
#    during `LLM(...)` init when used with a 4-bit model.
#  - >=0.48.1: satisfies both vllm's gate and our cu128 wheel needs.
pip install -q transformers==4.56.2 accelerate==1.5.2 peft==0.14.0 'bitsandbytes>=0.48.1'
pip install -q datasets==4.7.0
# Keep TRL pinned for OpenEnv path but bypass resolver deadlock with datasets pin.
pip install -q --no-deps trl==1.2.0
pip install -q vllm==0.18.0

# Optional transitive deps often required by quantized loaders / datasets stack
pip install -q safetensors sentencepiece einops scipy xxhash protobuf pyyaml fsspec aiohttp dill multiprocess pyarrow requests filelock packaging tokenizers regex tqdm

# Plotting deps for scripts/make_plots.py β€” NOT in the base pytorch image.
# Without these the plot step crashes with ModuleNotFoundError and judges lose
# the reward/loss curves on the model repo (they're regenerable locally from
# the job log via scripts/parse_log_to_plots.py, but remote-side is the
# happy path we want to keep working).
pip install -q matplotlib

# flash-attn removed: source build is the largest single memory spike during install.
# vLLM + transformers fall back to PyTorch SDPA without it (small throughput cost).
echo "flash-attn intentionally skipped to avoid build-time OOM"

nohup uvicorn env.server.app:app --host 0.0.0.0 --port 8001 > env.log 2>&1 &
sleep 10
python -c "import urllib.request, sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8001/healthz', timeout=5).status==200 else 1)" || (echo "env down"; cat env.log; exit 1)

# Smoke first β€” fail fast if pipeline broken
python -m train.smoke_test --env-url http://localhost:8001
grep -q "VERDICT: GO" outputs/smoke/SMOKE_RESULT.md || (cat outputs/smoke/SMOKE_RESULT.md; exit 1)

# SFT (Path 1 β€” production 1.5B). qwen_1_5b config points at
# unsloth/Qwen2.5-Coder-1.5B-Instruct-bnb-4bit per STACK Β§3.1.
# NOTE: use initialize_config_dir (absolute path) β€” heredocs have no caller file,
# so the relative `../configs` resolves against CWD and breaks. Absolute is robust.
python <<'PY'
from hydra import initialize_config_dir, compose
from train.model_load import load_model_and_tokenizer
from train.sft import run_sft
with initialize_config_dir(config_dir="/w/configs", version_base="1.3"):
    cfg = compose(config_name="config", overrides=["model=qwen_1_5b","train=sft"])
m, t = load_model_and_tokenizer(cfg)
print("SFT adapter:", run_sft(cfg, m, t))
PY

# GRPO 200 steps (Path 1 β€” production 1.5B on A100-80GB).
# Memory budget on a100-large (~80GB):
#  - 1.5B 4-bit + LoRA-r16 weights + optim state β‰ˆ 6-8 GB
#  - vLLM colocate KV cache @ num_generations=8, max_prompt=4096, max_completion=1024 β‰ˆ 25-35 GB
#  - vllm_gpu_memory_utilization=0.50 β†’ leaves ~30 GB for trainer + activations
# Ramping max_steps from 50 β†’ 200 to give the reward curve room to climb
# (50 steps was a smoke; 200 is enough to show the gate-crossing transition
# from format_gate=0 β†’ format_gate=1 β†’ composite reward rising).
python <<'PY'
from hydra import initialize_config_dir, compose
from train.model_load import load_model_and_tokenizer
from train.grpo import run_grpo
from rewards.compose import make_reward_fn
with initialize_config_dir(config_dir="/w/configs", version_base="1.3"):
    cfg = compose(config_name="config", overrides=[
        "model=qwen_1_5b",
        "train=grpo",
        "train.max_steps=200",
        "train.num_generations=8",
        "train.max_prompt_length=4096",
        "train.max_completion_length=1024",
        "train.vllm_gpu_memory_utilization=0.50",
    ])
m, t = load_model_and_tokenizer(cfg)
print("GRPO merged:", run_grpo(cfg, m, t, make_reward_fn(cfg.reward), "http://localhost:8001"))
PY

# Generate reward + loss curve PNGs from the training run (judges need these)
python /w/scripts/make_plots.py || echo "plot generation failed (non-fatal)"

# Push artifacts + plots to a PUBLIC model repo (judges must access)
HF_USER=$(python -c "from huggingface_hub import HfApi; print(HfApi(token='${HF_TOKEN}').whoami()['name'])")
python <<PY
from huggingface_hub import HfApi
api = HfApi(token="${HF_TOKEN}")
repo = "${HF_USER}/fathom-1.5b-grpo"
api.create_repo(repo, repo_type="model", exist_ok=True, private=False)
import os
for sub in ["sft_adapter", "grpo_merged_16bit", "plots"]:
    if os.path.isdir(f"outputs/{sub}"):
        api.upload_folder(folder_path=f"outputs/{sub}", path_in_repo=sub, repo_id=repo, repo_type="model")
        print(f"Uploaded outputs/{sub} -> {repo}/{sub}")
print(f"Trained model + plots: https://huggingface.co/{repo}")
PY
echo "DONE"