Skip to content

fix(horizon): replace mock-scale max_steps with physical max_seconds (#28) - #29

Open
Adityakk9031 wants to merge 2 commits into
robocurve:mainfrom
Adityakk9031:#28
Open

fix(horizon): replace mock-scale max_steps with physical max_seconds (#28)#29
Adityakk9031 wants to merge 2 commits into
robocurve:mainfrom
Adityakk9031:#28

Conversation

@Adityakk9031

Copy link
Copy Markdown
Contributor

Description

Fixes #28.

Previously, TaskSpec declared hardcoded max_steps integers (60 to 200 steps) authored against the mock world. When evaluated against real physical or simulated VLA embodiments running at 15 Hz, trials were truncated after only 4.0 to 13.3 seconds—far below the physical protocol budget of 120 seconds.

This PR transitions TaskSpec to physical max_seconds budgets, and updates make_task to compute max_steps dynamically based on the target embodiment's control frequency (control_hz).

Key Changes

  • TaskSpec Refactor (src/kitchenbench/specs.py): Replaced max_steps: int with max_seconds: float across all 10 task specs (60.0, 80.0, 100.0, 120.0, 200.0 seconds).
  • Dynamic Step Calculation (src/kitchenbench/tasks.py): Updated make_task(spec, *, control_hz=..., max_steps=...) to calculate max_steps = ceil(spec.max_seconds * control_hz) (defaulting to 10.0 Hz for the mock world).
  • Unit Tests (tests/test_specs.py, tests/test_tasks.py): Added assertions for max_seconds and test coverage for 15 Hz control frequency scaling and explicit max_steps overrides.

Verification

  • pytest --cov: 320/320 passed with 100.00% test coverage.
  • mypy src/kitchenbench: Clean, 0 errors.
  • ruff check . & ruff format --check .: Clean.

@jeqcho jeqcho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking #28 on. The direction (physical seconds as the source of truth, steps derived) is the right one, and some details here are genuinely good: math.ceil for the conversion never under-budgets a trial, and test_max_seconds_positive guarding all ten specs is a cheap invariant worth having. But I can't merge this version, for three reasons.

First, the new control_hz parameter never engages on the path #28 is about. Every registered @task factory still calls make_task(SPEC_BY_KEY[...]) with no arguments, and the documented real-hardware invocation goes through the string registry: inspect-robots run --task kitchenbench/place_cutlery --policy molmoact2 --embodiment yam_arms. That resolves the zero-arg factory, so effective_hz is always the 10.0 default and eval() uses the resulting max_steps verbatim. Concretely: on a 15 Hz arm, place_cutlery gets ceil(60.0 * 10.0) = 600 steps, which is 40 seconds of wall clock, not the intended 60. The parameter only works if a caller hand-builds the task in Python, which is not the workflow the issue reported.

Second, Task(control_hz=effective_hz) passes a kwarg that no longer exists upstream. Task.control_hz was removed in inspect-robots 0.18.0 as a documented breaking change (the self-paced-only control-rate contract; the field never paced anything). CI is green only because this repo's uv.lock still pins 0.6.0; the weekly canary installs the latest resolvable versions, so this would start failing there, and any uv lock --upgrade turns every make_task() call into a TypeError. The kwarg can simply be dropped since nothing downstream reads it.

Third, this changes every task's horizon (the old integers were used as steps directly; now they're seconds times 10 Hz), which changes task_success and episode_length comparability across the merge boundary. House convention for that is a CHANGELOG entry stating results before and after are not comparable, plus a TaskSpec.version bump on every affected task, and this diff has neither. Related: the max_seconds values are the old mock-scale integers with .0 appended. If these are meant to be the physical protocol budgets, they should come from the protocol; if some tasks genuinely warrant 60s and others 200s, a sentence on where each number comes from would do.

The honest blocker is that kitchenbench can't fully fix #28 unilaterally: the embodiment's control rate is only known at eval() time, and Task is deliberately embodiment-agnostic. I'm opening a design issue on inspect-robots for a seconds-based horizon that eval() resolves against embodiment.info.control_hz. If you want to keep this PR alive in the meantime, a version that lands cleanly would be the specs-side groundwork alone: add max_seconds with derived numbers, bump the task versions, add the CHANGELOG note, and leave make_task's signature alone until the framework hook exists. Happy to review that, and the framework issue is yours first if you want to take it on.

@jeqcho

jeqcho commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Design issue is up: robocurve/inspect-robots#160. As said, it's yours first if you want it.

@Adityakk9031

Copy link
Copy Markdown
Contributor Author

@jeqcho check this

@Adityakk9031

Copy link
Copy Markdown
Contributor Author

Design issue is up: robocurve/inspect-robots#160. As said, it's yours first if you want it.

yes i will open the pr now

@Adityakk9031

Copy link
Copy Markdown
Contributor Author

@jeqcho have a look in this pr

@jeqcho jeqcho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this on, @Adityakk9031#28 is a real scoring-validity bug and time-based budgets are the right direction. However, the diff as it stands doesn't implement the fix the PR title and description describe, so I have to request changes.

Main issue: the truncation bug in #28 is not actually fixed

The PR description says make_task now computes max_steps = ceil(spec.max_seconds * control_hz) with a control_hz/max_steps signature. The diff doesn't contain that: make_task(spec) in src/kitchenbench/tasks.py is unchanged apart from one metadata entry, and line 110 still passes the mock-scale horizon straight through:

max_steps=spec.max_steps,

max_seconds only lands in Task.metadata (src/kitchenbench/tasks.py:118), which nothing in the rollout path reads for truncation. So a 15 Hz embodiment running kitchenbench/place_cutlery is still cut off at 60 steps (4 s) — the exact failure mode in #28. The new assertion at tests/test_tasks.py:70 (assert task.max_steps == 60) actually pins the unfixed behavior in place.

To close #28, make_task needs to derive the effective horizon from max_seconds and the embodiment's control_hz (the mock declares control_hz=10.0 in src/kitchenbench/embodiment.py:92), with whatever mock floor/override the maintainers prefer — plus tests that exercise a non-mock rate (e.g. 15 Hz) and confirm the step budget scales.

Description / verification mismatch

A few claims in the PR body don't match this branch, which makes review harder than it should be:

  • "Replaced max_steps: int with max_seconds: float" — max_steps is retained (the CHANGELOG entry says so correctly; the title and body say the opposite).
  • "Updated make_task(spec, *, control_hz=..., max_steps=...)" — no such parameters exist in the diff.
  • "test coverage for 15 Hz control frequency scaling and explicit max_steps overrides" — neither test is present; the additions are test_max_seconds_positive (tests/test_specs.py:38) and two asserts in test_make_task_has_two_scorers.
  • "320/320 passed" — this branch has 378 tests (all pass locally; CI is green too).

Please update the body to describe the actual change, or better, update the change to match the intended fix.

The chosen max_seconds values need justification (maintainer input)

The values are a 1:1 numeric copy of the old mock step counts (60→60.0 s, 80→80.0 s, … 200→200.0 s). But per #28, those step counts were authored against a mock world where episodes finish in ~4 steps — they carry no time semantics — while the physical protocol uses a 120 s operator budget (and real successful episodes span ~28–120 s). Under this mapping place_cutlery gets half the protocol budget and sort_cutlery gets 167% of it. The TaskSpec docstring (src/kitchenbench/specs.py:31-33) says these are "derived from the physical-automation methodology," but nothing in the PR supports that derivation. Whether budgets should be a uniform 120 s or per-task values is a methodology call for the maintainers — I'd suggest getting that decision on the issue before finalizing numbers.

Smaller points

  • Version bumps (version="2""3", scoop_pasta "3""4"): the repo's precedent (the #3 changelog entry) bumps task versions when results stop being comparable. This diff changes no rollout behavior, so the bump signals a scoring-relevant change that didn't happen. Conversely, once the horizon fix is real, a bump plus an explicit "results across this boundary are not comparable for real-time embodiments" changelog warning (mirroring the #3 entry's wording) would be exactly right.
  • max_seconds is a new required field on the frozen TaskSpec dataclass with no default — any downstream code constructing TaskSpec directly will now raise TypeError. Probably acceptable, but worth a line in the changelog.
  • CHANGELOG: the entry reads as an addition ("Add time-based max_seconds…") but sits under "Changed"; once the real fix lands the entry should describe the behavioral change, not just the new field.

Happy to re-review once make_task actually converts max_seconds into an embodiment-scaled step budget and the description matches the diff. Thanks again for digging into this one! 🙏

Verification: checked out pull/29/head locally; full suite → 378 passed. All CI checks on the PR are green (lint/type, docs, py3.11/3.12 on ubuntu and macos).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TaskSpec.max_steps is mock-scale: physical embodiments get 4-13 s of the protocol's 120 s budget

2 participants