lovellai-dev/odyssey

Git repo for the Lovell AI Odyssey project

14

stars

181

commits

Python

primary language

Aug 19, 2026

updated

README

Odyssey

CI Python 3.10+ License: Apache 2.0 Status: Alpha

Status: alpha (v0.1.0-alpha.1). The API, CLI, schemas, and wire protocols are still subject to change without notice. See docs/ for the design refs.

Odyssey intro

odyssey.dev ↗

What it is

You train a robot with a multi-agent brain by describing a mission in YAML — an embodiment to operate, agents with models for execution, tasks with a dataset to train on, an evaluation benchmark to score against — and odyssey run walks it through the full lifecycle: load → validate → execute training tasks → execute the evaluation task → persist results. Local-mode by default; the hosted Lovell AI services (leaderboard, learning graph, hosted runners) are optional layers that land in later releases.

Install

[!TIP] Linux only — install build dependencies before proceeding (needed by .[all]):

sudo apt update && sudo apt install build-essential python3-dev -y
git clone https://github.com/lovellai-dev/odyssey.git
cd odyssey
python3 -m venv .venv
source .venv/bin/activate
pip install -e .              # CLI, validate, mock runs (lightweight)
pip install -e ".[all]"       # real training + evaluation (torch, robosuite…)
pip install -e ".[all,dev]"   # + pytest, ruff, mypy

The base install pulls in pydantic, click, pyyaml, and aiosqlite — enough to run validate, list, status, and run --use-mock-runner against any mission spec without a GPU. .[all] adds everything needed for real training and evaluation runs.

Quick start (no GPU, no network)

# Validate the mission spec
$ odyssey validate examples/quickstart-openvla/mission.yaml
OK  examples/quickstart-openvla/mission.yaml
  spec version : 0.1
  tasks        : 1 training, 1 evaluation

# Run the full mission with a CPU mock (no GPU needed)
$ odyssey run examples/quickstart-openvla/mission.yaml --use-mock-runner
...
{"ts": "...", "event": "mission.completed", "overall_grade": 1.0}

COMPLETED  c1756bad855e45cc9a95b5b0566c948b
  overall_grade : 1.000

--use-mock-runner swaps in the CPU mock for every task, so this works on a laptop without a GPU. Inspect runs afterward with odyssey list and odyssey status <id>. State is persisted to ~/.odyssey/missions.db; artifacts under ~/.odyssey/runs/<mission-id>/<task-id>/.

Launching a training mission

Two training paths ship today: GR00T (NVIDIA Isaac GR00T) and OpenVLA. Both run through odyssey run <mission.yaml> — pick the quickstart that matches the agent's model.

GR00T (Isaac-GR00T + Isaac Lab)

Fine-tunes nvidia/GR00T-N1.7-3B on the LeRobot-format demo set that ships inside the Isaac-GR00T repo (no separate download), evaluated in the Isaac Lab cube-lift environment.

Prerequisites:

Run bash examples/quickstart-gr00t/setup.sh — it builds the three eval venvs (Odyssey core, GR00T server, Isaac Lab client transport) with uv. See examples/quickstart-gr00t/README.md for the full walkthrough and why they're separate venvs by design.

Isaac Sim + Isaac Lab are a separate, heavy precondition — a large NVIDIA Omniverse binary installed by NVIDIA's own tooling, not by setup.sh (the script wires the eval client into it, and skips with a note if it's absent). Provision them once following the quickstart-gr00t README, then point Odyssey at your checkouts:

export ISAAC_GR00T_REPO_PATH=/srv/Isaac-GR00T   # GR00T checkout + demo dataset
export ISAACLAB_PATH=$HOME/IsaacLab             # provides isaaclab.sh

Run:

odyssey run examples/quickstart-gr00t/mission.yaml

The mission routes its training task to the GR00T runner with config: { runner: gr00t } — OpenVLA and GR00T both serve wildcard training tasks, so the family is selected explicitly.

OpenVLA (Bridge V2 + Robosuite)

