| # Planting GRPO on OpenTSLM — runbook |
|
|
| Goal: (A) train OpenTSLM on **our** faithful rationales and compare to their baseline |
| rationales (downstream SFT experiment), then (B) add a **GRPO** stage with the dual |
| answer+faithfulness reward on top of the SFT model. |
|
|
| Everything here is GPU-blocked at *run* time (all 8 GPUs busy with ECG agent2 + other |
| users). The code/setup below is done; launch when a GPU frees. |
|
|
| --- |
|
|
| ## 0. Environment (no GPU needed to install) |
|
|
| Their stack: Python 3.12, torch ≥2.9.1, transformers ≥4.57, peft, open-flamingo, wfdb. |
| Keep it separate from our agent pipeline venv (which is 3.13). |
|
|
| ```bash |
| cd ~/Adinath/TimeAgent/OpenTSLM |
| python3.12 -m venv .venv_otslm |
| source .venv_otslm/bin/activate |
| pip install --upgrade pip |
| pip install -r requirements.txt # installs torch 2.9.x + open-flamingo + -e . |
| # HF access for the base LLM: |
| huggingface-cli login # needs a token with Llama-3.2-1B access, |
| # OR use the open google/gemma-3-270m instead |
| ``` |
|
|
| Decision — base LLM: `meta-llama/Llama-3.2-1B` (default, gated) vs `google/gemma-3-270m` |
| (open, smaller, faster to iterate). Recommend gemma-3-270m for the first end-to-end |
| smoke run, then Llama-3.2-1B for the paper numbers. |
|
|
| --- |
|
|
| ## A. Train on our faithful data (SFT: ours vs baseline) |
|
|
| OpenTSLM's HAR loader reads `src/data/har_cot/har_cot_{train,val,test}_cot.csv` |
| with columns `x_axis,y_axis,z_axis,label,rationale`. `compute_loss` teacher-forces on |
| `rationale`, so swapping that column is the whole intervention. |
|
|
| 1. Let their loader download the originals once (so we have the time series + labels): |
| ```bash |
| python -c "from opentslm.time_series_datasets.har_cot.har_cot_loader import ensure_har_cot_dataset; ensure_har_cot_dataset()" |
| ``` |
| 2. Inject our rationales (our faithful HAR CoT lives on anviksha: |
| `faithful_har_cot_{train,val,test}.csv`): |
| ```bash |
| for sp in train val test; do |
| python grpo/inject_faithful_data.py \ |
| --orig src/data/har_cot/har_cot_${sp}_cot.csv \ |
| --ours /path/to/faithful_har_cot_${sp}.csv \ |
| --ours-rationale-col our_rationale \ |
| --out src/data/har_cot/har_cot_${sp}_cot.csv --backup |
| done |
| ``` |
| (`--backup` keeps `*.orig.csv` so you can train the baseline arm too.) |
| 3. Train each arm and compare test macro-F1: |
| ```bash |
| # ours |
| python curriculum_learning.py --model OpenTSLMSP --stages stage3_cot --llm_id google/gemma-3-270m |
| # baseline: restore *.orig.csv into place, retrain into a different results dir |
| ``` |
| Results land under `results/<llm_id_safe>/OpenTSLMSP/stage3_cot/`. |
|
|
| The same pattern applies to Sleep (`stage4_sleep_cot`) and ECG (`stage5_ecg_cot`) once |
| those faithful CSVs are final; ECG uses `--key sample_id` in the injector. |
|
|
| --- |
|
|
| ## B. Add the GRPO stage (`stage6_grpo`) |
| |
| The pieces are in `grpo/`: |
| - `reward.py` — `dual_reward()` = 0.7·answer + 0.3·faithfulness (HAR scorer ready; |
| add ECG/Sleep/WESAD scorers from `codebase/agent3_*.py` patterns). |
| - `grpo_trainer.py` — `GRPOTrainer.grpo_loss(batch)`: samples N rollouts via |
| `model.generate`, scores them, group-normalizes advantages, returns a PG loss |
| (+ optional KL to a frozen SFT reference). |
|
|
| ### Three wiring edits in `curriculum_learning.py` |
| |
| 1. **Carry reward fields through the dataset.** The reward needs the gold label and the |
| signal facts per sample. In the GRPO dataset, attach to each item: |
| - `item["gold_label"]` = the activity/stage/answer label (already available pre-rationale), |
| - `item["facts"]` = the Stage-1 facts dict for that sample (load `signal_facts_*.json` |
| keyed by sample_id; add a `sample_id` column to the injected CSVs to join). |
| Build a `reward_fn(completion, item)` closure: |
| ```python |
| from grpo.reward import dual_reward, HAR_SCORER |
| reward_fn = lambda c, it: dual_reward(c, it["gold_label"], it["facts"], HAR_SCORER) |
| ``` |
| |
| 2. **Swap the train step.** In `_train_stage`'s inner loop (currently L1114–1121): |
| ```python |
| # SFT: |
| # optimizer.zero_grad(); loss = model.compute_loss(batch) |
| # loss.backward(); optimizer.step() |
| # GRPO: |
| optimizer.zero_grad() |
| loss, stats = grpo.grpo_loss(batch) # grpo = GRPOTrainer(model, reward_fn, cfg, ref_model) |
| loss.backward() |
| clip_grad_norm_(model.parameters(), 1.0) |
| optimizer.step() |
| ``` |
| Gate this on `stage_name == "stage6_grpo"` so SFT stages are untouched. Use a smaller |
| LR (e.g. 1e-6–5e-6) and a smaller batch (rollouts multiply cost by N). |
|
|
| 3. **Register the stage.** Add `"stage6_grpo"` to `CURRICULUM_STAGES` and a `stage6_grpo` |
| method mirroring `stage3_cot` (same HAR dataset, but it must start from the completed |
| `stage3_cot` checkpoint — GRPO needs a competent SFT policy first). Initialize |
| `ref_model` as a frozen clone of that checkpoint for the KL term (start with |
| `kl_coef=0.0`, raise if the policy degenerates). |
|
|
| ### GRPO config starting point |
| ```python |
| from grpo.grpo_trainer import GRPOConfig |
| GRPOConfig(num_rollouts=8, max_new_tokens=400, temperature=1.0, |
| kl_coef=0.0, w_answer=0.7, w_faith=0.3) |
| ``` |
|
|
| --- |
|
|
| ## Order of operations (when a GPU frees) |
| 1. Set up `.venv_otslm`, HF login, smoke-test with `demo/huggingface/03_test_hf_har_cot.py`. |
| 2. SFT arm A (ours) + arm B (baseline) on `stage3_cot`; record test macro-F1 → paper §Downstream. |
| 3. From arm A's checkpoint, run `stage6_grpo`; track `reward_mean / answer_reward / |
| faith_reward` from `grpo_loss` stats; evaluate faithfulness + accuracy vs the SFT model. |
| 4. Repeat for Sleep/ECG once their faithful data is final. |
|
|
| ## Open risks to watch |
| - `model.generate` must accept `num_return_sequences`/`do_sample` (they pass `**kwargs` |
| to `self.llm.generate`, so it should — verify on the smoke run). |
| - Rollout cost = N × generate per sample; keep batch small and consider fewer rollouts |
| (4–6) for the large splits. |
| - Reward needs per-sample facts; the cleanest join is a `sample_id` column threaded from |
| Stage-1 facts → injected CSV → dataset item. |
| ``` |
| |