Server–Client Policies#
Robot foundation models such as vision-language-action models (VLAs) and world-model policies
bring their own dependencies, GPUs, and release schedules. Arena keeps them out of the
simulation process. The model runs as a server, and Arena runs a thin client policy
that implements PolicyBase. The two talk over a websocket, so the server can run in another
container, on another GPU, or on another machine.
┌──────────── Arena process ────────────┐ ┌──── model server ────┐
│ env obs ─► EmbodimentAdapter │ request │ │
│ extract + pack_request ────┼──────────► │ infer(request) │
│ │ │ │
│ env.step ◄─ action ◄─ chunk replay ◄──┼─────────── │ {"actions": (H, D)} │
└───────────────────────────────────────┘ response └──────────────────────┘
Arena includes clients for Cosmos (CosmosRemotePolicy), π0 and π0.5 from OpenPI
(Pi0RemotePolicy), GR00T (Gr00tRemoteClosedloopPolicy), and DreamZero
(DreamZeroRemotePolicy). The OpenPI
and GR00T quickstarts start a server and
run its client end to end.
How the client works#
The Cosmos and OpenPI clients share one implementation, remote_policy_base.py, which separates what is sent from how it is sent.
An EmbodimentAdapter decides what is sent for one robot and one server format. Its
extract(obs, env_id) method pulls one environment’s camera images and joint state out of
Arena’s observation dictionary. pack_request(extracted, instruction) turns them into the
request the server expects. action_dim declares the width of each action the server returns.
RemoteChunkReplayPolicy decides how it is sent, and every adapter shares it. For each
environment, it sends one request and receives an action chunk of shape (H, action_dim). It
replays the first open_loop_horizon actions, one per step, and then requests a new chunk.
When an environment resets, or the connection drops and reconnects, the client discards the
cached chunk so that it never replays stale actions. close() disconnects the client but
leaves the server running.
Requests and responses are flat dictionaries of NumPy arrays and strings, packed with
msgpack_numpy. For example, the Cosmos DROID adapter sends:
{
"observation/image": uint8[H, W, 3], # wrist view on top, two exterior views below
"observation/joint_position": float32[7],
"observation/gripper_position": float32[1],
"prompt": "put the banana in the bowl",
}
# response: {"action": float32[H, 8]}
The client’s configuration holds the connection settings: remote_host, remote_port,
ping_interval, ping_timeout, and policy_device. The RoboLab example below evaluates
π0.5 and Cosmos on the same two tasks:
Configuration file (robolab_2_tasks_pi0_and_cosmos.yaml)
# Runs two robolab tasks with both the OpenPI (pi0) and Cosmos policies (4 runs)
# Values under shared are used by every Run below.
# A Run only needs to list what is different; its values override the shared ones.
# To change a shared value for every Run on the command line:
# - with experiment_runner.py, use shared.<path>=<value>
# - with osmo/submit_arena_experiment.py, use experiment_cfg.shared.<path>=<value>
shared:
environment:
enable_cameras: true
environment_builder:
num_envs: 25
rollout_limit:
num_episodes: 100
variations:
droid_abs_joint_pos:
camera_extrinsics_wrist_camera:
enabled: false
sampler_cfg:
low: [-0.05, -0.05, -0.05]
high: [0.05, 0.05, 0.05]
runs:
banana_in_bowl_pi0:
environment: &banana_in_bowl_env
type: isaaclab_arena_environments/robolab/tasks/banana_in_bowl.yaml
policy: &openpi_policy
type: isaaclab_arena_openpi.policy.pi0_remote_policy.Pi0RemotePolicy
policy_variant: pi05
policy_device: cuda:0
remote_host: 127.0.0.1
remote_port: 8000
openpi_embodiment_adapter: droid
banana_in_bowl_cosmos:
environment: *banana_in_bowl_env
policy: &cosmos_policy
type: isaaclab_arena_cosmos.policy.cosmos_remote_policy.CosmosRemotePolicy
cosmos_embodiment_adapter: droid
policy_device: cuda:0
remote_host: 127.0.0.1
remote_port: 8000
banana_on_plate_pi0:
environment: &banana_on_plate_env
type: isaaclab_arena_environments/robolab/tasks/banana_on_plate.yaml
policy: *openpi_policy
banana_on_plate_cosmos:
environment: *banana_on_plate_env
policy: *cosmos_policy
GR00T and DreamZero servers use their own protocols, so Arena keeps their native clients. Both
still implement PolicyBase, and the runners treat them like any other policy. GR00T also
converts chunks into per-step actions through a configurable ActionScheduler (see
action_scheduling/).
Add a model or a robot#
To run an existing model on a new robot, write an EmbodimentAdapter for that robot’s
cameras, joint state, and action layout. Then add its name to the client’s adapter lookup,
which cosmos_embodiment_adapter or openpi_embodiment_adapter selects. The transport
does not change.
To add a model whose server speaks the OpenPI protocol, subclass RemoteChunkReplayPolicy and
PolicyBase[YourCfg]. Set name and server_response_actions_key, and pass your adapter
and horizon to super().__init__. CosmosRemotePolicy is the reference: it differs from
the OpenPI client only in its response key and adapter.
cosmos_remote_policy.py
from __future__ import annotations
from typing import TYPE_CHECKING
from isaaclab_arena.assets.register import register_policy
from isaaclab_arena.policy.policy_base import PolicyBase
from isaaclab_arena.policy.remote_policy_base import RemoteChunkReplayPolicy
from isaaclab_arena_cosmos.policy.cosmos_remote_config import CosmosRemotePolicyCfg
if TYPE_CHECKING:
from isaaclab_arena_cosmos.policy.droid_adapter import CosmosDroidAdapter
# Cosmos serves over openpi's WebsocketPolicyServer, so this reuses the shared
# openpi-protocol machinery (RemoteChunkReplayPolicy). It differs only in the response key
# (the server returns the chunk under the singular "action") and the DROID wire format
# (see CosmosDroidAdapter); the open-loop horizon comes from the config rather than a
# per-variant table because the released Cosmos policies are a single DROID family.
@register_policy
class CosmosRemotePolicy(RemoteChunkReplayPolicy, PolicyBase[CosmosRemotePolicyCfg]):
"""Cosmos remote closed-loop policy, parameterized by an embodiment adapter."""
name = "cosmos_remote"
server_response_actions_key = "action"
def __init__(self, config: CosmosRemotePolicyCfg) -> None:
adapter = _resolve_cosmos_embodiment_adapter(config.cosmos_embodiment_adapter)
super().__init__(config, adapter, config.open_loop_horizon)
@classmethod
def default_image_size(cls, config: CosmosRemotePolicyCfg) -> tuple[int, int]:
"""Return the (height, width) the selected Cosmos adapter resizes camera images to."""
adapter = _resolve_cosmos_embodiment_adapter(config.cosmos_embodiment_adapter)
return adapter.IMAGE_H, adapter.IMAGE_W
def _resolve_cosmos_embodiment_adapter(key: str) -> CosmosDroidAdapter:
"""Instantiate the adapter registered under ``key``.
Imports are deferred to call time so adapter modules can import the shared
``EmbodimentAdapter`` at module top without creating a circular import.
"""
if key == "droid":
from isaaclab_arena_cosmos.policy.droid_adapter import CosmosDroidAdapter
return CosmosDroidAdapter()
raise ValueError(f"Unknown cosmos_embodiment_adapter {key!r}; expected 'droid'")
If the server uses its own protocol, implement PolicyBase directly, as the GR00T client
does. Keep the client thin. Image preprocessing, chunk handling, and reconnects belong in the
client, and model code belongs in the server.
Because the model sits behind a socket, scaling up is a deployment choice, not a code change.
Point remote_host at a larger GPU or another node, and raise num_envs to run more
environments per client. On OSMO, each client policy is paired with a server task in
osmo/tasks/, so one workflow launches both. To serve a new model there, subclass
PolicyServerTask and register it with @register_server_task.
Next steps#
Continue to Agent as Policy to use a hosted foundation model as a policy without robot training. See Arena Experiments to submit a server–client evaluation to OSMO.