Skip to content

Integrate K=1–5 NMG benchmarking and differentiable rollout support - #623

Open
yangchen73 wants to merge 10 commits into
mainfrom
yc/nmg-bench
Open

Integrate K=1–5 NMG benchmarking and differentiable rollout support#623
yangchen73 wants to merge 10 commits into
mainfrom
yc/nmg-bench

Conversation

@yangchen73

Copy link
Copy Markdown
Collaborator

Description

Add K=1–5 NMG benchmark support, complete functional-state gradients for differentiable rollouts, and align NeuralPlanner with K=5 ONNX policies.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which improves an existing functionality)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (existing functionality will not work without user modification)
  • Documentation update

Checklist

  • I have run the black . command to format the code base.
  • I have made corresponding changes to the documentation.
  • Public API changes are reflected in the API docs (python docs/scripts/check_api_docs.py), if applicable.
  • I have added tests that prove my fix is effective or that my feature works.
  • Dependencies have been updated, if applicable. No dependency changes were required.

Copilot AI lite review requested due to automatic review settings September 14, 2026 00:43
@yangchen73 yangchen73 changed the title Yc/nmg bench Integrate K=1–5 NMG benchmarking and differentiable rollout support Sep 14, 2026
@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with the previously reported cuRobo benchmark preparation issue fully addressed and no new actionable failures identified.

Summary

  • Adds explicit ONNX observation-layout metadata and fingerprint validation.
  • Supports Cartesian and joint waypoint benchmark cases with capacity-aware NMG rollout budgets.
  • Prepares every required cuRobo backend variant before timed trials.
  • Bridges recurrent state tensors through the Warp/PyTorch autograd boundary.
  • Updates benchmark suites, atomic-object physics configuration, documentation, and focused tests.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Cases[Benchmark cases] --> Gate[Capability and capacity checks]
    Gate --> Prepare[Prepare backend variants]
    Prepare --> Curobo[cuRobo Cartesian or joint planning]
    Prepare --> NMG[NMG K=1–5 closed-loop rollout]
    NMG --> Contract[Validate ONNX observation metadata]
    Contract --> Result[Timed batched PlanResult]

    Action[PyTorch action] --> Bridge[NewtonStepFunc]
    State[Functional recurrent state] --> Bridge
    Bridge --> Warp[Warp kinematic tape]
    Warp --> Outputs[Observation and reward]
    Outputs --> Backward[Action and state gradients]
Loading

Reviews (7) · Last reviewed commit: "Prepare cuRobo backends before timing"

Copilot AI 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.

🟡 Changes recommended

Two critical issues remain: stale K=4 batched observation fixtures and unsupported joint-space cases being scheduled through NMG.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds K=1–5 NMG benchmarking, differentiable recurrent-state gradients, and NeuralPlanner alignment with K=5 ONNX policies.

Changes:

  • Updates NeuralPlanner observation layouts and defaults.
  • Adds functional-state gradient propagation through differentiable rollouts.
  • Adds NMG capacity filtering, waypoint budgets, and benchmark configurations.
  • Updates atomic scenarios, tests, and documentation.
File summaries
File Summary
tests/sim/motion/planners/test_neural_planner.py Adds NeuralPlanner observation-dimension coverage.
tests/gym/envs/test_differentiable_embodied_env.py Tests recurrent functional-state gradients.
tests/benchmark/motion_generation/test_motion_generation_benchmark.py Tests NMG capacity and rollout budgets.
tests/benchmark/motion_generation/test_atomic_task_benchmark.py Tests atomic benchmark configuration.
scripts/benchmark/motion_generation/suites/smoke.yaml Updates smoke benchmark settings.
scripts/benchmark/motion_generation/suites/coverage.yaml Updates coverage benchmark settings.
scripts/benchmark/motion_generation/suites/atomic_franka_pgi_curobo.yaml Updates atomic benchmark configuration.
scripts/benchmark/motion_generation/suites/atomic_franka_pgi_curobo_randomized.yaml Updates randomized atomic cases, including joint-waypoint cases.
scripts/benchmark/motion_generation/suites/atomic_franka_pgi_curobo_pose_batch.yaml Updates pose-batch benchmark configuration.
scripts/benchmark/motion_generation/scenarios/atomic_objects.py Updates collision and physics configuration.
scripts/benchmark/motion_generation/runner.py Records unsupported benchmark cases.
scripts/benchmark/motion_generation/planners/nmg_onnx.py Adds capacity and waypoint-budget handling; joint-space cases must be excluded before scheduling.
scripts/benchmark/motion_generation/planners/base.py Updates planner base interfaces.
embodichain/lab/sim/motion/planners/neural_planner.py Aligns observation layouts with K=5 policies; existing K=4 batched fixtures still expect width 101 instead of 97.
embodichain/lab/sim/diff/bridge.py Bridges functional-state gradients through Warp and PyTorch.
embodichain/lab/gym/envs/differentiable_env.py Forwards recurrent state through environment steps.
embodichain_tasks/embodichain_tasks/special/franka_reach_apg.py Updates the differentiable action-kernel contract.
agent_context/topics/motion-planning/motion-planning.md Documents updated motion-planning contracts.
agent_context/topics/differentiable-env/differentiable-env.md Documents differentiable environment changes.
Review details
  • Files reviewed: 19/19 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread embodichain/lab/sim/motion/planners/neural_planner.py Outdated
