Skip to content

Add experiment-runner support for VLMStepPolicy, VLMChunkPolicy, and VLMGoalPolicy with DROID PnP example - #1521

Open
xyao-nv wants to merge 3 commits into
mainfrom
xyao/feat/vlm_agent_example
Open

xyao-nv wants to merge 3 commits into
mainfrom
xyao/feat/vlm_agent_example

Conversation

@xyao-nv

@xyao-nv xyao-nv commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Add experiment-runner support for VLMStepPolicy, VLMChunkPolicy, and VLMGoalPolicy. Add a DROID pick-and-place experiment with flat policy configuration and one Run per execution strategy.

Why this change?

The existing VLM policies require callers to construct inference, observation, and action adapters in Python. Allow the experiment runner to construct these policies from typed YAML, using the DROID adapters introduced in #1482.

What changes?

  • Register the three existing VLM policies with typed configuration classes and support loading them by dotted class path.
  • Add vlm_embodiment_adapter: droid to supply absolute-pose action adapters, Robotiq gripper measurements for goal completion, and strategy-specific default prompts.
  • Expose flat fields for observation modality, chunk size, goal step budget, and pose tolerances. Preserve explicit Python dependency injection and system-prompt overrides.
  • Discover camera observations from the runtime environment when no embodiment is passed to the observation adapter.
  • Add droid_abs_ik for absolute pose control and use the action manager's dimension when validating goal commands.
  • Close policy-owned inference clients while leaving injected clients caller-owned.

Usage example

Before, construct the dependencies in Python:

policy = VLMGoalPolicy(
    config,
    inference_backend=backend,
    observation_adapter=observation_adapter,
    action_adapter=action_adapter,
)

After, declare the policy in an Experiment Definition:

policy:
  type: isaaclab_arena_vlm_agent.vlm_agent_policy.VLMGoalPolicy
  endpoint: internal
  vlm_embodiment_adapter: droid
  observation_modality: image_state
  max_steps_per_goal: 30

Use environment.embodiment: droid_abs_ik with cameras enabled. The example YAML also includes step and chunk policies. Set NV_API_KEY for the configured internal endpoint; credentials remain outside the Experiment Definition.

Validation

  • 110 focused unit tests passed, covering typed YAML loading and serialization, configuration overrides, execution strategies, resets, and client ownership.
  • All pre-commit checks passed.
  • A 20-step zero-action rollout passed in the uv environment with Isaac Sim 6.2 and Warp 1.17.
  • The three-strategy VLM experiment is running in uv with viewport and camera video recording. Its task-success results are pending.

Signed-off-by: Xinjie Yao <xyao@nvidia.com>
Signed-off-by: Xinjie Yao <xyao@nvidia.com>
@xyao-nv
xyao-nv marked this pull request as ready for review October 24, 2026 04:39
@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes reach the typed experiment-runner path for VLM-controlled DROID runs.

Fix discovery of the new short policy names before merging.

Findings

  1. P1 Policy names fail to load ▶

Summary

Adds YAML-driven construction for the three VLM execution strategies and a DROID pick-and-place example.

  • The experiment runner can build VLM policies from YAML.
  • DROID VLM runs can send absolute pose targets to the robot.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Experiment YAML] --> B[Resolve policy selector]
  B -->|Dotted class path| C[Import VLM policy module]
  B -->|Short name in fresh process| X[Name not discovered]
  C --> D[Build typed policy configuration]
  D --> E[Create observation and DROID action adapters]
  E --> F[Create owned inference client]
  F --> G[Execute step, chunk, or goal strategy]
  G --> H[Close policy and environment]
Loading

Reviews (1) · Last reviewed commit: "Use registry_name for the DROID IK embod..." · Reviewed by Greptile

Comment on lines +297 to +301
@register_policy
class VLMStepPolicy(VLMPolicy, PolicyBase[VLMStepPolicyCfg]):
"""Predict one action per environment from fresh observations on every step."""

name = "vlm_step"

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.

P1 Policy names fail to load

A fresh runner cannot load vlm_step, vlm_chunk, or vlm_goal by name. ensure_assets_registered() never imports this module, so a YAML policy such as type: vlm_step fails the loader's dotted-path assertion before the experiment starts. The selector test misses this because it imports all three policies first.

Add lazy discovery for these names and test name-based loading without first importing the policies.

return True


@pytest.mark.with_cameras

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.

🟡 This camera test won't get cameras in CI

The "Run VLM agent policy tests" step runs pytest isaaclab_arena_vlm_agent/tests/ in one process with no -m filter (ci.yml:318-321, uv-ci.yml:198-203). So with_cameras doesn't move this test to a separate phase. test_droid_vlm_adapters_simulation.py runs first and starts the shared SimulationApp without cameras. After that, get_persistent_simulation_app prints a warning and ignores enable_cameras=True. This test then builds a camera environment on an app that was started without camera rendering.

