File size: 6,522 Bytes
f0273c6
 
 
 
 
 
 
a7525f2
f0273c6
a7525f2
 
 
73712c7
 
 
 
 
 
a7525f2
 
 
 
f0273c6
 
 
a7525f2
 
 
 
 
 
 
f0273c6
 
 
 
 
a7525f2
f0273c6
a7525f2
 
f0273c6
 
 
 
 
 
 
a7525f2
 
 
f0273c6
a7525f2
73712c7
a7525f2
f0273c6
 
 
a7525f2
 
f0273c6
 
 
73712c7
a7525f2
f0273c6
 
 
 
a7525f2
 
f0273c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a7525f2
f0273c6
 
a7525f2
 
f0273c6
 
 
a7525f2
f0273c6
 
 
a7525f2
 
 
 
 
 
 
 
 
f0273c6
 
 
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
# OpenCode-only RL on LFM2.5-2.6B with LoRA

Reproduction of the OpenCode-only run from
[The ultimate guide to multi-harness RL](https://huggingface.co/spaces/FineEnvs/multi-harness-rl),
but training a **LoRA adapter** instead of full weights, so the result is a small,
distributable adapter on top of `LiquidAI/LFM2.5-2.6B`.

Stack: OpenEnv (capture proxy + Harbor integration) β†’ Harbor (tasks + **HF Sandboxes**) β†’
TRL `AsyncGRPOTrainer` with `HarnessRolloutWorker` β†’ vLLM policy server.
Every API here was verified against the released wheels (trl 1.16.0, openenv 0.10.0,
harbor 0.24.0), the `harbor_env` source, and the FineEnvs production env Space.

**Version pins that matter:** `trl==1.15.0` (1.16.0 made tree packing unconditional
in AsyncGRPOTrainer and refuses hybrid models β€” LFM2.5 has conv layers; 1.15.0 has
the harness worker but predates tree packing) and `vllm/vllm-openai:v0.31.0` (top
of trl 1.15.0's supported vLLM band, and predates its mcp>=2 requirement which
conflicts with openenv's fastmcp 3.x).

**Sandboxes are HF Sandboxes** (Harbor's `hf-sandbox` backend, harbor>=0.24): each
rollout boots an HF Jobs cpu-basic machine running the task's prebuilt image
(`savatar101/env-data-agent-train:base` β€” the same image every task's Dockerfile is
a one-line `FROM` of). Only `HF_TOKEN` is needed; no Daytona/E2B account.

## Why merged-weight sync (source-verified)

Generation goes harness β†’ capture proxy β†’ vLLM, addressed by a **fixed model name**.
TRL's adapter-only sync publishes versioned adapters (`trl-policy-v{N}`) and only the
trainer-built default worker learns the versioned name; an injected
`HarnessRolloutWorker` never gets one. So the server runs *without* `--enable-lora`
and the trainer merges the LoRA into the base and NCCL-streams full weights each step
(~5.2 GB for 2.6B β€” trivial on one node; vLLM also serves adapters 1.33–1.39x slower
per token). **Checkpoints still contain only the LoRA adapter** β€” that is what you ship.

## Files

| File | Purpose |
|---|---|
| `train_lora.py` | Worker wiring + `AsyncGRPOConfig` + `LoraConfig` (r=32 on q/k/v/out_proj, w1/2/3). Task dataset is discovered from the env server via `HarborSessionFactory.prompt_rows()` β€” `create()` rejects any prompt the server doesn't serve |
| `serve_policy.sh` | vLLM with capture-proxy flags + NCCL weight transfer |
| `serve_env.sh` | `openenv harbor serve` with production env vars (`MAX_CONCURRENT_ENVS`, pinned OpenCode 1.18.31, 4096 output cap) |
| `prepare_tasks.py` + `data/` | Materializes the pinned 1,000 train / 250 test tasks (hash-checked, from the FineEnvs Space) and patches `docker_image` into each task.toml β€” the one change HF Sandboxes require |
| `run_job.sh` | Single-container entrypoint for HF Jobs (setup + all 3 processes) |
| `export_adapter.py` | Publish any adapter checkpoint to the Hub (+ optional merged model) |

## Launch on HF Jobs

```bash
# 0. One-time: put this folder on the Hub so the job can pull it
hf repos create YOU/lfm-opencode-lora-rl-code --type model --private
hf upload YOU/lfm-opencode-lora-rl-code . \
  --include "*.py" --include "*.sh" --include "*.md" --include "data/*.json"

# 1. Smoke run (~1-2h): validates weight sync, capture contract, sandbox boots
hf jobs run vllm/vllm-openai:v0.31.0 --flavor h200x2 --timeout 6h --detach \
  --secrets HF_TOKEN="$(hf auth token)" \
  --env ADAPTER_REPO=YOU/LFM2.5-2.6B-opencode-lora-RL MAX_STEPS=20 \
  -- bash -c 'pip -q install huggingface_hub && hf download YOU/lfm-opencode-lora-rl-code --local-dir /app && bash /app/run_job.sh'

# 2. Watch: merged weight transfers (no .vllm_lora/), sandbox Jobs booting under
#    your account (hf jobs ps), rollouts returning rewards, no CaptureContractError.
hf jobs logs <job-id> --follow

# 3. Full run (the article's OpenCode-only run took ~32h on 2x H100)
hf jobs run vllm/vllm-openai:v0.31.0 --flavor h200x2 --timeout 72h --detach \
  --secrets HF_TOKEN="$(hf auth token)" \
  --env ADAPTER_REPO=YOU/LFM2.5-2.6B-opencode-lora-RL MAX_STEPS=1000 \
  -- bash -c 'pip -q install huggingface_hub && hf download YOU/lfm-opencode-lora-rl-code --local-dir /app && bash /app/run_job.sh'
```

Checkpoints (adapter-only) push to `ADAPTER_REPO` every 100 steps, so a dead job
loses little. `SANDBOX=daytona`/`e2b` + the matching API key secret still works.

## What you distribute

Every checkpoint contains `adapter_model.safetensors` + `adapter_config.json`
(~50 MB at r=32). `export_adapter.py` adds a model card and pushes:

```bash
python export_adapter.py checkpoints/checkpoint-1000 YOU/LFM2.5-2.6B-opencode-lora-RL [--also-merged]
```

Consumers load it with `PeftModel.from_pretrained("LiquidAI/LFM2.5-2.6B", YOU/...)` or
`vllm serve LiquidAI/LFM2.5-2.6B --enable-lora --max-lora-rank 32 --lora-modules opencode-lora=YOU/...`
(`Lfm2ForCausalLM` implements `SupportsLoRA`).

## Recipe choices carried over from the article (don't regress these)

- `top_p=1.0`, `top_k=0` β€” truncation biases the policy and breaks logprob agreement.
- Same 4096-token output cap in training and evaluation (`OPENENV_MAX_OUTPUT_TOKENS`
  + worker `max_tokens`). A Qwen run with 16k training answers collapsed at 4k eval.
- Correctness + <=0.1 tool-efficiency bonus on correct answers (else all-correct
  GRPO groups carry no signal and tool calls creep up).
- `num_generations=8`, one harness per group, so advantage compares actions.
- `max_tool_calling_iterations=32` on the worker β€” the loop-owning default would
  silently cap the agent at 8 model calls.
- `max_staleness=4` (config default) matches the article's async setup.

## Honest caveats

- `HarnessRolloutWorker` + `peft_config` and `sandbox="hf-sandbox"` are both
  untested-in-print combinations on this stack. All pieces are released and the
  interfaces line up (verified in source) β€” the smoke run exists to prove it before
  you spend 32+ GPU-hours.
- HF Sandboxes are experimental (per HF docs) and each rollout is an HF Job under
  your token: mind your Jobs concurrency quota (`MAX_CONCURRENT_ENVS=8` bounds it)
  and per-minute billing of thousands of short cpu-basic jobs over a full run.
- Sandbox boot time (job scheduling + first image pull) adds latency per rollout
  vs Daytona/E2B's warm pools; expect slower wall-clock than the article's 32h.
- Expect the article's single-harness shape: gains concentrate in OpenCode
  (34 β†’ 58% there in their run) and transfer unevenly elsewhere.
- LoRA r=32 may cap peak accuracy vs full FT; no published comparison on this stack.