Comment thread scripts/benchmark/motion_generation/planners/nmg_onnx.py
Copilot AI review requested due to automatic review settings September 14, 2026 01:11

Copilot AI 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.

🔵 Needs a closer look

Three moderate validation issues remain in functional-state handling and rollout configuration.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

embodichain/lab/sim/diff/bridge.py:137

  • Passing a non-tensor functional state (for example, None from _functional_state_tensors()) fails here while evaluating .shape, so the explicit TypeError below is unreachable and callers receive an AttributeError instead. Validate all state inputs before deriving saved_state_shapes so invalid state-hook implementations fail with the documented, actionable error.

scripts/benchmark/motion_generation/planners/nmg_onnx.py:101

  • [P2] Reject non-positive rollout budgets before passing them to the planner. With steps_per_waypoint: 0 (or a non-positive max_steps), this returns 0/negative, but NeuralPlanner.plan() uses options.max_steps or self._max_steps, so zero silently falls back to the configured 150-step rollout while a negative value produces no rollout. Validate both config values as positive here (or in suite validation) so the per-waypoint budget cannot be bypassed.
        steps_per_waypoint = int(self.spec.config.get("steps_per_waypoint", 30))
        configured_max = int(self.spec.config.get("max_steps", 150))
        return min(configured_max, int(case.num_waypoints) * steps_per_waypoint)

scripts/benchmark/motion_generation/suites/smoke.yaml:46

  • [P2] Validate the new NMG rollout controls at suite-load time. SuiteCfg.validate_benchmark() currently checks only pos_eps/rot_eps, so steps_per_waypoint: 0 or a non-positive max_steps is accepted and reaches _case_max_steps(); this either silently falls back through NeuralPlanOptions(max_steps=0) or produces an empty rollout, making the benchmark budget invalid instead of failing configuration validation. Add positive-integer validation for num_waypoints, steps_per_waypoint, and max_steps (and apply it to the other updated suites).
      num_waypoints: 5
      steps_per_waypoint: 30
      max_steps: 150
  • Files reviewed: 20/20 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@yuecideng

Copy link
Copy Markdown
Contributor

The NMG integration direction is good, but I recommend the following changes before merge:

  1. Reject unsupported joint-space cases before scheduling (P2): NmgOnnxAdapter.supports_case() currently checks waypoint capacity, but capability validation should also cover the case modality/motion validity. A joint-waypoint case that is not representable by the exported NMG policy can still reach plan(), producing an invalid benchmark result. Please make the supported modalities explicit and filter these cases in the runner before planner construction.

  2. Validate rollout budgets as positive integers (P2): steps_per_waypoint and max_steps are converted to integers without rejecting zero or negative values. A zero budget can be interpreted as “unset” by NeuralPlanner.plan() and silently fall back to the default horizon, while a negative budget can produce an empty rollout. Please validate these fields during suite/config loading (and apply the check to every updated suite).

  3. Validate functional-state types before reading .shape (P2): NewtonStepFunc.forward() builds saved_state_shapes before checking that each functional state is a torch.Tensor. A bad _functional_state_tensors() implementation therefore raises AttributeError instead of the documented actionable TypeError. Move the type check ahead of shape extraction.

  4. Add a cross-repository observation contract test (P1 integration risk): NMG PR Minor modification on docs #8 changes the exported K=1–5 policy while this PR changes the default NeuralPlanner layout to K=5. Please lock K=1/3/5 widths, block ordering, masks, quaternion (xyzw) ordering, and ONNX batch-1/batch-3 parity in a shared or mirrored contract test.

  5. End-to-end differentiable rollout: Please run a real K=3/K=5 ONNX policy through MotionGenerator and DifferentiableEnv, verifying multi-step action/state gradients and deferred reset behavior. The current focused tests do not cover the complete cross-repository path.