Prerequisites:

  1. Install the training extras:
    pip install -e ".[huggingface,openvla,robosuite]"
    
  2. Clone the upstream OpenVLA repo and install its dependencies (needed for draccus and the fine-tuning script):
    git clone https://github.com/openvla/openvla.git /srv/openvla
    pip install -e /srv/openvla
    export OPENVLA_REPO_PATH=/srv/openvla
    
  3. Download the Bridge V2 dataset in RLDS format (~124 GB):
    wget -r -nH --cut-dirs=4 --reject="index.html*" \
      https://rail.eecs.berkeley.edu/datasets/bridge_release/data/tfds/bridge_dataset/
    mv bridge_dataset bridge_orig
    
    Set --data_root_dir to the parent directory containing bridge_orig/.

Run:

odyssey run examples/quickstart-openvla/mission.yaml

Hardware: 24 GB GPU (RTX 4090-class or better) for the OpenVLA LoRA fine-tune.

[!NOTE] GCP users: single-GPU VMs require export NCCL_NET=Socket before running, to bypass Google's NCCL plugin. See issue #5 for details.

[!NOTE] Evaluation: the Robosuite runner auto-wires an OpenVLA→robosuite-action adapter (make_openvla_policy in runners/models/openvla.py) when no custom policy_factory is injected — it loads either a LoRA adapter or a full merged checkpoint, so eval works without extra glue. Full episode-completion validation on a real GPU is still in progress.

Known-good OpenVLA stack

