Running Evaluations¶
An evaluation drives a model against an environment one step at a time: reset, predict, step, repeat until the episode ends. RLMesh exposes three entry points onto that loop, from one fully automated call down to a read-only peek at a single observation. All three resolve the model-to-environment adapter from the environment’s published contract and the model’s ModelSpec, so your prediction code only ever sees its own declared input and output format with no per-environment glue.
This page is the home for the loop itself: run(), session(), Session.read / Session.reader, and the execution horizon. See Adapters for how tags and specs match into the adapter every entry point applies, and Models for the predict corners a model implements.
Pick an entry point¶
Entry point |
You get |
Reach for it when |
|---|---|---|
|
One call drives whole episodes and returns a typed |
Scoring a model: leaderboards, sweeps, CI checks. |
|
A |
Rendering, custom stop conditions, branching, or mixing your own per-step logic. |
|
A read-only, role-addressed view of each raw observation. |
Inspecting an env, logging canonical roles, or shaping a reward. |
The three share one resolution step and diverge only in how much of the loop they drive for you.
flowchart LR
contract["env contract<br/>(published tags)"] --> resolve["resolve adapter"]
spec["model spec"] --> resolve
resolve --> run["run()<br/>whole episodes"]
resolve --> session["session()<br/>step by hand"]
resolve --> read["read() / reader()<br/>inspect by role"]
run(): the automated rollout¶
run() pumps full episodes to completion and returns a RunResult. Lead with the bound method on a model:
result = model.run(env, seeds=range(100))
print(f"{result.success_rate:.0%} success, mean reward {result.mean_reward:.2f}")
env may be a local Gymnasium-style env, an EnvFactory (built and tag-stamped, then driven locally), a remote handle such as a RemoteEnv, or a bare address string the loop dials:
result = model.run("tcp://127.0.0.1:5555", seeds=range(100))
The module-level run() is the same loop over an explicit (model, env) pair. Its model argument is the flexible one: a bare predict callable, a Model subclass class or instance, or a served RemoteModel / SandboxModel handle. Pass rlmesh.RANDOM_SAMPLE for a baseline that samples the action space and ignores observations:
import rlmesh
result = rlmesh.run(my_policy_fn, env, seeds=range(10)) # any obs -> action callable
baseline = rlmesh.run(rlmesh.RANDOM_SAMPLE, env, max_episodes=10)
Arguments¶
Argument |
Default |
Meaning |
|---|---|---|
|
|
Per-episode seed sequence; also sets the episode count unless |
|
|
Number of episodes to run; overrides the length of |
|
|
Overrides every |
|
|
Actions executed per predicted chunk; only engages on a chunk corner (see below). |
|
|
Shut the env down when the run finishes (opt-in). |
|
|
Auth token for a remote env or model. |
With neither seeds nor max_episodes, run() does a single episode. execution_horizon is accepted by both the bound methods (model.run / model.session) and the module-level run() / session(), which forwards it through.
The result¶
RunResult is immutable and aggregates its episodes:
Member |
Type |
Meaning |
|---|---|---|
|
|
One |
|
|
Mean total reward across episodes. |
|
|
Fraction of episodes that succeeded. |
|
|
Episode count. |
|
|
Summed steps across episodes. |
Each EpisodeResult carries index, seed, steps, reward, terminated, truncated, and success:
for ep in result.episodes:
print(ep.index, ep.seed, ep.steps, ep.reward, ep.terminated, ep.success)
Caution
success_rate prefers the env’s own task outcome: Gymnasium’s info["is_success"] or info["success"], captured per episode as the success field on EpisodeResult. When the env emits no such flag it falls back to terminated for that episode. A time-limit env whose success is the truncation cap should report it through info, because a plain terminal state is not read as success.
session(): manual, step-by-step control¶
session() hands back a Session you drive yourself. Use it as a context manager so the env connection (and any managed model) closes on exit:
with model.session(env, instruction="put the cup on the plate") as sess:
obs, info = sess.reset(seed=0)
while not sess.done:
action = sess.predict(obs)
obs, reward, terminated, truncated, info = sess.step(action)
The loop primitives mirror Gymnasium, with the adapter folded in.
flowchart LR
reset["reset(seed)"] --> predict["predict(obs)"]
predict --> step["step(action)"]
step -->|not done| predict
step -->|done| reset
sess.reset(seed=None)→(obs, info). Begins an episode; ends the previous one (firingon_episode_end) and clears adapter state such as the frame-stack buffer.sess.predict(obs)→action. Applies the model’s adapter around the model’s own predict: the declarative obs transform, host-side frame stacking, anyCustomcode, instruction injection into declared text leaves, and chunk replay (one action per call). Returns an env-ready action.sess.step(action)→(obs, reward, terminated, truncated, info). Applies the action and records reward and termination.sess.doneisTrueonce the current episode terminated or truncated.sess.close()releases the connection, shuts the env down only on theclose_envopt-in, and fireson_close.
Drive multiple episodes by hand when you want to branch on each step: render a frame, apply your own stop condition, or fork the rollout.
sess = model.session(env)
try:
for seed in range(10):
obs, info = sess.reset(seed=seed)
while not sess.done:
action = sess.predict(obs)
obs, reward, terminated, truncated, info = sess.step(action)
if my_should_stop(info):
break
finally:
sess.close()
The context-manager form above is the idiomatic one; the explicit try/finally is the same thing written out when you cannot wrap the whole loop in a with. The module-level session() accepts the same flexible model argument as run(), including rlmesh.RANDOM_SAMPLE.
on_episode_end fires at every episode boundary (the next reset(), or close() for the last episode), so a stateful model clears its per-episode state identically whether you drive by hand or call run(). Session.run pumps whole episodes through these same primitives and is exactly what model.run(...) calls under the hood.
read and reader: inspect observations by role¶
reader and read give a read-only, role-addressed view of a raw observation. They reuse the model adapter pipeline pointed at the consumer (resolve_from_contract() plus the obs transform with a no-op action), so they are encoding-agnostic across envs and never mutate the observation.
sess.reader(*items) resolves once and returns a callable mapping a raw observation to {role: value}:
import rlmesh.adapters as adapt
with model.session(env) as sess:
read = sess.reader(adapt.Image(adapt.IMAGE_PRIMARY, layout="hwc"), adapt.EEF_POS)
obs, _ = sess.reset(seed=0)
while not sess.done:
view = read(obs) # {IMAGE_PRIMARY: ..., EEF_POS: ...}
screen.show(view[adapt.IMAGE_PRIMARY])
obs, *_ = sess.step(sess.predict(obs))
sess.read(obs, item) is the one-shot single-role convenience. The underlying reader is cached per item, so calling it every step does not re-resolve:
ee = sess.read(obs, adapt.EEF_POS)
img = sess.read(obs, adapt.Image(adapt.IMAGE_PRIMARY, layout="hwc"))
An item is one of:
A bare role constant (
adapt.EEF_POS,adapt.IMAGE_PRIMARY), kept in the env’s native encoding and using the env’s own declared layout.A model-input leaf that declares the encoding you want, such as
adapt.Image(adapt.IMAGE_PRIMARY, layout="hwc")oradapt.State(adapt.EEF_POS). The adapter converts to that form whatever the env stores.
Roles and leaves are the same vocabulary the rest of the adapter system uses; see Adapters. The env must publish adapter tags (via an EnvFactory or rlmesh.adapters.tag(...)), or the read raises an AdapterResolutionError, since there are no roles to address otherwise. Values come back in the env’s own framework (NumPy for a Gymnasium env, torch for a torch route).
Three things this is for:
Debugging an env. Confirm what a camera returns (shape, layout, value range) without threading it through a model.
Logging canonical roles. Record
EEF_POSor the primary image the same way across heterogeneous envs, since the role addresses the quantity, not the env’s key.Reward shaping. Compute a shaped term over canonical roles, e.g.
reward - 0.1 * distance(sess.read(obs, adapt.EEF_POS), goal).
Execution horizon, end to end¶
A policy that emits an action chunk (ACT, diffusion, flow, VLA action heads) defines a predict_chunk or predict_chunk_batch corner (see the four corners in Models). execution_horizon tells the rollout how many actions of each predicted chunk to apply before re-planning:
result = model.run(env, seeds=range(50), execution_horizon=8)
The runtime owns the replay. It calls the model once, executes the first execution_horizon actions of the returned chunk one per env step, then calls the model again.
flowchart LR
predict["predict_chunk(obs)"] --> chunk["native chunk [H, ...]"]
chunk --> prefix["execute first execution_horizon<br/>actions, one per env step"]
prefix -->|chunk exhausted| predict
One action is applied per step regardless. The model returns its native chunk and the runtime uses a prefix of it; an autoregressive head that declares execution_horizon can instead decode exactly that many.
execution_horizon only matters when the model defines a chunk corner. Requesting execution_horizon > 1 on a model with no predict_chunk warns and runs un-chunked (one fresh prediction per step), so the default of 1 is always safe.
Where next¶
Models: the four predict corners, the lifecycle seams, and the batched-observation fusion this loop drives.
Adapters: how environment tags and a model spec resolve into the adapter every entry point applies.
Serve an Environment: addresses, readiness, and health for the remote envs
run()andsession()dial.Core Exports: the autodoc signatures for the top-level
run/sessionentry points and the result types.