@yangchen73

Copy link
Copy Markdown
Collaborator Author

The NMG integration direction is good, but I recommend the following changes before merge:

  1. Reject unsupported joint-space cases before scheduling (P2): NmgOnnxAdapter.supports_case() currently checks waypoint capacity, but capability validation should also cover the case modality/motion validity. A joint-waypoint case that is not representable by the exported NMG policy can still reach plan(), producing an invalid benchmark result. Please make the supported modalities explicit and filter these cases in the runner before planner construction.
  2. Validate rollout budgets as positive integers (P2): steps_per_waypoint and max_steps are converted to integers without rejecting zero or negative values. A zero budget can be interpreted as “unset” by NeuralPlanner.plan() and silently fall back to the default horizon, while a negative budget can produce an empty rollout. Please validate these fields during suite/config loading (and apply the check to every updated suite).
  3. Validate functional-state types before reading .shape (P2): NewtonStepFunc.forward() builds saved_state_shapes before checking that each functional state is a torch.Tensor. A bad _functional_state_tensors() implementation therefore raises AttributeError instead of the documented actionable TypeError. Move the type check ahead of shape extraction.
  4. Add a cross-repository observation contract test (P1 integration risk): NMG PR Minor modification on docs #8 changes the exported K=1–5 policy while this PR changes the default NeuralPlanner layout to K=5. Please lock K=1/3/5 widths, block ordering, masks, quaternion (xyzw) ordering, and ONNX batch-1/batch-3 parity in a shared or mirrored contract test.
  5. End-to-end differentiable rollout: Please run a real K=3/K=5 ONNX policy through MotionGenerator and DifferentiableEnv, verifying multi-step action/state gradients and deferred reset behavior. The current focused tests do not cover the complete cross-repository path.

Support Joint case, and the missing validation and contract tests are now covered.

Copilot AI review requested due to automatic review settings September 14, 2026 05:45
"""Return the unified NMG constraint-observation width."""
n = int(num_waypoints)
dim = 7 + 7 + n * (3 + 4 + 7 + 5) + 7 + n
dim = 7 + 7 + n * (3 + 4 + 7 + 5) + 7

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.

It woould be better to use config file for dim definition. Since we may change it later

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I stored the obs layout when export to onnx file. Then read the layout from EmbodiChain's side.

Copilot AI 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.

🟡 Changes recommended

Unresolved critical and moderate findings remain.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

scripts/benchmark/motion_generation/config.py:255

  • The new NMG settings validation covers waypoint and rollout budgets plus pos_eps/rot_eps, but it still accepts joint_eps <= 0. This PR adds ordered_joint_waypoints; with the planner's strict joint_dist < self._joint_eps check, zero or negative values make every joint waypoint unreachable (even an exact match), so benchmark configuration should reject this value just like the other convergence thresholds.
        for nmg in (spec for spec in self.planners if spec.adapter == "nmg_onnx"):
            for field, default in (
                ("num_waypoints", 5),
                ("steps_per_waypoint", 30),
                ("max_steps", 150),
            ):
                _positive_int(
                    nmg.config.get(field),
                    name=f"NMG {field}",
                    default=default,
                )

scripts/benchmark/motion_generation/planners/nmg_onnx.py:151

  • The new JOINT_MOVE branch contradicts the benchmark design, which still says NMG supports only EEF_MOVE and that joint-space cases are cuRobo-only (scripts/benchmark/motion_generation/BENCHMARK_DESIGN.md:370-372). Update that contract and any leaderboard-track guidance, or avoid advertising joint NMG support; otherwise the documented eligibility rules disagree with the runner.
        if motion_validity == "ordered_joint_waypoints":
            targets = [
                PlanState.from_qpos(
                    case.reference_qpos[:, index], move_type=MoveType.JOINT_MOVE
                )
                for index in range(case.num_waypoints)
            ]
        elif motion_validity == "ordered_cartesian_waypoints":

