| # Running evaluation β BEHAVIOR-1K PiBehavior / Ο0.5 checkpoints |
|
|
| How to take a checkpoint from this repo and score it in the BEHAVIOR-1K simulator. |
| Written for `task85_putting_dirty_dishes_in_sink`, but everything except `--task_id` |
| and the stage count applies to any checkpoint here. |
|
|
| > **Provenance of this document.** Every interface below was read directly from the |
| > solution source (`scripts/serve_b1k.py`, `src/b1k/shared/eval_b1k_wrapper.py`, |
| > `src/b1k/policies/`). The **end-to-end loop has not been executed by the author of |
| > this file** β the machine these checkpoints were trained on has no OmniGibson |
| > install and the `BEHAVIOR-1K` submodule was never checked out there. Treat the |
| > data contracts as accurate and the operational steps as unverified. Anything you |
| > find that differs from this, believe your own run. |
| |
| --- |
| |
| ## 1. Architecture of the loop |
| |
| ``` |
| ββββββββββββββββββββββββββββ websocket ββββββββββββββββββββββββββββββ |
| β OmniGibson evaluator β βββββββββββ> β serve_b1k.py β |
| β - steps the simulator β obs dict β WebsocketPolicyServer β |
| β - scores task success β β β B1KPolicyWrapper β |
| β β <βββββββββββ β β Policy (JAX) β |
| ββββββββββββββββββββββββββββ action chunk ββββββββββββββββββββββββββββββ |
| ``` |
| |
| Two processes. `serve_b1k.py` holds the model and does **nothing** on its own β it |
| waits for observations. The evaluator owns the simulator, the episode loop and the |
| success metric. |
| |
| **The policy server does not need a display.** The evaluator does (Isaac Sim), so in |
| practice they often run on different machines; the server just needs a reachable port. |
| |
| --- |
| |
| ## 2. Prerequisites |
| |
| ### Policy-server side |
| |
| ```bash |
| git clone https://github.com/IliaLarchenko/behavior-1k-solution |
| cd behavior-1k-solution |
| git submodule update --init --recursive # openpi AND BEHAVIOR-1K |
| uv sync |
| ``` |
| |
| > These checkpoints were produced by a fork with the task-embedding table widened |
| > from 50 to 100 entries. On stock upstream the parameter shapes will not match β |
| > see Β§5. |
| |
| `serve_b1k.py` imports from OmniGibson even though it only serves: |
| |
| ```python |
| from omnigibson.learning.utils.network_utils import WebsocketPolicyServer |
| from omnigibson.learning.datas import BehaviorLerobotDatasetMetadata |
| ``` |
| |
| so OmniGibson must be importable on the server host too. Checking `git submodule |
| status` is worthwhile: a leading `-` means `BEHAVIOR-1K` was never checked out, and |
| you will get `ModuleNotFoundError: No module named 'omnigibson'`. |
| |
| ### Evaluator side |
| |
| Isaac Sim + OmniGibson per the [BEHAVIOR-1K challenge instructions](https://behavior.stanford.edu/). |
| That stack is the authority on how episodes are launched and scored; this document |
| does not attempt to replace it. |
| |
| ### Download the checkpoint |
| |
| ```bash |
| huggingface-cli download JackLiu0406/b1k-checkpoints \ |
| --include "task85_putting_dirty_dishes_in_sink/29000/*" \ |
| --local-dir ./ckpts |
| ``` |
| |
| --- |
| |
| ## 3. Start the policy server |
| |
| ```bash |
| export B1K_TASK_SPACE=100 # REQUIRED β 100-task embedding table |
| export XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 |
| |
| uv run scripts/serve_b1k.py \ |
| --policy.config pi_behavior_b1k_fast \ |
| --policy.dir ./ckpts/task85_putting_dirty_dishes_in_sink/29000 \ |
| --task_id 85 \ |
| --port 8000 |
| ``` |
| |
| `--task_id` selects the task embedding. Omit it only if your evaluator puts a |
| `task_id` field in the observation, which the wrapper reads per-step and will |
| hot-swap on (`_handle_task_change`). |
| |
| `assets_base_dir` must resolve so that `norm_stats.json` and `fast_tokenizer/` are |
| found under `<assets>/IliaLarchenko/behavior_224_rgb/`. The bundled `assets/` |
| directory already has that layout. |
| |
| ### Multi-task serving |
| |
| `--task_checkpoint_mapping <file.json>` enables `CheckpointSwitcher`, which swaps |
| checkpoints per `task_id` at runtime. Useful when evaluating several single-task |
| models in one session. |
| |
| --- |
| |
| ## 4. The data contract |
| |
| ### What the evaluator sends |
| |
| Raw OmniGibson keys β the wrapper does the conversion, so send them unmodified: |
| |
| | key | content | |
| |---|---| |
| | `robot_r1::proprio` | full proprioception vector | |
| | `robot_r1::robot_r1:zed_link:Camera:0::rgb` | head camera, RGB(A) | |
| | `robot_r1::robot_r1:left_realsense_link:Camera:0::rgb` | left wrist | |
| | `robot_r1::robot_r1:right_realsense_link:Camera:0::rgb` | right wrist | |
| | `task_id` | *optional*, `int`; overrides `--task_id` per step | |
| |
| Only the first three channels of each image are used (`[..., :3]`), so RGBA is fine. |
| |
| ### What the wrapper does with it |
| |
| 1. **Images** β `resize_with_pad` to the model's square input (aspect preserved, padded). |
| 2. **State** β `extract_state_from_proprio`, which slices by `PROPRIOCEPTION_INDICES["R1Pro"]`: |
| |
| | field | dims | |
| |---|---| |
| | `base_qvel` | 3 | |
| | `trunk_qpos` | 4 | |
| | `arm_left_qpos` | 7 | |
| | `arm_right_qpos` | 7 | |
| | left gripper (both fingers summed, normalised to [-1,1]) | 1 | |
| | right gripper | 1 | |
| | **total** | **23** | |
| |
| The model's state vector is 32-wide; the remainder is padding. |
| |
| 3. **Conditioning** β the wrapper builds this itself, you do not supply it: |
| |
| ```python |
| batch["tokenized_prompt"] = np.array([task_id, current_stage], np.int32) |
| batch["tokenized_prompt_mask"] = np.array([True, True], bool) |
| batch["subtask_state"] = np.array(current_stage, np.int32) |
| ``` |
| |
| Any `prompt` string is deleted before inference. **This model is not |
| language-conditioned** β conditioning is two integer embedding lookups. |
| |
| ### What comes back |
| |
| The model emits an action chunk `[30, 32]`. The wrapper slices it to |
| **`actions[:, :23]`** β the same 23 controlled DOF as the state vector. The extra |
| dims are padding and are discarded. |
| |
| --- |
| |
| ## 5. Task and stage indexing β the thing most likely to break |
| |
| These are 2026-only activities, absent from the upstream 50-task table. The |
| checkpoints were trained with the table expanded to 100 tasks. |
| |
| **`B1K_TASK_SPACE=100` is mandatory.** A 50-task model fails on a shape mismatch at load. |
| |
| | checkpoint | `task_id` | stages | stage-embedding rows | |
| |---|---|---|---| |
| | `task85_putting_dirty_dishes_in_sink` | **85** | **14** | 962β975 | |
| | `task77_installing_a_modem` | 77 | 5 | 888β892 | |
| |
| Stage counts follow `clip(ceil(avg_episode_length / 900), 5, 15)`. The wrapper clamps |
| predictions to `TASK_NUM_STAGES[task_id] - 1`, so a wrong stage count silently caps |
| progress rather than erroring β worth checking if a policy stalls late in an episode. |
| |
| --- |
| |
| ## 6. Execution behaviour you should understand before reading results |
| |
| The wrapper is not a thin shim; it materially shapes rollout behaviour. |
| |
| ### Chunk scheduling |
| |
| | parameter | default | meaning | |
| |---|---|---| |
| | `--actions_to_execute` | 26 | actions consumed per inference | |
| | `--actions_to_keep` | 4 | overlap retained between chunks | |
| | `--execute_in_n_steps` | 20 | env steps per executed chunk | |
| | `--num_steps` | 20 | flow-matching sampling steps | |
| |
| Re-inference happens when `action_index >= execute_in_n_steps`. Because |
| `execute_in_n_steps (20) < actions_to_execute (26)`, the chunk is **compressed via |
| cubic spline interpolation** (`_interpolate_actions`) before execution. There is also |
| rolling inpainting: the tail of the previous chunk is fed back as `initial_actions`. |
| |
| ### Stage advancement by majority vote |
| |
| | parameter | default | |
| |---|---| |
| | `--history_len` | 3 | |
| | `--votes_to_promote` | 2 | |
| |
| The stage head's logits are argmaxed each step and pushed into a 3-deep history. Once |
| full, the wrapper counts votes for `current_stage + 1`; **2 of 3 promotes**. It can |
| also skip ahead (`next_stage + 1`) or fall back (`current_stage - 1`). Stage never |
| exceeds `TASK_NUM_STAGES[task_id] - 1`. |
| |
| ### Eval tricks β β οΈ on by default |
| |
| `--apply_eval_tricks` (default **`True`**) runs `apply_correction_rules(task_id, |
| stage, state, actions)`, which can rewrite actions *and* force a stage correction, |
| plus gripper-variation checks. |
|
|
| **For a clean measurement of the policy itself, set `--apply_eval_tricks False`.** |
| Leaving it on is legitimate for a leaderboard number, but it is not a measurement of |
| the network alone, and results with and without are not comparable. |
|
|
| --- |
|
|
| ## 7. Sanity check without the simulator |
|
|
| If you only want to know whether a checkpoint loads and predicts sensibly, there is a |
| forward-only gate that needs no Isaac Sim: |
|
|
| ```bash |
| B1K_CKPT=./ckpts/task85_putting_dirty_dishes_in_sink/29000 \ |
| B1K_TASK_SPACE=100 \ |
| XLA_PYTHON_CLIENT_MEM_FRACTION=0.2 \ |
| uv run scripts/eval_gate_2026.py |
| ``` |
|
|
| It measures flow-matching **action loss** on recorded demos. A low value means the |
| camera/state/task-index wiring is right; a high value means a pipeline bug. It is |
| **not** a success rate and cannot substitute for a rollout. |
|
|
| For reference, training-set losses at the end of fine-tuning: |
|
|
| | checkpoint | `action_loss` | |
| |---|---| |
| | task 85 | 0.0342 | |
| | task 77 | 0.0081 | |
|
|
| Do not compare across tasks β task 85 episodes are ~12,100 frames against task 77's |
| ~2,413. |
|
|
| --- |
|
|
| ## 8. Troubleshooting |
|
|
| | symptom | cause | |
| |---|---| |
| | `ModuleNotFoundError: omnigibson` | `BEHAVIOR-1K` submodule not checked out (`git submodule status` shows a leading `-`) | |
| | shape mismatch on `task_embeddings` | `B1K_TASK_SPACE=100` not set | |
| | `norm_stats.json` not found | `assets_base_dir` doesn't resolve to `<assets>/IliaLarchenko/behavior_224_rgb/` | |
| | policy acts randomly | wrong `--task_id`; only the trained task has a meaningful embedding | |
| | stalls near the end of an episode | stage count wrong for the task β predictions are clamped, not errored | |
| | every episode identical | `prompt` is ignored by design; conditioning is `task_id` + stage only | |
|
|
| --- |
|
|
| ## 9. Known limitations of these checkpoints |
|
|
| - **Single-task.** Only the named task was fine-tuned. Other `task_id` values are |
| either inherited from the 50-task meta checkpoint (0β49) or still at random |
| initialisation (50β99). Do not expect meaningful behaviour from them. |
| - **Inference only.** `train_state/` is not published β you cannot resume training. |
| - **Base-velocity frame.** Trained on 2026 demos *after* the upstream fix (commit |
| `e6c9756`) moving `base_qvel` from world to robot frame. A checkpoint trained on |
| the older data saw a different distribution for state dims 0:3. |
| - **Reported accuracies are training-set numbers**, not generalisation estimates. |
|
|