Repository navigation
Add experimental posttrain_env prototype (RFC 014) - #1500
Conversation
…tall CPU-only torch in its evaluator
There was a problem hiding this comment.
Thanks for this. I checked out 8429a25 and ran the checks locally. Findings below; the RFC design itself is for maintainers to decide, so I list open questions rather than a verdict on it.
Non-blocking
server/eval_scripts/t0_arithmetic_format_eval.py:80rejects missing and mismatched keys but notunexpected_keys. Transformers logs unexpected key names, which the agent controls, andbroker.py:538takes the score from any log line starting withPOSTTRAIN_SCORE. The real score line is printed last, so I could not build an exploit, but rejecting unexpected keys (or writing the result to a file in the score job's/datainstead of logs) would close it.rfcs/014-environment-services.md:8links tohuggingface/OpenEnv; the repo now lives atopenenv/OpenEnv(the redirect works).- Decision 1 in the RFC cites only TRL's
AsyncGRPOTrainerfor staleness. A second framework, or a framework-neutral phrasing, would keep with the neutrality rule.
Open design questions
- Is the env server as broker (credential in the env process) acceptable for v1, given the server has no auth and must stay on a private network until RFC 009 lands?
submitblocks one WebSocket step for up toeval_timeout*60 + 900 + 240s. Is a long blocking step the right contract, or should scoring be async with a status poll (#1026)?- Should an orphaned job block the next
resetwith an error, or should the orchestrator get a typed state it can handle without a failed reset? - Reward is
Noneon a failed score job but0.0on step or time limits. Is that the split the trainers in RFC 013 expect? - Step and time limits are enforced only on
step, not on direct/mcpcalls. Is that acceptable for v1, or should the limits move into the tool path? - The no-network policy depends on a seccomp launcher because HF Jobs has no egress control. Should the RFC treat a native HF Jobs no-network option as a precondition for step 2, and should arbitrary PyPI wheels and public datasets stay allowed given the contamination risk the RFC names?
- The service manifest lives in
tasks.pyfor now. Is the proposed manifest shape andjobs_reserved prefix the one core should adopt in step 2?
…view notes Ending an episode on the WebSocket path waited for HF Jobs to confirm cancels on the shared event loop. Run it in a thread instead. The evaluator now rejects checkpoints with unexpected keys, and RFC 014 links to openenv/OpenEnv and describes trainer staleness without naming one framework.
|
Thanks @burtenshaw! The three non-blocking points are fixed in eb21148. The scope is the slice agreed with Sergio in #1382, and the open questions map to the RFC's plan:
|
Reserve budget and job slots under a thread-safe admission condition before HF submission, and reuse pending idempotent requests. Publish successful jobs atomically and release only the failed request reservation. Keep direct close retries off the event loop with retained broker ownership. Serialize reset with completion and reject stale completion workers so cancelled steps cannot end later episodes. Cover the reproduced races with fake-API concurrency tests.
…env#1500) Capture the scoring episode before provider calls and guard result publication. Retain request labels, reservations and idempotency ownership when creation reconciliation fails. Bound admission and teardown waits, cancel late creations, and report incomplete cleanup including jobs published after its snapshot.
sergiopaniego
left a comment
There was a problem hiding this comment.
thanks @rycerzes, great work! the scope matches what we agreed in #1382: batch jobs, dev_evaluate vs score and the per-rollout budget, with the other profiles moved to future work. the live runs table in the RFC is really useful too.
I tested it end to end on HF Jobs (my own namespace, at 83202a0):
- unit tests: 84 passed, 1 skipped (the Linux-only seccomp one).
t0_artifact, scripted: reward 1.0, $0.000167.- isolation probes inside an agent job: no
*TOKEN*in the env, uid 10000,AF_INET/AF_INET6/AF_NETLINK/AF_PACKET/AF_VSOCKsockets, a rawsocketsyscall andio_uring_setupall getEACCES, DNS fails, no Unix sockets in the container, pid 1's environ isn't readable and/base_modelis read-only. Nice job on the launcher. t0_arithmetic_format, scripted SFT ont4-small: 310 s, $0.040, dev 0.92, hidden split 0.92, close to your numbers. The ledger matched: $0.20 reserved, $0.040 settled, and the score job wasn't charged.- the example LLM agent (Qwen3.8 via Inference Providers): 19 turns, 3 agent jobs (the first failed on its own tokenizer bug and it recovered from the logs), reward 1.0, $0.37 of jobs (
gpu_s1283), no orphaned jobs andfinal: true. The final observation came throughstepwith the reward andservice_spendas expected.
Split the RFC from the env
The goal of the prototype was to have a real, working example of the design, and it does that really well. Since the RFC is a design decision and the env is an implementation, I'd land them as two PRs, like RFC 005 did:
- RFC PR: only
rfcs/014-environment-services.mdand therfcs/README.mdentry, with the../envs/posttrain_env/link pointing to the env PR. The "Prototype results" table can stay, since it's the evidence that the design works. No core changes needed: the core types (manifest, ledger, jobs) stay as step 2, in their own PR. Happy to approve this one as soon as it's up. - Env PR (this one):
posttrain_envas an experimental env. I'd merge it once:- the README "Client" section, the
PostTrainEnvdocstring and the RFC example useenv.step(CallToolAction(...))instead ofenv.call_tool(...).call_toolgoes through/mcp, so it skipsmax_stepsand the time limit (I checked: threecall_tools leavestep_countat 0) and never returns the final observation with the reward andservice_spend.examples/posttrain_env_agent.pyalready does it right. - the README says this is eval-first today (see below).
- it's labelled as experimental in the README and the docs env list, and stays out of the Spaces release and the collections.
- small one:
serverbinds0.0.0.0by default while the README says to keep it on localhost. Defaulting to127.0.0.1outside Docker would match.
- the README "Client" section, the
Then we keep iterating on it in main with smaller PRs: the first real task (something closer to PostTrainBench, where the hidden split can't be enumerated), reading the score from a file, and so on.
Provider neutrality (before step 2)
The three rules, the ledger, service_spend, the job states and the dev_evaluate / score split aren't HF-specific, which is great. The v1 mechanics are, though: run_uv_job, artifacts as folders in the jobs-artifacts bucket, per-minute billing from list_jobs_hardware, the Resource Group spend cap, and the seccomp launcher, which only exists because HF Jobs has no egress control (Modal, for example, seems to have native network blocking for sandboxes).
Since step 2 moves these types into core with a single backend, it'd be easy for the abstraction to end up shaped like HF Jobs. Two suggestions:
- add a short section to the RFC with the minimal contract a batch-job provider has to offer (submit, inspect, cancel, logs, price, artifacts), with HF Jobs as its first implementation.
- sketch a second backend before step 2. A local Docker one would be the cheapest and would also give us end-to-end tests that spend nothing. Modal would be a good check that the abstraction isn't HF-shaped.
Training / HF side
- README: say this is eval-first today. RFC decision 1 says post-training episodes are multi-hour and fall outside the staleness window of async GRPO trainers, so their near-term use is evaluation. The env README doesn't say that, and someone will try to plug
posttrain_envstraight into a trainer's rollout loop. A short "Using it with a trainer" note would help:submitblocks for up toscore_timeout_s(2700 s fort0_arithmetic_format), so the client's tool/message timeout has to be sized from that value, andmetadata["service_spend"]is what to forward ascosts. - Score from a file instead of logs. +1 to @burtenshaw's suggestion. Evaluator jobs already get a read-write
/datafolder in thejobs-artifactsbucket, and the broker already lists that bucket for artifacts. If the evaluator wrote/data/result.jsonand the broker read it from there, the log parsing (and anything the agent can make the loader print) would be out of the picture. Fine as a follow-up. - No-network flag on HF Jobs. Thanks for opening huggingface/huggingface_hub#5261, let's follow it there.
reward=Noneon a failed score job. Could the RFC say explicitly that this depends on RFC 013 (#1256), so it follows whatever 013 settles on?
On the open design questions
- Broker inside the env server: fine for v1, documented as private-network only.
- Blocking
submit: fine for v1. It keeps the reward inside the env, andscore_timeout_slets clients size their timeout. - Orphaned job blocks
reset: fine for v1. Failing loudly beats billing silently. The typed state can come with step 2. Nonevs0.0: agree with the split. A grader failure shouldn't penalize the policy.- Limits only on
step: fine for v1 while the server is private, once the docs point clients tostep(see above). Since enforcing them on/mcpis a core issue rather than aposttrain_envone, could you open an issue so it doesn't get lost? - Native no-network flag as a precondition for step 2: agree. Open wheels and datasets are fine for T0, but a real task with public test data will need a contamination check or an allowlist.
- Manifest and
jobs_prefix in core: not yet. I'd wait for the provider contract and a second backend (see above) before adopting them.
thanks again, really solid work 🚀
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 834c8c2. Configure here.
Serialize per-episode scoring and reuse an internal broker idempotency key through uncertain creation recovery. Pin the selected artifact before creation. Capture episode ownership before MCP scheduling, validate the request marker at dispatch, and keep broker ownership in tool results. Reproduces Bugbot findings 4391723507 and 4391723523 on 834c8c2: 11 regression cases fail before and pass after. Focused suite: 194 passed, 1 skipped. Existing adversarial probes: 15 passed. Core MCP suite: 117 passed. No real HF Jobs or spend.
Snapshot current episode ownership under the lifecycle lock at MCP dispatch instead of consulting the session-inherited ContextVar. Keep captured request ownership and nonce validation so stale requests remain rejected after reset. Add four deterministic regression cases for sessions opened by episode A and preopened controls, with budget and jobs_submit paused across reset. Verify fresh B creates exactly one job and stale A cannot mutate B. All 11 prior race regressions, 4 independent cases, 15 adversarial cases and 117 core MCP cases pass. Posttrain: 103 passed, 1 skipped; release/catalog/docs selection: 294 passed. No HF Jobs or spend.

Summary
The experimental, eval-first env-only prototype agreed in #1382: batch jobs on HF Jobs, an evaluator (
dev_evaluatevsscore) and a per-rollout budget. The agent post-trains a model by submitting scripts. The env server's broker runs them on HF Jobs with its own token. A grader-only job scores the submitted checkpoint on a hidden split, and that score is the reward. No core changes.cc @burtenshaw @sergiopaniego
Type of Change
What's in it
secrets={}.orphanedand blocks the nextreset.AF_UNIXsockets, and drops privileges. Inputs are read-only mounts: the pinned base model and the public datasets the agent asks for.reward=None), which RFC 013'supdateleaves out.service_spendin every observation, as a cost for RFC 013 trainers, never a sampling weight.examples/posttrain_env_agent.py.Compared with
huggingface_hubrun_uv_jobas is, including itsjobs-artifactsbucket and/datafolder.Volumemounts for inputs, andlist_jobs_hardware/inspect_jobfor prices and durations.hf jobs uv run, whose script header can requestHF_TOKENfrom the caller (source).run_jobonly hasnetwork_group. A native no-network option would replace the launcher.Known gaps
step, not on direct/mcpcalls.All three are in the README.
RFC Status
Test Plan
PYTHONPATH=src:envs uv run pytest tests/envs/test_posttrain_environment.py -v: 58 pass against a fake HF Jobs; one Linux-only seccomp test also passes in a container. The full suite passes apart from three timing tests that fail the same way onmainlocally.Live runs on HF Jobs, with no network in agent jobs (full table in the RFC):
t0_artifactt0_arithmetic_formatt4-smallt0_arithmetic_formatAlignment Checklist
.claude/docs/PRINCIPLES.mdand this PR aligns with our principles.claude/docs/INVARIANTS.mdand no invariants are violated/pre-submit-pr(orbash .claude/hooks/lint.shand tests) and addressed all issuesMaintainer review follow-up
Addresses #1500 (review):
step(CallToolAction(...)), including the final reward and observationservice_spend. README explains direct MCP bypasses episode step/time limits, eval-first use, blocking submit (2700 s), timeout overhead, forwarding settled spend ascosts, and spendfinalversus episodedone.SKIP_HF_DEPLOYMENTexcludes automatic Spaces releases. No Space or collection entries are added. Local entry point binds 127.0.0.1; Docker keeps its explicit 0.0.0.0 command.Validation: 95 passed, 1 Linux-only skip across posttrain and deployment-script tests; the README/client examples execute through server serialization and client parsing. RFC example also executed offline. No real HF Jobs or spend. Changed Python files pass lint/format/import sorting; environment doc synchronization and whitespace checks pass. Full-repository lint still reports existing main formatting/import-sort failures. Full local pytest selection: 3958 passed, 150 skipped, 79 deselected, 3 failed (259 s). Failures are in unchanged main files: two harness-process tests raised macOS
os.killpgPermissionError during the full run, but passed in isolation on main; the Harbor occupied-port test failed identically on main (expected "already in use", observed startup timeout). These results do not establish a regression from this change. Full lint has 47 pre-existing main formatting findings plus the two known usort files; changed files pass. The test hook itself lackstimeouton this host, so the same pytest selection ran directly. CI was pending at the post-push check; no monitoring requested. Latest-main changes are merged.Note
High Risk
Introduces a credential-holding broker that can spend real HF Jobs budget if mis-deployed; job isolation relies on seccomp rather than platform egress controls, and the server has no built-in auth.
Overview
Adds
posttrain_env, an experimental MCP environment (RFC 014 prototype) where agents post-train models via brokered Hugging Face Jobs—the server holdsHF_TOKEN, enforces a reserve-then-settle per-rollout budget, and uses a hidden-split grader job as the episode reward.Agent jobs run inside
job_launcher.py: PyPI wheels only, read-only model/dataset mounts, then seccomp blocks non-Unix sockets before the user script runs. MCP tools coverjobs_submit/ status / wait / logs / cancel,dev_evaluate(public dev split), andsubmit(final score + episode end). Observations includeservice_spendfor trainer cost accounting.Also ships two T0 tasks,
PostTrainEnvclient,examples/posttrain_env_agent.py, site/docs updates,SKIP_HF_DEPLOYMENT(private deploy only), and a large fake-HF-Jobs test suite (~2k lines).Reviewed by Cursor Bugbot for commit 15d7d02. Bugbot is set up for automated code reviews on this repo. Configure here.