feat(v1): training run metadata on episode - #2252
Draft
mikasenghaas wants to merge 17 commits into
Draft
Conversation
EnvInfo.name is what the caller knows the env by when that differs from its id — the key it was configured under, which a run over several envs keys its metrics by. GroupInfo is the cohort: the episodes planned together from one task, which a consumer compares against each other. Env.slots mints one per call, so -r k is a group of k, and replanning the same task later is a new group rather than a merge into the old one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
slots() minted a group from the count it was asked to plan, but a resume asks only for what it still owes — so its replacements got a fresh id and a smaller size while the kept episodes stayed in the original group, and one -r k split into two partial groups. Group-keyed pass@k read those halves as whole groups. slots() now accepts a group to join, and the eval runner recovers each task's group from its kept episodes (resume.groups_by_key). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
group joins run as a consumer-stamped field and nothing in verifiers sets it. Minting one in slots() was speculative — no verifiers code reads it, pass@k there groups by task.data.idx — and it dragged in a resume bug: slots() sized the group by what it was asked to plan, but a resume asks only for what it owes, so one -r k split into two partial groups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to_record policy_version and off_policy_steps are the training path's own facts — which policy generated an episode, and how far behind the step that trains on it — so they sit on TrainRunInfo beside its id and step. to_record is the episode form of Trace.to_record: the same tensor exclusions applied through traces, which is the unit traces.jsonl stores. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A training run's evals are its own: same run id, same step, same policy as the rollouts it trains on. They were an EvalRunInfo, which made those shared facts optional on both records and left a standalone eval carrying a step and a policy version it has none of. TrainRunInfo now covers both through kind, and EvalRunInfo is what it should always have been: a model measured once, against nothing that is training. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Staleness was a number a consumer had to compute and store, and it only ever described one end of the episode. The run now records the policy versions generation spanned — TimeSpan's shape over updates instead of seconds — and both readings fall out: drift is what changed mid-episode, off_policy_steps how far behind training the generating policy was. Neither is stored, so neither can disagree with the span. A frozen sampler follows no version, so its policy is None and staleness with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
to_record had no caller here — write_episode dumps the episode itself, and the eval path has no tensors to exclude. It was added for a consumer that can hold it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Queue time is only in it on the training path, where step is the window the episode landed in. An eval is placed by the epoch it was dispatched for, so its lag is the one it had when it started. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etadata kind was a field, and step meant one thing under it and another under the other value: the window an episode landed in, or the epoch an eval was dispatched for. Two meanings, one field, and only one of them could be required. They become TrainMetadata and EvalMetadata, nested under the run because that is the relationship — one training run, one id, two things an episode can be to it. Each states its own step, and an eval's is required, since it is known the moment the eval is dispatched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An eval's step is fixed when it is dispatched, so measuring staleness against it undercounts: an eval that outlives three updates still looked on-policy, while one that measured a single version cleanly reported the gap between the step index and that version. Nothing trains on an eval, so there is no policy for it to be behind. What makes it off-policy is the policy moving under it. Each metadata now owns that definition, which is what splitting them was for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They were written when the run was a field on Trace. #2244 moved the field to Episode; the types stayed behind, and Trace has not referred to one since. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It sat beside the run id, where it was the one per-episode fact among shared ones — the same mixing that nesting the metadata was meant to undo. Moving it completes that: a run is an id, and everything true of a single episode is its metadata. off_policy_steps follows it, so each kind owns both the definition and the data it reads, and it is a property rather than a method the run has to hand the span to. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Enough on the episode that a consumer stops keeping its own copy of where it came from and what it
is to the run. Everything is optional or defaulted, so existing records and readers are unaffected.
A group is what a consumer compares against each other — pass@k over it, a GRPO baseline within it.
utils/platform.pyreconstructs that ad hoc today. The id is per group, not per task: planningthe same task again is a new group, so two rounds never merge into one comparison.
The run, and what an episode is to it
A training run's evals are its own — same id, same policy as the rollouts they are measured against
— so one run record, with the per-episode part nested under it:
Splitting the metadata is what lets each say something true.
stepmeans two different things andonly the eval's is knowable upfront, so under a single
kindflag it had to be optional for both.And
off_policy_stepsdiffers the same way:Nothing trains on an eval, so there is no policy for it to be behind; what makes it off-policy is
the policy moving under it. Its
stepcannot say that — fixed at dispatch, it reports an eval thatoutlived three updates as on-policy, and one that measured a single version cleanly as one behind.
Both readings derive from the span, so nothing stored can contradict it.
policyisNonewhen theepisode did not come from the live policy at all (a frozen sampler), so staleness reads
Nonerather than a
0that looks fresh.The run types also move from
trace.pytoepisode.py— they were written when the run was a fieldon
Trace, and #2244 moved the field without them.Verification
uv run pytest tests— full suite passes; round-trips throughWireEpisode.model_validate(episode.model_dump(mode="json")).The eval cases that motivated the split:
0310Live in prime-rl (feat(orchestrator)!: align multi-agent types prime-rl#3183), 6-step runs with eval throughout.
Consumer
prime-rl carried all of this on
EpisodeandTracesubclasses and now extends neither. Its twostaleness mechanisms — an in-flight counter, then a recompute at ship — collapse into the derived
property.
Not included
An earlier revision had
Env.slotsmint a group so-r kwas a group of k. Dropped: nothing herereads a group, and it broke resume —
slots()sized the group by what it was asked to plan, but aresume asks only for what it still owes, so one
-r ksplit in two. Worth doing when something hereconsumes a group, with the resume threading it needs.
🤖 Generated with Claude Code