Could this test run without cameras instead (policy.observation_modality=state, environment.enable_cameras=false)? The unit test test_configured_execution_and_partial_reset already covers finding camera terms. The other fix is to split the VLM CI step by marker, but that's a workflow change.



@pytest.mark.with_cameras
@pytest.mark.parametrize("mode", ["step", "chunk", "goal"])

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.

🔵 Three full scene builds for one end-to-end check

Each case builds the maple-table scene with DROID, an HDR and cameras. Existing tests already cover most of what differs between the modes:

  • Step vs chunk behavior runs without the simulator in test_configured_execution_and_partial_reset and test_droid_vlm_adapters.py::test_policy_execution_and_partial_resets.
  • Live absolute-pose decoding and gripper measurements run in test_droid_absolute_pose_adapters.

The goal case covers the rest: a YAML run building the registered policy in build_and_run, the total_action_dim check, the Robotiq gripper measurements, and closing the policy's own client. Could we keep only that case?

Suggested change
@pytest.mark.parametrize("mode", ["step", "chunk", "goal"])
@pytest.mark.parametrize("mode", ["goal"])

from isaaclab_arena_vlm_agent.response_errors import InvalidAgentResponseError


class AbsoluteStepActionAdapter(AgentActionAdapter):

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.

🔵 A second way to run single-step DROID control

In #1482, single-step absolute control was settled as AbsoluteChunkActionAdapter(1). Its docstring still says "use chunk_size=1 for one prediction per step", and test_droid_vlm_adapters.py::_policy runs "step" as VLMChunkPolicy with that adapter. This class brings a step adapter back, and its body matches AbsoluteGoalActionAdapter line for line.

Does vlm_step need a DROID preset? If not, the example could drop the step run, or use vlm_chunk with chunk_size: 1. If we keep it, could the three absolute adapters share the index lookup and decoding? Then the chunk_size=1 hint in the chunk docstring can go.

vlm_embodiment_adapter: str | None = "droid"
"""Named action and prompt preset."""

chunk_size: int = 1

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.

🔵 Should chunk_size be required?

With the default of 1, vlm_chunk does the same thing as vlm_step, only with a different response schema. AbsoluteChunkActionAdapter requires chunk_size for that reason (from the #1482 review), and max_steps_per_goal in VLMGoalPolicyCfg is required the same way. The tests that build VLMChunkPolicyCfg without it would need to pass chunk_size.

Suggested change
chunk_size: int = 1
chunk_size: int = field(kw_only=True)

vlm_embodiment_adapter: str | None = None
"""Named action and prompt preset; droid supports the droid_abs_ik embodiment."""

observation_modality: str = "image_state"

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.

🔵 Enum for observation_modality?

The three allowed values appear in __post_init__ and again in the dict in VLMPolicy.__init__. Gr00tRemoteClosedloopPolicyCfg.scheduler uses a str Enum with a method that returns the class, and the typed loader converts YAML strings to enums. So the YAML can stay observation_modality: image_state:

class ObservationModality(str, Enum):
    """Observations included in each VLM request."""

    IMAGE = "image"
    STATE = "state"
    IMAGE_STATE = "image_state"

    def get_adapter_type(self) -> type[VLMObservationAdapter]:
        """Return the observation adapter class this modality selects."""
        return {
            ObservationModality.IMAGE: ImageObservationAdapter,
            ObservationModality.STATE: StateObservationAdapter,
            ObservationModality.IMAGE_STATE: ImageStateObservationAdapter,
        }[self]


def __init__(self, embodiment: EmbodimentBase):
observation_cfg = embodiment.get_observation_cfg()
def __init__(self, embodiment: EmbodimentBase | None = None):

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.

🔵 Two places to read camera terms from

VLMPolicy.__init__ now builds this adapter without an embodiment, so only tests still pass one. In #1482, StateObservationAdapter was changed to read the live managers, because callbacks can change the built environment. The embodiment path here can still disagree with the environment.

Could we drop the embodiment parameter here and in ImageStateObservationAdapter, and read env.unwrapped.cfg.observations on each call? That also removes the camera_keys value that is filled in on first use.

)
)

def close(self) -> None:

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.

🔵 close() can be smaller

build_and_run builds a new policy for each rebuild and closes it once through close_run_resources. Clearing pending commands and the _closed flag only guard a second close(), which nothing in Arena makes. The test's double close() and its _chunks/_goals checks after it could go too. Reaching into self._inference_backend.client also suggests InferenceBackend should have its own close().

    def close(self) -> None:
        """Close the inference client when this policy created it."""
        if self._owns_inference_backend:
            self._inference_backend.client.close()


