transfer / eval_stack /harness /policy.py
Ronaldo-GOAT's picture
eval_stack: pi0.5 negmesh8 160-episode replay eval harness, job scripts, protocol docs
51c2c72 verified
Raw History Blame Contribute Delete
4.46 kB
from collections.abc import Sequence
import logging
import pathlib
import time
from typing import Any, TypeAlias
import flax
import flax.traverse_util
import jax
import jax.numpy as jnp
import numpy as np
from openpi_client import base_policy as _base_policy
from typing_extensions import override
from openpi import transforms as _transforms
from openpi.models import model as _model
from openpi.shared import array_typing as at
from openpi.shared import nnx_utils
BasePolicy: TypeAlias = _base_policy.BasePolicy
class Policy(BasePolicy):
def __init__(
self,
model: _model.BaseModel,
*,
rng: at.KeyArrayLike | None = None,
transforms: Sequence[_transforms.DataTransformFn] = (),
output_transforms: Sequence[_transforms.DataTransformFn] = (),
sample_kwargs: dict[str, Any] | None = None,
metadata: dict[str, Any] | None = None,
):
self._sample_actions = nnx_utils.module_jit(model.sample_actions)
self._input_transform = _transforms.compose(transforms)
self._output_transform = _transforms.compose(output_transforms)
self._rng = rng or jax.random.key(0)
self._sample_kwargs = sample_kwargs or {}
self._metadata = dict(metadata or {})
# REPRO FIX: advertise the model's action shape so clients can size a per-query
# noise tensor without guessing (see infer()).
self._metadata.setdefault("action_horizon", int(getattr(model, "action_horizon", 0)))
self._metadata.setdefault("action_dim", int(getattr(model, "action_dim", 0)))
@override
def infer(self, obs: dict) -> dict: # type: ignore[misc]
# Make a copy since transformations may modify the inputs in place.
inputs = jax.tree.map(lambda x: x, obs)
# REPRO FIX: an optional client-supplied flow-matching noise tensor of shape
# [action_horizon, action_dim]. When present it fully determines the sample, so
# the result no longer depends on this server's cumulative RNG stream (which is
# advanced by every query from every client). Popped before the transforms.
client_noise = inputs.pop("noise", None) if isinstance(inputs, dict) else None
inputs = self._input_transform(inputs)
# Make a batch and convert to jax.Array.
inputs = jax.tree.map(lambda x: jnp.asarray(x)[np.newaxis, ...], inputs)
start_time = time.monotonic()
self._rng, sample_rng = jax.random.split(self._rng)
sample_kwargs = dict(self._sample_kwargs)
if client_noise is not None:
client_noise = np.asarray(client_noise, dtype=np.float32)
expected = (self._metadata["action_horizon"], self._metadata["action_dim"])
if tuple(client_noise.shape) != expected:
raise ValueError(f"client noise shape {client_noise.shape} != model (action_horizon, action_dim) {expected}")
sample_kwargs["noise"] = jnp.asarray(client_noise)[np.newaxis, ...]
outputs = {
"state": inputs["state"],
"actions": self._sample_actions(sample_rng, _model.Observation.from_dict(inputs), **sample_kwargs),
}
# Unbatch and convert to np.ndarray. # Unbatch and convert to np.ndarray.
outputs = jax.tree.map(lambda x: np.asarray(x[0, ...]), outputs)
model_time = time.monotonic() - start_time
outputs = self._output_transform(outputs)
outputs["policy_timing"] = {
"infer_ms": model_time * 1000,
}
return outputs
@property
def metadata(self) -> dict[str, Any]:
return self._metadata
class PolicyRecorder(_base_policy.BasePolicy):
"""Records the policy's behavior to disk."""
def __init__(self, policy: _base_policy.BasePolicy, record_dir: str):
self._policy = policy
logging.info(f"Dumping policy records to: {record_dir}")
self._record_dir = pathlib.Path(record_dir)
self._record_dir.mkdir(parents=True, exist_ok=True)
self._record_step = 0
@override
def infer(self, obs: dict) -> dict: # type: ignore[misc]
results = self._policy.infer(obs)
data = {"inputs": obs, "outputs": results}
data = flax.traverse_util.flatten_dict(data, sep="/")
output_path = self._record_dir / f"step_{self._record_step}"
self._record_step += 1
np.save(output_path, np.asarray(data))
return results