feat(train): flip Path 3 -> Path 1 — production 1.5B GRPO on a100-large
Browse filesPrevious job (69ece94a, a10g-largex2) loaded
unsloth/Qwen2.5-Coder-0.5B-Instruct-bnb-4bit because job_train.sh
hardcoded `model=qwen_0_5b_smoke` and never got switched to the
production 1.5B target documented in HACKATHON_FINAL_PLAN.md and
PROJECT.md ("A 1.5B model trained on our env solves 200K-token QA
tasks via recursion"). Submission was about to ship as 0.5B.
This commit switches both the SFT and GRPO blocks to:
model=qwen_1_5b -> unsloth/Qwen2.5-Coder-1.5B-Instruct-bnb-4bit
(LoRA r16, max_seq=16384)
train.max_steps=200 -> enough room for the format-gate-crossing
transition + visible reward climb (50 was a
smoke; previous 50-step run with broken
sys_msg had reward=0 every step).
Memory budget retuned for the bigger model on the bigger GPU:
num_generations 4 -> 8 (back to TRL/STACK default)
max_prompt_length 2048 -> 4096 (full prompt budget)
max_completion_length 512 -> 1024 (long-form answers fit)
vllm_gpu_mem_util 0.35 -> 0.50 (A100-80GB has the headroom)
Hardware: scripts/hf_jobs_helper.py default flavor moves
a10g-largex2 -> a100-large
The CUDA_VISIBLE_DEVICES=0 guardrail stays as a defensive no-op for
single-GPU instances (still trips the multi-rank entropy bug if we
ever fall back to a 2x flavor).
Estimated run: SFT ~12 min + GRPO 200 steps ~75 min = ~90 min total,
~$5-7 on HF Jobs a100-large.
Made-with: Cursor
- scripts/hf_jobs_helper.py +6 -1
- scripts/job_train.sh +23 -19
|
@@ -75,10 +75,15 @@ def submit() -> None:
|
|
| 75 |
'git clone -b main https://oauth2:$HF_TOKEN@huggingface.co/Pratham-math/fathom-code /w && '
|
| 76 |
'bash /w/scripts/job_train.sh',
|
| 77 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
job = api.run_job(
|
| 79 |
image="pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel",
|
| 80 |
command=cmd,
|
| 81 |
-
flavor=
|
| 82 |
namespace=USER,
|
| 83 |
secrets={
|
| 84 |
"HF_TOKEN": os.environ["HF_TOKEN"],
|
|
|
|
| 75 |
'git clone -b main https://oauth2:$HF_TOKEN@huggingface.co/Pratham-math/fathom-code /w && '
|
| 76 |
'bash /w/scripts/job_train.sh',
|
| 77 |
]
|
| 78 |
+
# a100-large = 1x A100-80GB. Required for the 1.5B production run:
|
| 79 |
+
# 1.5B 4-bit weights + LoRA + grad/optim state + vLLM colocate KV cache
|
| 80 |
+
# for num_generations=8 @ max_completion=1024 needs ~30-40 GB.
|
| 81 |
+
# The previous a10g-largex2 (24 GB usable on 1 GPU) only held 0.5B safely.
|
| 82 |
+
flavor = os.environ.get("HF_JOB_FLAVOR", "a100-large")
|
| 83 |
job = api.run_job(
|
| 84 |
image="pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel",
|
| 85 |
command=cmd,
|
| 86 |
+
flavor=flavor,
|
| 87 |
namespace=USER,
|
| 88 |
secrets={
|
| 89 |
"HF_TOKEN": os.environ["HF_TOKEN"],
|
|
@@ -5,11 +5,12 @@ set -e
|
|
| 5 |
cd /w
|
| 6 |
export PYTHONPATH="/w:${PYTHONPATH}"
|
| 7 |
|
| 8 |
-
# Force single-GPU training.
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
#
|
|
|
|
| 13 |
export CUDA_VISIBLE_DEVICES=0
|
| 14 |
|
| 15 |
# Install huggingface_hub first so we can download data files (parity with job_smoke.sh)
|
|
@@ -62,7 +63,8 @@ python -c "import urllib.request, sys; sys.exit(0 if urllib.request.urlopen('htt
|
|
| 62 |
python -m train.smoke_test --env-url http://localhost:8001
|
| 63 |
grep -q "VERDICT: GO" outputs/smoke/SMOKE_RESULT.md || (cat outputs/smoke/SMOKE_RESULT.md; exit 1)
|
| 64 |
|
| 65 |
-
# SFT (Path
|
|
|
|
| 66 |
# NOTE: use initialize_config_dir (absolute path) — heredocs have no caller file,
|
| 67 |
# so the relative `../configs` resolves against CWD and breaks. Absolute is robust.
|
| 68 |
python <<'PY'
|
|
@@ -70,17 +72,19 @@ from hydra import initialize_config_dir, compose
|
|
| 70 |
from train.model_load import load_model_and_tokenizer
|
| 71 |
from train.sft import run_sft
|
| 72 |
with initialize_config_dir(config_dir="/w/configs", version_base="1.3"):
|
| 73 |
-
cfg = compose(config_name="config", overrides=["model=
|
| 74 |
m, t = load_model_and_tokenizer(cfg)
|
| 75 |
print("SFT adapter:", run_sft(cfg, m, t))
|
| 76 |
PY
|
| 77 |
|
| 78 |
-
# GRPO
|
| 79 |
-
# Memory
|
| 80 |
-
# -
|
| 81 |
-
# -
|
| 82 |
-
# -
|
| 83 |
-
#
|
|
|
|
|
|
|
| 84 |
python <<'PY'
|
| 85 |
from hydra import initialize_config_dir, compose
|
| 86 |
from train.model_load import load_model_and_tokenizer
|
|
@@ -88,13 +92,13 @@ from train.grpo import run_grpo
|
|
| 88 |
from rewards.compose import make_reward_fn
|
| 89 |
with initialize_config_dir(config_dir="/w/configs", version_base="1.3"):
|
| 90 |
cfg = compose(config_name="config", overrides=[
|
| 91 |
-
"model=
|
| 92 |
"train=grpo",
|
| 93 |
-
"train.max_steps=
|
| 94 |
-
"train.num_generations=
|
| 95 |
-
"train.max_prompt_length=
|
| 96 |
-
"train.max_completion_length=
|
| 97 |
-
"train.vllm_gpu_memory_utilization=0.
|
| 98 |
])
|
| 99 |
m, t = load_model_and_tokenizer(cfg)
|
| 100 |
print("GRPO merged:", run_grpo(cfg, m, t, make_reward_fn(cfg.reward), "http://localhost:8001"))
|
|
|
|
| 5 |
cd /w
|
| 6 |
export PYTHONPATH="/w:${PYTHONPATH}"
|
| 7 |
|
| 8 |
+
# Force single-GPU training. On a100-large (1x A100-80GB) this is a no-op,
|
| 9 |
+
# but kept as a defensive guardrail in case we ever fall back to a 2x flavor:
|
| 10 |
+
# TRL's SFTTrainer entropy logging path on multi-GPU crashes with
|
| 11 |
+
# `RuntimeError: tensor a (2) must match tensor b (4) at non-singleton
|
| 12 |
+
# dimension 0` because per-rank logits get mixed with the full-batch
|
| 13 |
+
# attention mask. Hiding extra GPUs completely sidesteps this.
|
| 14 |
export CUDA_VISIBLE_DEVICES=0
|
| 15 |
|
| 16 |
# Install huggingface_hub first so we can download data files (parity with job_smoke.sh)
|
|
|
|
| 63 |
python -m train.smoke_test --env-url http://localhost:8001
|
| 64 |
grep -q "VERDICT: GO" outputs/smoke/SMOKE_RESULT.md || (cat outputs/smoke/SMOKE_RESULT.md; exit 1)
|
| 65 |
|
| 66 |
+
# SFT (Path 1 — production 1.5B). qwen_1_5b config points at
|
| 67 |
+
# unsloth/Qwen2.5-Coder-1.5B-Instruct-bnb-4bit per STACK §3.1.
|
| 68 |
# NOTE: use initialize_config_dir (absolute path) — heredocs have no caller file,
|
| 69 |
# so the relative `../configs` resolves against CWD and breaks. Absolute is robust.
|
| 70 |
python <<'PY'
|
|
|
|
| 72 |
from train.model_load import load_model_and_tokenizer
|
| 73 |
from train.sft import run_sft
|
| 74 |
with initialize_config_dir(config_dir="/w/configs", version_base="1.3"):
|
| 75 |
+
cfg = compose(config_name="config", overrides=["model=qwen_1_5b","train=sft"])
|
| 76 |
m, t = load_model_and_tokenizer(cfg)
|
| 77 |
print("SFT adapter:", run_sft(cfg, m, t))
|
| 78 |
PY
|
| 79 |
|
| 80 |
+
# GRPO 200 steps (Path 1 — production 1.5B on A100-80GB).
|
| 81 |
+
# Memory budget on a100-large (~80GB):
|
| 82 |
+
# - 1.5B 4-bit + LoRA-r16 weights + optim state ≈ 6-8 GB
|
| 83 |
+
# - vLLM colocate KV cache @ num_generations=8, max_prompt=4096, max_completion=1024 ≈ 25-35 GB
|
| 84 |
+
# - vllm_gpu_memory_utilization=0.50 → leaves ~30 GB for trainer + activations
|
| 85 |
+
# Ramping max_steps from 50 → 200 to give the reward curve room to climb
|
| 86 |
+
# (50 steps was a smoke; 200 is enough to show the gate-crossing transition
|
| 87 |
+
# from format_gate=0 → format_gate=1 → composite reward rising).
|
| 88 |
python <<'PY'
|
| 89 |
from hydra import initialize_config_dir, compose
|
| 90 |
from train.model_load import load_model_and_tokenizer
|
|
|
|
| 92 |
from rewards.compose import make_reward_fn
|
| 93 |
with initialize_config_dir(config_dir="/w/configs", version_base="1.3"):
|
| 94 |
cfg = compose(config_name="config", overrides=[
|
| 95 |
+
"model=qwen_1_5b",
|
| 96 |
"train=grpo",
|
| 97 |
+
"train.max_steps=200",
|
| 98 |
+
"train.num_generations=8",
|
| 99 |
+
"train.max_prompt_length=4096",
|
| 100 |
+
"train.max_completion_length=1024",
|
| 101 |
+
"train.vllm_gpu_memory_utilization=0.50",
|
| 102 |
])
|
| 103 |
m, t = load_model_and_tokenizer(cfg)
|
| 104 |
print("GRPO merged:", run_grpo(cfg, m, t, make_reward_fn(cfg.reward), "http://localhost:8001"))
|