The fine-tune runs through the cloned OpenVLA repo, which carries its own dependency set — most onboarding friction comes from there, not from Odyssey. Mixing versions surfaces as protobuf / TensorFlow / tensorflow-metadata conflicts or draccus import errors. Known-good versions (from OpenVLA's own requirements — treat its repo as the source of truth):

Python        3.10
torch         2.2.0
torchvision   0.17.0
transformers  4.40.1
tokenizers    0.19.1
timm          0.9.10
flash-attn    2.5.5

To avoid re-downloading the 7B base model each run, point its path env var at a local copy (HF id upper-cased, / and -_, suffixed _PATH):

export OPENVLA_OPENVLA_7B_PATH=/path/to/openvla-7b   # for base: openvla/openvla-7b

Dataset: how source: oxe / ref: bridge_orig resolves

Odyssey does not download the datasetoxe is a pass-through. The runner forwards two values to OpenVLA's finetune.py, which loads via TFDS/RLDS:

mission.yamlbecomes the flagmeaning
dataset.ref: bridge_orig--dataset_name bridge_origthe OXE registry key OpenVLA looks up
config.data_root_dir: <path>--data_root_dir <path>the parent dir containing the RLDS dataset folder

⚠️ Naming gotcha: the registry key and the on-disk folder name can differ. In validation, ref: bridge_orig resolved to data under ~/bridge_dataset/1.0.0/, so data_root_dir had to point at the parent of that folder — not the key name. Check where your download actually landed and set data_root_dir to its parent.

Weights & Biases (W&B)

OpenVLA's finetune.py calls wandb.init() unconditionally, so a run stalls or fails if W&B isn't reachable. Control it yourself:

# Disable for local / smoke runs:
export WANDB_MODE=disabled
# Or log to your account, then pass project/entity via mission config:
#   config: { wandb_project: my-project, wandb_entity: my-entity }

Any config: key Odyssey doesn't consume is forwarded verbatim as --<key> <value> to finetune.py.

What to expect during a run

Timing varies widely with hardware, disk, and network — treat these as orientation, not promises:

  1. Base model downloadopenvla-7b (~14 GB) on first run, unless OPENVLA_OPENVLA_7B_PATH is set.
  2. Dataset load / indexing — Bridge V2 (~124 GB); RLDS indexing on a cold cache takes a while.
  3. Training startup — model load + LoRA wrap, then steps begin.
  4. Steady state — throughput logs as it/s (~1.49 it/s on an NVIDIA L4 for the quickstart config).

If a stage seems stuck, it's almost always a download in progress or a dataset-path / W&B issue rather than a training bug — check those first.

Multi-agent (PILOT + SPECIALIST)

Show setup & how it works

HuggingFace login (gated models)

The models pulled from the Hub are gated — you must accept each model's license on its HuggingFace page, then authenticate on the machine before the first run, or the download fails with 401/403:

huggingface-cli login          # paste a token from https://huggingface.co/settings/tokens
# or, non-interactive (CI / headless VM):
export HF_TOKEN=hf_xxx          # a read token on an account that accepted the licenses

A mission with a SPECIALIST agent (a task planner) in addition to the PILOT runs a plan-then-execute loop during eval: the SPECIALIST decomposes the instruction into sub-steps once per episode, and the PILOT executes each. Only the PILOT produces actions and only the PILOT is trained — the SPECIALIST is inference-only (it runs its base checkpoint to plan and has no training task).

robot:
  agents:
    - id: pilot
      role: PILOT
      model: { source: huggingface, base: openvla/openvla-7b }
    - id: task-planner
      role: SPECIALIST
      model:
        source: huggingface
        base: google/gemma-4-E2B-it
        quantization: int4
        modality: multimodal

The SPECIALIST is a vision-grounded multimodal Gemma 4 planner: it sees the first camera frame of each episode and grounds its plan in the scene. Gemma 4 needs a modern transformers + torchvision, which conflicts with OpenVLA's pinned transformers==4.40.1, so the SPECIALIST must run out of process in a separate venv. The PILOT stays in the main venv; the two talk over a JSON-lines subprocess protocol (the planner runs once per episode, off the per-step hot loop).

Setting up the two environments (setup.sh)

Multi-agent needs two venvsenv_pilot (the OpenVLA pilot + Robosuite eval) and env_specialist (the Gemma 4 planner) — because their transformers versions are mutually incompatible. The setup.sh script builds both idempotently, each pinned to its known-good stack, clones the upstream OpenVLA repo, and writes a sourceable .env:

# from the repo root — builds env_pilot + env_specialist, clones OpenVLA, writes .env
examples/multiagent-openvla-gemma/setup.sh

# load the active venv + env vars (incl. ODYSSEY_SPECIALIST_PYTHON) before each run
source examples/multiagent-openvla-gemma/.env

📖 Full GCP walkthrough (provisioning, dataset download, troubleshooting): the multi-agent GCP tutorial. For the simpler single-agent (OpenVLA-only) path, see the training tutorial.

Build the two venvs by hand instead

The role-based names keep the two transformers stacks straight: env_pilot holds OpenVLA (pinned transformers==4.40.1), env_specialist the Gemma planner (modern transformers + torchvision).

# env_specialist — the Gemma planner (env_pilot is the OpenVLA install)
python -m venv env_specialist
env_specialist/bin/pip install -e ".[specialist]" \
  -c constraints/specialist-known-good.txt

# point Odyssey at that venv's python (read per-process; export it in every shell
# that runs a mission, or persist it in your shell profile / VM startup script)
export ODYSSEY_SPECIALIST_PYTHON="$PWD/env_specialist/bin/python"

ODYSSEY_SPECIALIST_PYTHON is required for any mission with a SPECIALIST. The planner is launched in that venv (RemotePlannerpython -m odyssey.runners.agents.planner_server). If it is unset, multi-agent eval fails fast with a clear RuntimeError: the multimodal Gemma 4 planner cannot load in the main venv, which pins transformers==4.40.1 for OpenVLA.

Quick check without a simulator (launches the planner in the specialist venv and prints a decomposition — no OpenVLA or simulator needed):

python tests/manual/smoke_remote_planner.py

Why Gemma 4, not Gemma 3, for multimodal. Gemma 3 4B emits NaN logits under int4 bitsandbytes on this stack (verified across eager/sdpa attention, text-only and with-image), so it can't run quantized here. Gemma 4 (Apache-2.0, ungated) loads cleanly in int4 and grounds plans in the scene image.

VRAM note. Both models still share the GPU — the venv split solves the dependency conflict, not VRAM. The SPECIALIST is pinned to GPU 0 (device_map={"": 0}) so bitsandbytes never silently offloads layers to CPU. Gemma 4 E4B-it int4 (~9.3 GB) alongside bf16 OpenVLA (~14 GB) peaks at ~23 GB — tight on a 24 GB L4; drop to E2B-it for headroom (this is what the multimodal example mission uses).

Two known-good stacks. The main venv pins OpenVLA's stack (constraints/openvla-known-good.txt: torch 2.2.0, transformers 4.40.1); the specialist venv pins a modern one with torchvision (constraints/specialist-known-good.txt). They no longer need to be mutually compatible.

CLI reference

CommandWhat it does
odyssey init [DIR]Scaffold a new mission directory. --template openvla|cpu_mock.
odyssey validate <mission.yaml>Parse + validate a spec. Exits 0 if clean.
odyssey run <mission.yaml>Execute end-to-end. --use-mock-runner for no-GPU smoke.
odyssey listRecent missions from the local SQLite DB. --status to filter.
odyssey status <mission_id>One mission's detail. Accepts an id prefix.

All commands respect --db and --working-dir to override the ~/.odyssey/ defaults.

Status snapshot (v0.1.0-alpha.1)

AreaDoneDeferred
Spec + validate
Engine + lifecyclewatchdog timers, materialized profiles
In-memory + SQLite persistence
Provider ABCs + Local + HFOXE, Lovell-mode
CPU mock runner
OpenVLA training runner✓ (validated on L4)
GR00T training runner✓ (validated on H100); task-level runner: gr00t routing
Robosuite eval runner✓ (auto-wired OpenVLA adapter)full GPU end-to-end validation
Isaac Lab eval runner✓ skeleton + tests, subprocess launch + ODYSSEY_* stdout protocolblessed eval script (GR00T/VLA recipe), real-Isaac smoke
Multi-agent eval (PILOT + SPECIALIST)✓ (out-of-process Gemma 4 planner)full GPU end-to-end validation
odyssey init / run / list / status / validatelogs, publish
Leaderboard publish, Learning Graph, Anonymizer, Authpost-v0.1.0-alpha.1

Recording rollout videos

Add capture_video: true to any Robosuite eval task's config to save an MP4 of each rollout (the example missions already enable it):

  - name: eval-on-robosuite-lift
    kind: evaluation
    evaluation_type: robosuite
    benchmark_name: Lift
    num_episodes: 10
    config:
      unnorm_key: bridge_orig
      capture_video: true        # one MP4 per episode

Clips land in the task's output dir, one per episode:

~/.odyssey/runs/<mission_id>/<task_id>/videos/episode_01_PASS.mp4

(~/.odyssey/runs is the default; override with odyssey run --working-dir.) The paths are also surfaced in the eval's result_summary.artifacts.videos and in a videos_saved progress event.

It's near-free: the camera-enabled eval env already renders the frame each step for the policy, so capture only adds a list append per step plus one encode per episode (offloaded to a thread). Optional knobs: video_fps (default 24), video_format (default mp4).

Choosing the video camera

By default the clip records the policy's own input frame (agentview at 256×256) — a view framed for the model, not for humans, so the robot may sit partially out of frame. To record from a dedicated, better-framed camera without changing what the policy sees, set video_camera:

    config:
      capture_video: true
      video_camera: frontview    # any robosuite camera, e.g. frontview, sideview
      video_height: 512          # optional, default 512
      video_width: 512           # optional, default 512

The env then renders both cameras each step — the policy keeps consuming its own camera and the MP4 records the other — at the cost of one extra render stream per step. When video_camera is unset, behavior is exactly the near-free default above. Setting it to the policy's own camera raises an error: unset it instead to record the policy view.

Dependency. MP4 encoding needs imageio[ffmpeg], which ships with the [robosuite] (and [all]) extra — no extra install step if you already run eval. It's best-effort: if the encoder is missing it logs a warning and the eval still completes, just without a video. On a headless VM the same MUJOCO_GL=egl / PYOPENGL_PLATFORM=egl the eval needs also drives the offscreen render used for capture.

License

Apache License 2.0. See LICENSE.

Contributing

See CONTRIBUTING.md. DCO sign-off required on every commit. Open an issue before non-trivial PRs — the API surface is moving weekly until v0.1.0-alpha.1 freezes.

Contributors

SoyGema

147 commits

femtechie

18 commits

Danny024

9 commits

lovellai-dev/odyssey

Git repo for the Lovell AI Odyssey project

14

stars

181

commits

Python

primary language

Aug 19, 2026

updated

README

Odyssey

CI Python 3.10+ License: Apache 2.0 Status: Alpha

Status: alpha (v0.1.0-alpha.1). The API, CLI, schemas, and wire protocols are still subject to change without notice. See docs/ for the design refs.

Odyssey intro

odyssey.dev ↗

What it is

You train a robot with a multi-agent brain by describing a mission in YAML — an embodiment to operate, agents with models for execution, tasks with a dataset to train on, an evaluation benchmark to score against — and odyssey run walks it through the full lifecycle: load → validate → execute training tasks → execute the evaluation task → persist results. Local-mode by default; the hosted Lovell AI services (leaderboard, learning graph, hosted runners) are optional layers that land in later releases.

Install

[!TIP] Linux only — install build dependencies before proceeding (needed by .[all]):

sudo apt update && sudo apt install build-essential python3-dev -y
git clone https://github.com/lovellai-dev/odyssey.git
cd odyssey
python3 -m venv .venv
source .venv/bin/activate
pip install -e .              # CLI, validate, mock runs (lightweight)
pip install -e ".[all]"       # real training + evaluation (torch, robosuite…)
pip install -e ".[all,dev]"   # + pytest, ruff, mypy

The base install pulls in pydantic, click, pyyaml, and aiosqlite — enough to run validate, list, status, and run --use-mock-runner against any mission spec without a GPU. .[all] adds everything needed for real training and evaluation runs.

Quick start (no GPU, no network)

# Validate the mission spec
$ odyssey validate examples/quickstart-openvla/mission.yaml
OK  examples/quickstart-openvla/mission.yaml
  spec version : 0.1
  tasks        : 1 training, 1 evaluation

# Run the full mission with a CPU mock (no GPU needed)
$ odyssey run examples/quickstart-openvla/mission.yaml --use-mock-runner
...
{"ts": "...", "event": "mission.completed", "overall_grade": 1.0}

COMPLETED  c1756bad855e45cc9a95b5b0566c948b
  overall_grade : 1.000

--use-mock-runner swaps in the CPU mock for every task, so this works on a laptop without a GPU. Inspect runs afterward with odyssey list and odyssey status <id>. State is persisted to ~/.odyssey/missions.db; artifacts under ~/.odyssey/runs/<mission-id>/<task-id>/.

Launching a training mission

Two training paths ship today: GR00T (NVIDIA Isaac GR00T) and OpenVLA. Both run through odyssey run <mission.yaml> — pick the quickstart that matches the agent's model.

GR00T (Isaac-GR00T + Isaac Lab)

Fine-tunes nvidia/GR00T-N1.7-3B on the LeRobot-format demo set that ships inside the Isaac-GR00T repo (no separate download), evaluated in the Isaac Lab cube-lift environment.

Prerequisites:

Run bash examples/quickstart-gr00t/setup.sh — it builds the three eval venvs (Odyssey core, GR00T server, Isaac Lab client transport) with uv. See examples/quickstart-gr00t/README.md for the full walkthrough and why they're separate venvs by design.

Isaac Sim + Isaac Lab are a separate, heavy precondition — a large NVIDIA Omniverse binary installed by NVIDIA's own tooling, not by setup.sh (the script wires the eval client into it, and skips with a note if it's absent). Provision them once following the quickstart-gr00t README, then point Odyssey at your checkouts:

export ISAAC_GR00T_REPO_PATH=/srv/Isaac-GR00T   # GR00T checkout + demo dataset
export ISAACLAB_PATH=$HOME/IsaacLab             # provides isaaclab.sh

Run:

odyssey run examples/quickstart-gr00t/mission.yaml

The mission routes its training task to the GR00T runner with config: { runner: gr00t } — OpenVLA and GR00T both serve wildcard training tasks, so the family is selected explicitly.

OpenVLA (Bridge V2 + Robosuite)

Prerequisites:

  1. Install the training extras:
    pip install -e ".[huggingface,openvla,robosuite]"
    
  2. Clone the upstream OpenVLA repo and install its dependencies (needed for draccus and the fine-tuning script):
    git clone https://github.com/openvla/openvla.git /srv/openvla
    pip install -e /srv/openvla
    export OPENVLA_REPO_PATH=/srv/openvla
    
  3. Download the Bridge V2 dataset in RLDS format (~124 GB):
    wget -r -nH --cut-dirs=4 --reject="index.html*" \
      https://rail.eecs.berkeley.edu/datasets/bridge_release/data/tfds/bridge_dataset/
    mv bridge_dataset bridge_orig
    
    Set --data_root_dir to the parent directory containing bridge_orig/.

Run:

odyssey run examples/quickstart-openvla/mission.yaml