registry_name = "droid_abs_ik"

def get_action_cfg(self):

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.

🔵 action_config and get_action_cfg() disagree

This is the only embodiment that overrides get_action_cfg, so self.action_config still has relative mode with scale=0.5 while the environment gets absolute targets. ArenaEnvBuilder calls configure_physics_backend for every backend before get_action_cfg() (arena_env_builder.py:334 and :401). So the switch could happen where the DROID classes already adjust their action config, after the Newton IK override. That keeps one config object and drops the copy:

    def _configure_physics_backend(self, backend: PhysicsBackend) -> None:
        """Apply the backend's IK settings, then accept unscaled absolute pose targets."""
        super()._configure_physics_backend(backend)
        self.action_config.arm_action.controller.use_relative_mode = False
        self.action_config.arm_action.scale = 1.0

@arena-review-bot

Copy link
Copy Markdown
Contributor

🤖 Isaac Lab-Arena Review Bot

Summary

This PR registers VLMStepPolicy, VLMChunkPolicy and VLMGoalPolicy with typed configs, so the experiment runner can build them from YAML. It adds a droid preset for action adapters and prompts, a droid_abs_ik embodiment, and a three-run DROID pick-and-place example. It also fixes VLMGoalPolicy to read the action size from action_manager.total_action_dim. The main problem is the new simulation test: in CI it won't get the camera-enabled SimulationApp it asks for. The other comments are simplifications.

Design, Boundaries & Scope

  • There are now two ways to run single-step DROID control. In Add robot-specific observation & action adapters for VLM policies #1482, single-step absolute control was settled as AbsoluteChunkActionAdapter(1), and the existing DROID tests still run "step" that way. This PR adds AbsoluteStepActionAdapter for vlm_step. With VLMChunkPolicyCfg.chunk_size defaulting to 1, vlm_chunk with the default setting also does one prediction per step. Could we pick one? See the comments on absolute_action_adapter.py:17 and vlm_agent_policy.py:113.

Findings

🟡 Warning: isaaclab_arena_vlm_agent/tests/test_vlm_policy_configuration_simulation.py:66: The VLM CI step runs the whole folder in one process with no marker filter. An earlier test file starts the shared SimulationApp without cameras, and later enable_cameras=True requests are ignored. So this camera test never gets camera rendering in CI.
🔵 Improvement: isaaclab_arena_vlm_agent/absolute_action_adapter.py:17: The new step adapter brings back what #1482 removed. Its body matches AbsoluteGoalActionAdapter, and the chunk adapter's docstring now gives conflicting advice.
🔵 Improvement: isaaclab_arena_vlm_agent/vlm_agent_policy.py:113: With the default chunk_size of 1, vlm_chunk does the same thing as vlm_step. Make it required, as max_steps_per_goal is.
🔵 Improvement: isaaclab_arena_vlm_agent/vlm_agent_policy.py:58: observation_modality could be a str Enum, following ActionSchedulerType in the GR00T policy.
🔵 Improvement: isaaclab_arena_vlm_agent/observation_adapter.py:97: Production code always finds camera terms from the live env, so only tests still pass an embodiment. Drop that parameter.
🔵 Improvement: isaaclab_arena_vlm_agent/vlm_agent_policy.py:204: close() doesn't need the reset() or the _closed flag.
🔵 Improvement: isaaclab_arena/embodiments/droid/droid.py:300: self.action_config and get_action_cfg() now disagree for droid_abs_ik. Set the absolute mode in _configure_physics_backend instead.

Test Coverage and CI Cost

  • test_vlm_experiment builds the full maple-table scene with DROID, an HDR and cameras three times. Existing non-simulation tests already cover the step and chunk differences. The goal case alone covers the behavior that needs the live simulator (see the inline comment on line 67).
  • This was raised on Add robot-specific observation & action adapters for VLM policies #1482 and still applies: isaaclab_arena_vlm_agent/tests/ has no conftest.py, so the failure hooks in isaaclab_arena/tests/conftest.py aren't registered for this CI step. Once a simulation test has started the shared SimulationApp, a failing pure-Python test in this folder can still let the process exit 0 when the app closes. This PR adds about 25 more tests to that step. A one-line conftest that re-exports pytest_sessionstart and pytest_runtest_logreport would fix it.
  • I couldn't check test results: the CI test jobs for e132f434 hadn't run yet when this review was written (the image build was still in progress).

Verdict

Minor fixes needed

Signed-off-by: Xinjie Yao <xyao@nvidia.com>

This branch has not been deployed

No deployments
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.

1 participant