scripts/benchmark/motion_generation/planners/nmg_onnx.py:91

  • validate_benchmark() treats an explicit null value as “use the default” via _positive_int, but this adapter casts the raw mapping value directly. A valid suite containing num_waypoints: null therefore passes validation and then raises TypeError here (and the same mismatch exists for steps_per_waypoint/max_steps in _case_max_steps() and build()). Resolve None to the same defaults before converting these fields so the validated configuration can actually run.
        capacity = int(self.spec.config.get("num_waypoints", 5))

scripts/benchmark/motion_generation/runner.py:422

  • This introduces a new unsupported_capacity failure code, but the benchmark's documented stable failure taxonomy in scripts/benchmark/motion_generation/BENCHMARK_DESIGN.md:822-841 does not include it. Either use the documented unsupported_capability code or update the taxonomy and downstream artifact/report contract so consumers do not encounter an undocumented value.
            self._record_unavailable(
                writer,
                metadata,
                case,
                reason or "case is outside planner capacity",
                failure_code="unsupported_capacity",
            )
  • Files reviewed: 21/21 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread embodichain/lab/sim/diff/bridge.py
Comment thread tests/gym/envs/test_differentiable_embodied_env.py
Copilot AI review requested due to automatic review settings September 14, 2026 06:24

Copilot AI 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.

🟡 Changes recommended

Unresolved critical and moderate findings block approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (7)

embodichain/lab/gym/envs/differentiable_env.py:198

  • [P2] Preserve kernel_args when adding recurrent state. NewtonStepFunc calls this wrapper as action_kernel(action, tape, *kernel_args, *state_wps), but _inner treats every argument after tape as a state array. Any DifferentiableEnv subclass that overrides the existing kernel_args entry and also returns functional state will therefore pass the static kernel arguments into _apply_action_kernel as state (and shift the real state positions), causing incorrect Warp inputs or an arity failure. Keep the kernel-argument count/closure separate from the recurrent state arguments, or explicitly reject non-empty kernel_args for this environment.
        def _inner(action_wp: Any, tape: Any, *state_wps: Any) -> None:
            env._apply_action_kernel(action_wp, *state_wps, tape=tape)

scripts/benchmark/motion_generation/config.py:255

  • [P2] Apply NMG CLI overrides to adapter-selected IDs — validation now recognizes every planner with adapter == "nmg_onnx" (including the renamed ID used by the new validation test), but _apply_overrides() still searches only for spec.id == "nmg". Selecting a valid renamed NMG planner therefore leaves --nmg-onnx-path and the NMG epsilon overrides unapplied; the planner is then skipped for a missing model path or runs with stale thresholds. Apply these overrides by adapter, or consistently require the canonical ID.
        for nmg in (spec for spec in self.planners if spec.adapter == "nmg_onnx"):
            for field, default in (
                ("num_waypoints", 5),
                ("steps_per_waypoint", 30),
                ("max_steps", 150),
            ):
                _positive_int(
                    nmg.config.get(field),
                    name=f"NMG {field}",
                    default=default,
                )

scripts/benchmark/motion_generation/planners/nmg_onnx.py:92

  • [P2] Resolve nullable NMG defaults before casting — validate_benchmark() treats None as the default for num_waypoints, steps_per_waypoint, and max_steps, so a YAML value such as num_waypoints: null is accepted. This call then executes int(None) and crashes before capability filtering; the same pattern exists in _case_max_steps() and build(). Normalize None to the documented defaults in the adapter or reject null values during validation.
        capacity = int(self.spec.config.get("num_waypoints", 5))
        if case.num_waypoints > capacity:

scripts/benchmark/motion_generation/planners/nmg_onnx.py:165

  • This per-case rollout cap is applied only when the runner calls NmgOnnxAdapter.plan. Atomic-task cases take a different path: AtomicTaskScenario.plan_case compiles an AtomicActionEngine, whose primitives pass MotionPolicy.plan_opts=None to the adapter-owned MotionGenerator, so NeuralPlanner falls back to its configured 150 steps. A one-waypoint atomic case therefore still gets 150 policy steps instead of the 30-per-waypoint budget asserted by the new test. Thread a case-specific NeuralPlanOptions(max_steps=...) through the atomic invocation path, or explicitly scope this budget to direct planner cases and test that scope.
                plan_opts=NeuralPlanOptions(max_steps=self._case_max_steps(case)),