Hardware: 24 GB GPU (RTX 4090-class or better) for the OpenVLA LoRA fine-tune.

[!NOTE] GCP users: single-GPU VMs require export NCCL_NET=Socket before running, to bypass Google's NCCL plugin. See issue #5 for details.

[!NOTE] Evaluation: the Robosuite runner auto-wires an OpenVLA→robosuite-action adapter (make_openvla_policy in runners/models/openvla.py) when no custom policy_factory is injected — it loads either a LoRA adapter or a full merged checkpoint, so eval works without extra glue. Full episode-completion validation on a real GPU is still in progress.

Known-good OpenVLA stack

The fine-tune runs through the cloned OpenVLA repo, which carries its own dependency set — most onboarding friction comes from there, not from Odyssey. Mixing versions surfaces as protobuf / TensorFlow / tensorflow-metadata conflicts or draccus import errors. Known-good versions (from OpenVLA's own requirements — treat its repo as the source of truth):

Python        3.10
torch         2.2.0
torchvision   0.17.0
transformers  4.40.1
tokenizers    0.19.1
timm          0.9.10
flash-attn    2.5.5

To avoid re-downloading the 7B base model each run, point its path env var at a local copy (HF id upper-cased, / and -_, suffixed _PATH):

export OPENVLA_OPENVLA_7B_PATH=/path/to/openvla-7b   # for base: openvla/openvla-7b

Dataset: how source: oxe / ref: bridge_orig resolves

Odyssey does not download the datasetoxe is a pass-through. The runner forwards two values to OpenVLA's finetune.py, which loads via TFDS/RLDS:

mission.yamlbecomes the flagmeaning
dataset.ref: bridge_orig--dataset_name bridge_origthe OXE registry key OpenVLA looks up
config.data_root_dir: <path>--data_root_dir <path>the parent dir containing the RLDS dataset folder

⚠️ Naming gotcha: the registry key and the on-disk folder name can differ. In validation, ref: bridge_orig resolved to data under ~/bridge_dataset/1.0.0/, so data_root_dir had to point at the parent of that folder — not the key name. Check where your download actually landed and set data_root_dir to its parent.

Weights & Biases (W&B)

OpenVLA's finetune.py calls wandb.init() unconditionally, so a run stalls or fails if W&B isn't reachable. Control it yourself:

# Disable for local / smoke runs:
export WANDB_MODE=disabled
# Or log to your account, then pass project/entity via mission config:
#   config: { wandb_project: my-project, wandb_entity: my-entity }

Any config: key Odyssey doesn't consume is forwarded verbatim as --<key> <value> to finetune.py.

What to expect during a run

Timing varies widely with hardware, disk, and network — treat these as orientation, not promises:

  1. Base model downloadopenvla-7b (~14 GB) on first run, unless OPENVLA_OPENVLA_7B_PATH is set.
  2. Dataset load / indexing — Bridge V2 (~124 GB); RLDS indexing on a cold cache takes a while.
  3. Training startup — model load + LoRA wrap, then steps begin.
  4. Steady state — throughput logs as it/s (~1.49 it/s on an NVIDIA L4 for the quickstart config).

If a stage seems stuck, it's almost always a download in progress or a dataset-path / W&B issue rather than a training bug — check those first.

Multi-agent (PILOT + SPECIALIST)

Show setup & how it works

HuggingFace login (gated models)

The models pulled from the Hub are gated — you must accept each model's license on its HuggingFace page, then authenticate on the machine before the first run, or the download fails with 401/403:

huggingface-cli login          # paste a token from https://huggingface.co/settings/tokens
# or, non-interactive (CI / headless VM):
export HF_TOKEN=hf_xxx          # a read token on an account that accepted the licenses

A mission with a SPECIALIST agent (a task planner) in addition to the PILOT runs a plan-then-execute loop during eval: the SPECIALIST decomposes the instruction into sub-steps once per episode, and the PILOT executes each. Only the PILOT produces actions and only the PILOT is trained — the SPECIALIST is inference-only (it runs its base checkpoint to plan and has no training task).

robot:
  agents:
    - id: pilot
      role: PILOT
      model: { source: huggingface, base: openvla/openvla-7b }
    - id: task-planner
      role: SPECIALIST
      model:
        source: huggingface
        base: google/gemma-4-E2B-it
        quantization: int4
        modality: multimodal

The SPECIALIST is a vision-grounded multimodal Gemma 4 planner: it sees the first camera frame of each episode and grounds its plan in the scene. Gemma 4 needs a modern transformers + torchvision, which conflicts with OpenVLA's pinned transformers==4.40.1, so the SPECIALIST must run out of process in a separate venv. The PILOT stays in the main venv; the two talk over a JSON-lines subprocess protocol (the planner runs once per episode, off the per-step hot loop).

Setting up the two environments (setup.sh)

Multi-agent needs two venvsenv_pilot (the OpenVLA pilot + Robosuite eval) and env_specialist (the Gemma 4 planner) — because their transformers versions are mutually incompatible. The setup.sh script builds both idempotently, each pinned to its known-good stack, clones the upstream OpenVLA repo, and writes a sourceable .env:

# from the repo root — builds env_pilot + env_specialist, clones OpenVLA, writes .env
examples/multiagent-openvla-gemma/setup.sh

# load the active venv + env vars (incl. ODYSSEY_SPECIALIST_PYTHON) before each run
source examples/multiagent-openvla-gemma/.env

📖 Full GCP walkthrough (provisioning, dataset download, troubleshooting): the multi-agent GCP tutorial. For the simpler single-agent (OpenVLA-only) path, see the training tutorial.

Build the two venvs by hand instead

The role-based names keep the two transformers stacks straight: env_pilot holds OpenVLA (pinned transformers==4.40.1), env_specialist the Gemma planner (modern transformers + torchvision).

# env_specialist — the Gemma planner (env_pilot is the OpenVLA install)
python -m venv env_specialist
env_specialist/bin/pip install -e ".[specialist]" \
  -c constraints/specialist-known-good.txt

# point Odyssey at that venv's python (read per-process; export it in every shell
# that runs a mission, or persist it in your shell profile / VM startup script)
export ODYSSEY_SPECIALIST_PYTHON="$PWD/env_specialist/bin/python"

ODYSSEY_SPECIALIST_PYTHON is required for any mission with a SPECIALIST. The planner is launched in that venv (RemotePlannerpython -m odyssey.runners.agents.planner_server). If it is unset, multi-agent eval fails fast with a clear RuntimeError: the multimodal Gemma 4 planner cannot load in the main venv, which pins transformers==4.40.1 for OpenVLA.

Quick check without a simulator (launches the planner in the specialist venv and prints a decomposition — no OpenVLA or simulator needed):

python tests/manual/smoke_remote_planner.py

Why Gemma 4, not Gemma 3, for multimodal. Gemma 3 4B emits NaN logits under int4 bitsandbytes on this stack (verified across eager/sdpa attention, text-only and with-image), so it can't run quantized here. Gemma 4 (Apache-2.0, ungated) loads cleanly in int4 and grounds plans in the scene image.

VRAM note. Both models still share the GPU — the venv split solves the dependency conflict, not VRAM. The SPECIALIST is pinned to GPU 0 (device_map={"": 0}) so bitsandbytes never silently offloads layers to CPU. Gemma 4 E4B-it int4 (~9.3 GB) alongside bf16 OpenVLA (~14 GB) peaks at ~23 GB — tight on a 24 GB L4; drop to E2B-it for headroom (this is what the multimodal example mission uses).

Two known-good stacks. The main venv pins OpenVLA's stack (constraints/openvla-known-good.txt: torch 2.2.0, transformers 4.40.1); the specialist venv pins a modern one with torchvision (constraints/specialist-known-good.txt). They no longer need to be mutually compatible.

CLI reference

CommandWhat it does
odyssey init [DIR]Scaffold a new mission directory. --template openvla|cpu_mock.
odyssey validate <mission.yaml>Parse + validate a spec. Exits 0 if clean.
odyssey run <mission.yaml>Execute end-to-end. --use-mock-runner for no-GPU smoke.
odyssey listRecent missions from the local SQLite DB. --status to filter.
odyssey status <mission_id>One mission's detail. Accepts an id prefix.

All commands respect --db and --working-dir to override the ~/.odyssey/ defaults.

Status snapshot (v0.1.0-alpha.1)

AreaDoneDeferred
Spec + validate
Engine + lifecyclewatchdog timers, materialized profiles
In-memory + SQLite persistence
Provider ABCs + Local + HFOXE, Lovell-mode
CPU mock runner
OpenVLA training runner✓ (validated on L4)
GR00T training runner✓ (validated on H100); task-level runner: gr00t routing
Robosuite eval runner✓ (auto-wired OpenVLA adapter)full GPU end-to-end validation
Isaac Lab eval runner✓ skeleton + tests, subprocess launch + ODYSSEY_* stdout protocolblessed eval script (GR00T/VLA recipe), real-Isaac smoke
Multi-agent eval (PILOT + SPECIALIST)✓ (out-of-process Gemma 4 planner)full GPU end-to-end validation
odyssey init / run / list / status / validatelogs, publish
Leaderboard publish, Learning Graph, Anonymizer, Authpost-v0.1.0-alpha.1

Recording rollout videos

Add capture_video: true to any Robosuite eval task's config to save an MP4 of each rollout (the example missions already enable it):

  - name: eval-on-robosuite-lift
    kind: evaluation
    evaluation_type: robosuite
    benchmark_name: Lift
    num_episodes: 10
    config:
      unnorm_key: bridge_orig
      capture_video: true        # one MP4 per episode

Clips land in the task's output dir, one per episode:

~/.odyssey/runs/<mission_id>/<task_id>/videos/episode_01_PASS.mp4

(~/.odyssey/runs is the default; override with odyssey run --working-dir.) The paths are also surfaced in the eval's result_summary.artifacts.videos and in a videos_saved progress event.

It's near-free: the camera-enabled eval env already renders the frame each step for the policy, so capture only adds a list append per step plus one encode per episode (offloaded to a thread). Optional knobs: video_fps (default 24), video_format (default mp4).

Choosing the video camera

By default the clip records the policy's own input frame (agentview at 256×256) — a view framed for the model, not for humans, so the robot may sit partially out of frame. To record from a dedicated, better-framed camera without changing what the policy sees, set video_camera:

    config:
      capture_video: true
      video_camera: frontview    # any robosuite camera, e.g. frontview, sideview
      video_height: 512          # optional, default 512
      video_width: 512           # optional, default 512

The env then renders both cameras each step — the policy keeps consuming its own camera and the MP4 records the other — at the cost of one extra render stream per step. When video_camera is unset, behavior is exactly the near-free default above. Setting it to the policy's own camera raises an error: unset it instead to record the policy view.

Dependency. MP4 encoding needs imageio[ffmpeg], which ships with the [robosuite] (and [all]) extra — no extra install step if you already run eval. It's best-effort: if the encoder is missing it logs a warning and the eval still completes, just without a video. On a headless VM the same MUJOCO_GL=egl / PYOPENGL_PLATFORM=egl the eval needs also drives the offscreen render used for capture.

License

Apache License 2.0. See LICENSE.

Contributing

See CONTRIBUTING.md. DCO sign-off required on every commit. Open an issue before non-trivial PRs — the API surface is moving weekly until v0.1.0-alpha.1 freezes.

Contributors

SoyGema

147 commits

femtechie

18 commits

Danny024

9 commits

Languages

Python

100.0%