feat(v1): lazy and infinite tasksets (load() may yield)#1969
Open
mikasenghaas wants to merge 4 commits into
Open
feat(v1): lazy and infinite tasksets (load() may yield)#1969mikasenghaas wants to merge 4 commits into
mikasenghaas wants to merge 4 commits into
Conversation
Taskset.load may now be a generator - Iterable[TaskT] instead of list[TaskT] - yielding each task as it's built, possibly forever. An `infinite` property (default False) marks a never-ending load; the new Taskset.select materializes only the tasks a run needs (islice), guards infinite tasksets (num_tasks required, shuffle a warned no-op), and replaces the load/shuffle/slice triplet in eval, validate, and debug. The env server materializes a finite taskset up front as before, but pulls an infinite one off its generator on demand, caching up to the highest requested idx; InfoResponse.num_tasks is now int | None with None meaning infinite, and the server eval path bounds such runs with -n. The procedural example tasksets generate forever by default now (num_tasks: int | None = None knob): textarena (and wordle_v1) yields seeded episodes lazily, alphabet_sort_v1 cycles its source name lists with fresh rng draws per pass, and color_codeword_v1 (num_examples renamed to num_tasks) streams synthetic episodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
num_tasks is now ge=1 on the eval/validate/debug configs and on the procedural tasksets' knobs - a negative value used to hang alphabet_sort's generator (idx == num_tasks never hit) and, post islice, raised an opaque ValueError from the eval runner instead of a config error. The alphabet stop condition is >= anyway so an out-of-band bound stops immediately. task_idx is ge=0 on the rollout RPCs (a negative idx silently indexed from the end of a materialized taskset), and EnvServer._task caps lazy generation at MAX_LAZY_TASKS so a runaway driver idx fails the request instead of generating and caching toward it forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ApprovabilityVerdict: Needs human review This PR introduces a significant new feature (lazy/infinite tasksets) that changes core infrastructure behavior - how tasks are loaded, materialized, and served. The changes span the Taskset base class, EnvServer, CLI commands, and multiple taskset implementations, warranting human review. You can customize Macroscope's approvability policy. Learn more. |
Infinity is inherent to a taskset, not a function of its config: the `infinite` property is now an `INFINITE` ClassVar (mirroring NEEDS_CONTAINER) and the procedural tasksets drop their `num_tasks`/`num_examples` knobs - bounding a run is the selector's job (`select(num_tasks)`, `-n` on the CLI), not the taskset's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Redo of #1829 on top of the Task/TaskData/Taskset split (#1948), scoped to the taskset side:
load()may now be an iterator.Taskset.loadis overloaded from-> list[TaskT]to-> Iterable[TaskT], so a taskset canyieldits tasks from a generator — built lazily, or genuinely infinite. Alistis still a validIterable, so existing dataset-backed tasksets are unchanged.Taskset.INFINITEclass sentinel (defaultFalse, mirroringNEEDS_CONTAINER) marks aloadthat never terminates. Infinity is inherent to the taskset class, not a config knob — how many tasks a run takes is the selector's decision (-n), so the procedural tasksets carry nonum_tasksconfig field at all.Taskset.select(num_tasks, shuffle, seed)is what runs call now (replacing the copy-pasted load → shuffle → slice triplet ineval,validate, anddebug): itislices the firstnum_tasksoffload, so a generator only builds what the run takes.shufflestill samples from the whole (finite) taskset — that requires materializing it first. On an infinite taskset,selectrequiresnum_tasks(clearValueErrorinstead of a hang) and ignoresshufflewith a warning (the firstngenerated tasks are already an arbitrary sample).The env server supports infinite tasksets without a protocol change: a finite taskset is materialized up front exactly as before; an infinite one is pulled off its generator on demand and cached up to the highest
task_idxrequested (bounded by the driver'snum_tasks, and hard-capped atMAX_LAZY_TASKS = 1_000_000per worker so a runaway driver idx fails its request instead of generating and caching forever).task_idxis validatedge=0on the rollout RPCs (a negative idx used to silently index from the end of the materialized list). Generation must be deterministic — pool workers each run their ownload()and rely on producing the same sequence, which all ported tasksets satisfy (constant seeds / seed-per-idx).InfoResponse.num_tasksis nowint | None,None⟺ infinite; the--servereval path bounds such runs with-nand errors clearly without it.Ported the procedurally generated example tasksets to infinite generators (
INFINITE = True):textarena(built-in;wordle_v1inherits) — one seeded episode per index, yielded lazily; seed-specific games (WordLadder, WordSearch) no longer deepcopy+reset the game for episodes a run never touches.alphabet_sort_v1— cycles the source name lists forever, with the rng advancing across passes so every pass draws fresh turn splits and the episode stream never repeats; a full pass yielding nothing (degenerate turn config) raises instead of spinning.color_codeword_v1— streams synthetic episodes (thenum_examplesknob is gone).Dataset-backed tasksets (gsm8k, reverse_text, lean, harbor, …) intentionally stay lists: their dominant cost is the eager
load_dataset, so laziness wouldn't buy anything.Docs: a "Lazy and infinite tasksets" section in
docs/v1/environments.md(yield form,infinite, the two rules, determinism requirement), notes onnum_tasks/shuffleindocs/v1/evaluation.md;configs/wordle.tomlandconfigs/textarena.tomldrop their now-redundant finite episode pools.num_tasksis validatedge=1on the eval/validate/debug configs — a negative value, withislice, raised an opaqueValueErrorfrom the eval runner instead of a config error.Tests:
tests/v1/test_taskset.pycoversselectover finite/generator/infiniteloads (bounding, the missing-num_taskserror, shuffle no-op + reproducible finite sampling, and that only the selected tasks are ever built).Relative to #1829, the pull-based
sample()RPC / broker cursor /TasksetConfig.shuffleredesign is dropped — idx-addressing works unchanged for infinite tasksets as long as generation is deterministic, which keeps this PR orchestrator-compatible for finite tasksets with no prime-rl companion.Verification
wordle-v1(-n 2 -r 1, gpt-4.1-mini via PI): built exactly tasks 0–1 from the generator, both scored, 0 errors.--servereval on the same:EnvServer up: … tasks=infinite, tasks 0–1 pulled on demand, both scored, 0 errors; finite path re-smoked withreverse-text-v1 --server(tasks=1000, 0 errors).-nfails withWordleTaskset is infinite - select a bounded subset with num_tasks (-n on the CLI)(in-process),wordle-v1 is infinite - bound the run with -n/--num-tasks(--server), and same viavalidate;--shuffle -n 1warnsshuffle is a no-op on an infinite tasksetand runs task 0.alphabet-sort-v1in-process eval (-n 2 -r 1): both episodes reward 1.0, 0 errors. Its generator was driven past a full source pass (112,248 episodes > 112,245 entries): idx stays monotonic, second-pass episodes differ, and two independentload()iterations produce identical heads (pool-worker determinism).color_codeword_v1and lazy WordLadder (3 seed-specific tasks in 0.9s) checked the same way.-n -1and--taskset.num-tasks -5now fail config validation (Input should be greater than or equal to 1);EnvServer._taskpast the lazy cap raisestask_idx 1000007 exceeds the lazy-generation cap (1000000)while a materialized taskset larger than the cap still serves fine (the cap only gates generation); out-of-range on a finite taskset stillIndexErrors.pytest tests/v1 -m "not e2e": 63 passed.ruff check/ruff formatclean.Breaking
Taskset.loadis annotated-> Iterable[TaskT](waslist[TaskT]). Returning a list still satisfies it; only consumers that indexed/len()ed the result must go throughselect(all in-tree ones now do).InfoResponse.num_tasksisint | None(wasint);None⟺ infinite. Finite tasksets still report anint, so existing consumers (e.g. prime-rl's orchestrator) keep working for finite tasksets; supporting infinite tasksets in training lands in the companion feat(orchestrator): support infinite tasksets prime-rl#2993 (verified live against this branch).textarena/wordle_v1(num_tasks: int = 1000gone),color_codeword_v1(num_examples: int = 1000gone), andalphabet_sort_v1(was one pass over its source dataset). Runs bound them with the selector (-non eval/validate/debug;num_exampleson a prime-rl eval env); a config still passing the old taskset field fails validation.Closes #1829 (superseded — that branch predates the Task/TaskData/Taskset split).
🤖 Generated with Claude Code
Note
Add lazy and infinite taskset support via generator-based
load()andTaskset.select()INFINITE: ClassVar[bool]to theTasksetbase class and broadensload()to returnIterable[TaskT], allowing subclasses to yield tasks indefinitely.Taskset.select(num_tasks, shuffle, seed)to centralize task materialization: infinite tasksets requirenum_tasks, ignoreshufflewith a warning, and useitertools.islicefor lazy consumption; finite tasksets support seeded shuffle.AlphabetSortTaskset,ColorCodewordTaskset, andTextArenaTasksetto infinite generators usingitertools.count().EnvServerto lazily build and cache tasks per-index for infinite tasksets, reportingnum_tasks=NoneinInfoResponse, with a cap of 1,000,000 tasks per worker.eval,debug,validate) now delegate task selection toTaskset.select()instead of manually loading and slicing.num_tasksis now required when running against infinite tasksets; passing--shufflewith an infinite taskset is silently ignored.Macroscope summarized 264a43f.
Note
Medium Risk
Breaking API/config changes (
InfoResponse.num_tasks, removed taskset size fields) affect external orchestrators and old TOML; infinite server paths depend on deterministic generation across pool workers.Overview
Taskset.load()may now return anIterable(generator) instead of only a list;INFINITE = Truemarks procedural tasksets that never end.Taskset.select(num_tasks, shuffle, seed)centralizes bounded materialization (islicefor lazy loads, full materialize + shuffle for finite sets) and replaces duplicated load/shuffle/slice logic in eval, validate, and debug.Env server keeps finite tasksets fully loaded; infinite ones use on-demand
_task(idx)generation with caching andMAX_LAZY_TASKS.InfoResponse.num_tasksisint | None(None= infinite). The--servereval path requires-nfor infinite tasksets and treats shuffle as a no-op with a warning.textarena, alphabet_sort_v1, and color_codeword_v1 are converted to infinite generators; per-taskset
num_tasks/num_examplesconfig fields are removed.num_taskson eval/validate/debug configs gainsge=1; rollout RPCtask_idxisge=0.Docs and sample configs are updated;
tests/v1/test_taskset.pycoversselectbehavior.Reviewed by Cursor Bugbot for commit 264a43f. Bugbot is set up for automated code reviews on this repo. Configure here.