Model Reference¶
The complete reference for the Model class: every predict corner and how the runtime derives the ones you leave out, the batching and chunk-replay semantics, the device and framework handling, the model-quirk recipes, and the serve-and-connect surface.
For the concepts (the two construction styles, the spec, the lifecycle), start with Models. For how the model spec matches an environment, see Adapters and Adapter Reference. For the evaluation loop (run / session / read), see Running Evaluations. For exact signatures, see Models. Examples live at examples/python/vla_adapters.
The four corners¶
A model implements one or more of four predict corners. They sit on a 2×2 lattice over a batch axis (one lane vs. all vectorized lanes fused into one forward) and a chunk axis (one action vs. a chunk of future actions per forward).
Corner |
Signature |
Receives |
Returns |
Reach for it when |
|---|---|---|---|---|
|
|
one observation |
one action |
a simple per-step policy with nothing to chunk or batch |
|
|
one observation |
action chunk |
an ACT / diffusion / flow head that emits a chunk per forward |
|
|
one fused observation, leaves |
batched action |
a batched policy with one forward over all lanes |
|
|
one fused observation, leaves |
batched chunk |
a batched VLA / action-head policy (all four derive from it) |
On the chunk corners the leading axis of the return is the chunk axis. On the batch corners the leading axis is the batch axis; a chunk corner that is also batched returns the batch axis first, then the per-lane chunk axis ([N, H, ...]). A predict that subclasses Model and does not override one of these inherits a method that raises, so the runtime knows it is undefined.
Corner synthesis¶
Implement the most general corner your policy supports and the runtime fills the rest. It only ever derives downward along the lattice.
graph TD
PCB["predict_chunk_batch<br/>[N, H, ...]"] -->|de-batch| PC["predict_chunk<br/>[H, ...]"]
PCB -->|de-chunk| PB["predict_batch<br/>[N, ...]"]
PB -->|de-batch| P["predict<br/>one action"]
PC -->|de-chunk| P
Derivation |
How it works |
|---|---|
De-batch |
Stack the lone observation into a batch of one, call the batched corner, peel lane 0 back off. Turns |
De-chunk |
Run the chunk corner at horizon 1 and take the first action. Turns |
So predict_chunk_batch alone yields all four corners. Going up either axis is impossible: chunking is a model capability rather than glue, and batching up is left to the engine’s per-lane loop.
Two cases are worth pinning down:
The ambiguous
predict. When bothpredict_batchandpredict_chunkexist butpredictdoes not, the runtime derivespredictfrom the un-chunkedpredict_batch, so a single-step call stays un-chunked instead of paying for a chunk decode it would discard.The native backend cannot de-chunk. De-chunking indexes the chunk axis on array leaves, which the native
rlmesh.Modelover raw values does not have. A chunk-only model on that backend must definepredict()explicitly, or construction raises a clearTypeError. The numpy, torch, and jax backends have no such restriction.
Note
De-batch and de-chunk are byte-consistent with the served path. The local first-frame split matches
the engine’s split_chunk, so a model derived down to predict behaves the same whether you drive it
in-process or over the wire.
The execution horizon¶
execution_horizon is optional on the chunk corners and detected by arity. A corner with a single positional parameter (predict_chunk(obs)) is called without the horizon; a corner that declares a second positional parameter (predict_chunk(obs, execution_horizon=1)) receives it.
Form |
Behavior |
|---|---|
|
The horizon is swallowed before the call. Return the native chunk; the runtime executes a prefix of it. Most policies use this; a trained chunk length is fixed by the weights. |
|
The runtime fills |
def predict_chunk(self, observation, execution_horizon=1):
chunk = self.policy.decode(observation) # native [H_native, A]
return chunk[:execution_horizon] # truncate to what runs
Replay semantics¶
The runtime owns the replay. It calls the model once, splits the returned chunk along its leading axis, executes one action per environment step, and re-plans only when the queue drains. A horizon of 1 is a passthrough that never queues. A horizon above 1 caps the replay at that many actions, so a receding-horizon model may emit a longer chunk than it re-plans. The episode boundary (reset) drops any un-replayed tail, so a chunk never bleeds across episodes.
The chunk split treats a string, a bytes value, a mapping (a Dict-space action with the chunk axis inside each leaf), or a non-iterable scalar as a single-step chunk, matching the native split_chunk. An array’s leading axis is the chunk axis. A returned empty chunk raises rather than running an empty step.
execution_horizon only engages on a chunk corner. Requesting it on a model with no chunk corner warns and runs un-chunked, so the default of 1 is always safe. The end-to-end story (where the horizon is passed and how the rollout applies it) is in Running Evaluations.
Batched observation fusion¶
The batch corners do not receive a list of N observations. The runtime fuses the per-lane observations into one batched observation: every leaf gains a leading batch axis, so a Dict observation arrives as {key: array[N, ...]}, the shape every RL/VLA runtime hands a policy. You return the batched action the same way, and the runtime splits the batch axis back per lane.
def predict_batch(self, observations):
# observations["image"] is array[N, H, W, C]; one forward over the batch.
return self.net(observations["image"]) # return array[N, action_dim]
Rule |
Detail |
|---|---|
Return shape |
Exactly |
Text leaves |
Stay per-lane lists, not stacked arrays. An |
Ragged leaves |
Error. Lanes must share an observation space so the leaves align. |
This is the tree_stack / tree_unstack machinery in _value_conversion.py. tree_stack recurses the container and stacks aligned leaves with the framework’s stack op; tree_unstack is its inverse and splits only the batch axis, leaving each lane’s chunk axis intact.
Note
The native rlmesh.Model over raw values cannot fuse opaque tensors, so its batch corners receive the
per-lane list and return a list. Use a numpy, torch, or jax backend when you want true batched fusion.
Device and framework handling¶
For torch and jax models, self.device is the single source of truth for placement. Set it inside load() when you move your weights:
def load(self):
self.device = "cuda"
self.policy = Policy.from_pretrained("org/checkpoint").to(self.device)
Behavior |
Detail |
|---|---|
Obs placement |
RLMesh moves every framework tensor leaf of an observation onto |
Read time |
|
Default |
|
Wrong backend |
Setting |
The backend itself only changes value conversion at the predict seam. The four corners and the lifecycle are identical across native, numpy, torch, and jax. See Framework Backends for choosing one and Framework Backends for the backend helpers.
Model quirks¶
Real checkpoints rarely speak the spec’s keys and shapes directly. The seams above absorb the difference so model code stays clean.
Quirk |
Handling |
|---|---|
Compute device |
Set |
Stateful policy (RNN, chunk replay) |
Implement |
Policy expects different keys |
Write an |
Normalization stats |
Load them in |
Native chunk longer than the horizon |
Return your native chunk; truncate to |
Instruction text |
Arrives as a plain |
The _obs() remap helper is the SmolVLA pattern: the adapter delivers the payload under the spec’s keys, and a private method renames them to whatever dict the underlying policy expects.
def _obs(self, observations):
return {
"image": observations["observation.images.image"],
"wrist": observations["observation.images.wrist"],
"state": observations["observation.state"],
"task": observations["instruction"], # a list of N strings in a batch corner
}
Tokenization stays in the model on purpose. Text delivers the instruction as a string; tokenize it inside your prediction function. There is no TokenizerInput on the spec.
Construction and lifecycle¶
The four lifecycle seams fire identically on the local loop and the served path.
Seam |
Default |
When it fires |
Notes |
|---|---|---|---|
|
no-op |
once, during construction (subclass mode), before the native worker is built |
Keep heavy imports here. On the served path the eager auto-load is suppressed and |
|
no-op |
at each episode boundary |
Wired to the |
|
no-op |
at the end of a run |
Wired to the |
|
none |
constructor callbacks |
Override a subclass’s |
There is no episode-begin hook. Per-episode state is lazy-seeded on the first predict, so a stateful model clears its state at episode end.
Other construction inputs:
Attribute / argument |
Meaning |
|---|---|
|
A |
|
A |
|
Allow |
|
Classmethod returning the model’s full metadata envelope (the same envelope |
For a baseline that ignores observations, pass rlmesh.RANDOM_SAMPLE as the model to rlmesh.run / rlmesh.session. It samples the environment’s action space and resolves no adapter, even on a tagged env.
Serve and connect¶
serve(address) hosts the model as a blocking endpoint. A spec’d model resolves its adapter per env from the contract the runtime delivers in the handshake, then applies it around predict; a spec-less or NO_ADAPTER model serves its own predict directly.
MyPolicy().serve("0.0.0.0:50051")
The lazy path skips a hand-written entrypoint. Point rlmesh.serve at the recipe and it writes the serve loop:
python -m rlmesh.serve my_pkg:MyPolicy
The serve env vars match the environment serve CLI:
Env var |
Meaning |
|---|---|
|
Bind address (e.g. |
|
JSON object bound to |
|
|
|
Device for the incoming action of a torch/jax env (env-side, torch/jax only), e.g. |
Connect to a served model in one of two ways:
rlmesh.RemoteModel(address): un-managed; you started the server yourself.rlmesh.SandboxModel("image://my-model:latest"): managed; RLMesh runs the prebuilt image.docker pushthe tag to a registry it can reach first.
sess = rlmesh.session(rlmesh.SandboxModel("image://my-model:latest"), env)
Both handles plug into rlmesh.run / rlmesh.session like a local model. The full bring-your-own-container example is examples/python/byo_container; the sandbox mechanics are in Sandbox Environments, and connecting clients in Connect a Remote Environment.
Match your model’s shape¶
Find the row that matches your policy, then build it.
My policy is… |
Build it as… |
|---|---|
a one-file baseline |
|
a per-step policy with weights |
a |
a batched VLA with an action head |
a |
an ACT / diffusion / flow chunker |
a |
a checkpoint whose keys differ from the spec |
the same, plus an |
stateful across steps (RNN, ensembling) |
the same, plus |
GPU-resident |
a torch/jax |
Common pitfalls¶
Symptom |
Cause |
Fix |
|---|---|---|
|
chunk-only on the native |
define |
|
the model has no chunk corner |
implement |
Batch corner gets a list, not a fused obs |
the model is the native |
use a numpy/torch/jax backend for true fusion |
|
|
use a torch/jax |
Obs not on the GPU |
|
set |
Stateful policy leaks across episodes |
per-episode state never cleared |
implement |
Where next¶
Models: the concept tour: construction styles, the spec, the lifecycle, and the worked SmolVLA model.
Running Evaluations:
run/session/read, seeds and instruction injection, and the execution horizon end to end.Adapters and Adapter Reference: how the model spec matches an environment by role.
Environments: the environment side of the contract.
Models: the autodoc signatures for every symbol above.