scripts/benchmark/motion_generation/planners/nmg_onnx.py:147

  • This new JOINT_MOVE route makes the benchmark contract in scripts/benchmark/motion_generation/BENCHMARK_DESIGN.md stale: its capability boundary and primary-track text still say current NMG supports only EEF waypoints and that joint-space cases are cuRobo-only (lines 370-372). Update the design/track capability documentation in this change so the newly eligible joint cases and resulting coverage are not misinterpreted.
                PlanState.from_qpos(
                    case.reference_qpos[:, index], move_type=MoveType.JOINT_MOVE

scripts/benchmark/motion_generation/runner.py:422

  • [P2] Keep unsupported-capacity cases out of the success denominator. This new path records an availability row but leaves the case in the full cases list; aggregation.py then treats the missing measured outcomes as zero in _case_macro_rate/_case_macro_primary_rate and includes the case in the expected coverage count. Selecting a suite with a waypoint count above NMG capacity therefore lowers NMG's success rates and makes it ineligible instead of applying the benchmark contract that unsupported cases are excluded from the track denominator (scripts/benchmark/motion_generation/BENCHMARK_DESIGN.md:672-680). Carry an explicit supported-case set into aggregation, while retaining separate coverage reporting.
            self._record_unavailable(
                writer,
                metadata,
                case,
                reason or "case is outside planner capacity",
                failure_code="unsupported_capacity",
            )

scripts/benchmark/motion_generation/scenarios/atomic_objects.py:186

  • [P2] Do not truncate invalid mesh hull counts. A YAML value such as max_convex_hull_num: 1.5 is silently converted to 1 here and selects a single convex hull, while the underlying MeshCollisionCfg.max_hulls contract requires an integer (and rejects booleans/non-integers). Validate the raw value before converting it so malformed benchmark scene configuration fails instead of changing the collision approximation.
    max_hulls = int(config.get("max_convex_hull_num", 16))
  • Files reviewed: 21/21 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread scripts/benchmark/motion_generation/planners/base.py
Copilot AI review requested due to automatic review settings September 14, 2026 06:49
Comment thread scripts/benchmark/motion_generation/planners/curobo.py

Copilot AI 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.

🔵 Needs a closer look

Seven unresolved moderate findings remain across planner, benchmark, and differentiable-environment behavior.

Review details

Suppressed comments (7)

Previously missed (1) — in code that hasn't changed since the last review.

scripts/benchmark/motion_generation/runner.py:422

  • [P2] Exclude unsupported cases from the planner's success denominator. These availability records are not excluded by aggregation: aggregate_results() still receives the full self.cases, and _case_macro_rate() treats every case without a measured outcome as 0.0. Thus an NMG case over the five-waypoint capacity (for example the eight-waypoint twist case) is reported as unsupported but also lowers success rates and prevents coverage/eligibility, contrary to the benchmark contract that unsupported cases are excluded from the track denominator; carry the per-planner supported-case set into aggregation (while retaining coverage reporting) or otherwise filter unsupported case IDs there.

embodichain/lab/sim/motion/planners/neural_planner.py:284

  • Changing the planner default to five slots leaves the existing example CLI inconsistent: examples/sim/motion/planners/neural_planner.py still accepts --num-waypoints values up to eight, but its NeuralPlannerCfg construction does not pass that argument, so requesting six or more targets leaves the planner at capacity five and _parse_waypoints raises. Forward the CLI capacity into the config or reject values above the supported policy capacity.
    num_waypoints: int = 5
    """Number of constraint slots encoded by the ONNX policy."""

scripts/benchmark/motion_generation/config.py:259

  • joint_eps is passed to NeuralPlanner and convergence uses joint_dist < self._joint_eps, but this validation only rejects non-positive position and rotation tolerances. A zero or negative joint tolerance is accepted and causes every joint waypoint to miss until the rollout budget is exhausted; validate it here with the other NMG tolerances.
            if float(nmg.config.get("pos_eps", 0.01)) <= 0.0:
                raise ValueError("NMG pos_eps must be > 0.")
            if float(nmg.config.get("rot_eps", 0.1)) <= 0.0:
                raise ValueError("NMG rot_eps must be > 0.")

scripts/benchmark/motion_generation/planners/base.py:108

  • This new generic capability gate treats capabilities as proof that a case is representable, but CuroboAdapter advertises joint_waypoint while its plan() still converts every case through PlanState.from_xpos(case.target_waypoints[...]) and prepares only the EEF backend. Atomic ordered_joint_waypoints cases will therefore be counted as supported while being planned in Cartesian space; either add the JOINT_MOVE path/backend preparation or remove that capability until it is implemented.
    def supports_case(self, case: BenchmarkCase) -> tuple[bool, str | None]:
        """Return whether one manifest case is representable without mutation."""
        capability, reason = _case_motion_capability(case)
        if capability is None:
            return False, reason

scripts/benchmark/motion_generation/planners/nmg_onnx.py:97

  • [P2] Normalize nullable NMG defaults before consuming them. SuiteCfg.validate_benchmark() deliberately accepts None for num_waypoints, steps_per_waypoint, and max_steps as requests for their defaults, but this adapter calls int(self.spec.config.get(...)) in supports_case, _case_max_steps, and build; a YAML entry such as max_steps: null therefore passes validation and then crashes with TypeError instead of using 150. Resolve these fields once with the same defaults used by validation and reuse the normalized values.
        capacity = int(self.spec.config.get("num_waypoints", 5))
        if case.num_waypoints > capacity:
            return (
                False,
                f"NMG policy supports at most {capacity} waypoints; case has "
                + f"{case.num_waypoints} and was not truncated or split.",
            )

scripts/benchmark/motion_generation/planners/nmg_onnx.py:104

  • This per-case budget is only applied by NmgOnnxAdapter.plan(). Atomic benchmark cases use the adapter-owned MotionGenerator through AtomicActionEngine; their MotionPolicy leaves plan_opts=None, so NeuralPlanner falls back to the fixed cfg.max_steps (150) instead of min(max_steps, num_waypoints * steps_per_waypoint). A one-waypoint atomic case can therefore run 150 steps rather than the configured 30, changing its success and latency measurement. Propagate a case-specific NeuralPlanOptions into that path or expose an adapter hook for the budget.
    def _case_max_steps(self, case: BenchmarkCase) -> int:
        """Match NMG's training/evaluation budget of 30 steps per waypoint."""
        steps_per_waypoint = int(self.spec.config.get("steps_per_waypoint", 30))
        configured_max = int(self.spec.config.get("max_steps", 150))
        return min(configured_max, int(case.num_waypoints) * steps_per_waypoint)

tests/gym/envs/test_differentiable_embodied_env.py:175

  • [P1] Make the default-hook regression test use the keyword-only tape contract. The wrapper now invokes the hook as env._apply_action_kernel(..., tape=tape), but this test callback still declares tape positionally. The call at line 187 therefore raises TypeError: unexpected keyword argument 'tape' before the assertion; declare the callback with *, tape (or otherwise accept the keyword).
def test_environment_action_hook_receives_no_state_by_default() -> None:
    """The default bridge adapter supplies only action and keyword tape."""
  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 14, 2026 07:03

Copilot AI 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.

🔵 Needs a closer look

Moderate issues remain in backend preparation, null/default handling, fixed-batch filtering, and unsupported-case aggregation.

Review details

Suppressed comments (5)

scripts/benchmark/motion_generation/planners/curobo.py:130

  • This prepares only the move type of the single case passed by the runner. The atomic benchmark contains both EEF and joint cases, while the runner calls prepare() only for supported_cases[0]; with the current ordering the joint backend is first created and warmed lazily inside the first joint plan. That charges backend construction/warmup to planning phases and makes the mixed-modality latency results invalid. Prepare every distinct (batch_size, move_type) used by the supported cases before entering the cold/warm/measured loops.
        return planner.prepare_backend(
            control_part=self.context.control_part,
            batch_size=case.batch_size,
            move_type=self._case_move_type(case),

scripts/benchmark/motion_generation/planners/nmg_onnx.py:92

  • Suite validation treats an explicitly present null as the default (config.py:_positive_int), but this .get returns None when the key is present and int(None) raises. The same failure occurs for steps_per_waypoint/max_steps in _case_max_steps and for max_steps/num_waypoints in build, so a valid YAML override such as num_waypoints: null fails during case filtering instead of using the documented default. Normalize present-but-null values through one resolver before all these reads.
        capacity = int(self.spec.config.get("num_waypoints", 5))
        if case.num_waypoints > capacity:

scripts/benchmark/motion_generation/planners/nmg_onnx.py:98

  • [P2] Reject fixed-batch exports before timed planning. _OnnxPolicy accepts static-batch ONNX graphs but raises whenever a case batch differs from that fixed size. Because supports_case() runs before build() and only checks waypoint capacity, a fixed-B=1 model is marked supported for the B=8/64 suites and fails later as a timed planner exception. Make case filtering honor the model's fixed batch size, or classify such models as non-batched before execution.
    def supports_case(self, case: BenchmarkCase) -> tuple[bool, str | None]:
        """Reject unsupported modalities or sequences beyond policy capacity."""
        supported, reason = super().supports_case(case)
        if not supported:
            return supported, reason
        capacity = int(self.spec.config.get("num_waypoints", 5))
        if case.num_waypoints > capacity:
            return (
                False,
                f"NMG policy supports at most {capacity} waypoints; case has "
                + f"{case.num_waypoints} and was not truncated or split.",
            )
        return True, None

scripts/benchmark/motion_generation/runner.py:422

  • [P1] Exclude unsupported cases from success denominators

When a manifest contains a case beyond the NMG capacity, this records an unsupported_capacity availability record but leaves the case in the manifest passed to aggregation. The _case_macro_* aggregators treat missing measured outcomes as 0.0, so the newly supported partial-capacity path penalizes NMG's success metrics as if the case had failed, contrary to BENCHMARK_DESIGN.md:672-680; planner errors should remain failures, but unsupported cases must be filtered from the success denominator. Propagate the unsupported case IDs/status into aggregation and retain them only for coverage/reporting.

            self._record_unavailable(
                writer,
                metadata,
                case,
                reason or "case is outside planner capacity",
                failure_code="unsupported_capacity",
            )

scripts/benchmark/motion_generation/runner.py:415

  • [P2] Prepare every backend modality before measured plans

CuroboAdapter now has distinct cached EEF and JOINT backends, but this lifecycle later prepares only first_case. In a supported batch containing Cartesian cases followed by joint cases, the first joint plan() lazily builds and warms its backend inside the measured phase, so backend construction/CUDA graph setup contaminates planning latency. Prepare each distinct supported case shape/modality before the cold and measured phases (or expose an adapter-level multi-backend prepare hook).

            supported, reason = adapter.supports_case(case)
            if supported:
                supported_cases.append(case)
                continue
  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 14, 2026 07:18

Copilot AI 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.

🔵 Needs a closer look

Resolve joint-epsilon validation, adapter-based NMG overrides, and outdated benchmark design documentation.

Review details

Suppressed comments (3)

scripts/benchmark/motion_generation/config.py:259

  • The new benchmark config forwards joint_eps into NeuralPlanner, but this validation block only rejects non-positive pos_eps and rot_eps. A suite can therefore set joint_eps <= 0; because _is_active_reached() uses the strict comparison joint_dist < self._joint_eps, every joint-waypoint case will fail to converge and be recorded as a planner failure instead of being rejected during suite validation. Validate joint_eps here with the same > 0 rule (and add it to the precision-validation test).
            if float(nmg.config.get("rot_eps", 0.1)) <= 0.0:
                raise ValueError("NMG rot_eps must be > 0.")

scripts/benchmark/motion_generation/config.py:245

  • [P2] Apply NMG overrides by adapter, not the literal planner id. This validation now intentionally accepts every adapter == "nmg_onnx" spec (including the renamed IDs exercised by the tests), but run_benchmark._apply_overrides() still searches only for spec.id == "nmg"; a valid suite using another ID silently ignores --nmg-onnx-path, --nmg-pos-eps, and --nmg-rot-eps, so the selected NMG planner can remain unavailable or use stale settings. Use the same adapter-based lookup (and handle duplicate NMG adapters explicitly if needed).
        for nmg in (spec for spec in self.planners if spec.adapter == "nmg_onnx"):

scripts/benchmark/motion_generation/planners/base.py:40

  • [P2] Update the benchmark design text with the new joint capability. This mapping makes NMG joint-waypoint cases supported, but BENCHMARK_DESIGN.md still says current NMG supports only EEF_MOVE and that joint-space cases are cuRobo-only, so the authoritative protocol now contradicts the shipped adapter and suites.
    "ordered_joint_waypoints": "joint_waypoint",
  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

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.

3 participants