Skip to content

CLI Internals

The eden command is an argparse tree declared in one table. eden/__main__.py holds COMMANDS, one Command per subcommand, and imports each handler from its area module under eden/utils/cli/; eden.utils.cli.registry holds the Command shape and the build and dispatch helpers, and knows nothing about which commands exist. To add a subcommand, write its handler in the area module it belongs to and add a Command entry to COMMANDS; the module docstrings of eden/__main__.py and eden/utils/cli/registry.py describe the contract. For what each command does, see the CLI reference.

Command table

Eden command-line interface entry point.

Declares the COMMANDS table — one :class:Command per subcommand, wiring each to a handler in the area modules under :mod:eden.utils.cli (grouped by area: tasks, terms, examples, viewers, devices, dataset, pose, retarget, teleop, maintenance) — then builds the argparse tree and dispatches over it via the primitives in :mod:eden.utils.cli.registry.

This is a curated in-tree table, not an :class:eden.utils.registry.Registry: commands are instances, the set is closed, and there's no plugin discovery.

To add a subcommand: write the handler in its area module, then add one :class:~eden.utils.cli.registry.Command entry to COMMANDS below — that class documents the three shapes an entry can take.

Attributes

COMMANDS module-attribute

COMMANDS: tuple[Command, ...] = (Command('list', 'List all available tasks', handler=_run_list), Command('terms', "List every registered term and the parameters it accepts (e.g. 'eden terms --kind reward', 'eden terms --json')", handler=_run_terms, configure=_configure_terms), Command('examples', 'Browse the runnable examples catalog under examples/', subcommands=(Command('list', "List/filter example scripts by tier, capability, or hardware (e.g. 'eden examples list --tier recipe --capability sensor')", handler=_run_examples_list, configure=_configure_examples_list), Command('smoke', 'Smoke-run one example headless in a few steps (EDEN_SMOKE); refuses examples needing hardware/devices/missing deps', handler=_run_examples_smoke, configure=_configure_examples_smoke))), Command('train', "Train a registered task. Use 'eden train --task <name> --help' for task options.", handler=train_task, forwards_extra=True), Command('inference', "Run inference for a registered task with a trained checkpoint, or roll out a frozen extension policy in sim (--policy). Add --async for a deployment-style, wall-clock-paced rollout (a sim-side dry run of 'eden deploy').", handler=inference_task, forwards_extra=True), Command('export', "Export a trained checkpoint's policy as ONNX / TorchScript plus a policy_spec.json (observations, actions, control rate) for a runtime outside Eden.", handler=export_task, forwards_extra=True), Command('deploy', "Deploy a trained task policy on real hardware (robot picked from the task's scene), or against a MuJoCo model of that robot for sim-to-sim validation (--mujoco).", handler=deploy_task, forwards_extra=True), Command('preprocess', 'Build the v2 dataset cache for a registered task. Supports --num_batches/--batch_idx for multi-GPU sharding.', handler=preprocess_task, forwards_extra=True), Command('smoke', "Build a registered task and step it a few times with no action — a quick end-to-end sanity check. Use 'eden smoke --task <name> --help' for options.", handler=smoke_task, forwards_extra=True), Command('datagen', "Generate scripted, demo-like demonstrations of a task into an HDF5 dataset (OCBench-style demonstrators). Use 'eden datagen --task <name> --help' for options.", handler=_run_datagen, forwards_extra=True), Command('gello', 'Gello commands', subcommands=(Command('calibrate', "Calibrate the Gello leader arm (pass --help after 'calibrate' for options)", handler=_run_gello_calibrate, forwards_extra=True), Command('leader', "Read the Gello leader arm and send the joint states to a GelloZMQServer (pass --help after 'leader' for options)", handler=_run_gello_leader, forwards_extra=True))), Command('lerobot', 'LeRobot dataset commands', subcommands=(Command('convert', "Convert an Eden HDF5 recording into a LeRobot v3 dataset (pass --help after 'convert' for options)", handler=_run_lerobot_convert, forwards_extra=True),)), Command('metaquest', 'Meta Quest WebXR teleoperation commands', subcommands=(Command('bridge', "Run the televuer XR bridge (requires the 'metaquest' extra; pass --help after 'bridge' for options)", handler=_run_metaquest_bridge, forwards_extra=True),)), Command('pico', 'PICO (XRoboToolkit) teleoperation commands', subcommands=(Command('bridge', "Run the PICO XR bridge — publishes headset/controller tracking and streams the stereo view back to the headset (requires the 'pico' extra; pass --help after 'bridge' for options)", handler=_run_pico_bridge, forwards_extra=True),)), Command('teleop', 'Teleoperation pipelines (operator station + robot process)', subcommands=(Command('sonic', 'GEAR-SONIC whole-body teleoperation (PICO)', subcommands=(Command('robot', "Run the robot process: Genesis G1 body + SONIC policy + planner; listens for the operator's ZMQ commands (pass --help after 'robot' for options)", handler=_run_sonic_robot, forwards_extra=True), Command('operator', "Run the operator station: PICO body tracking, mode state machine, calibration, and streaming to the robot process (pass --help after 'operator' for options)", handler=_run_sonic_operator, forwards_extra=True))),)), Command('pose', 'Human pose / motion estimation (video → SMPL motion)', subcommands=(Command('gvhmr', "World-grounded SMPL-X motion from monocular video via GVHMR (pass --help after 'gvhmr' for options)", handler=_run_gvhmr, forwards_extra=True),)), Command('retarget', 'Motion retargeting (human → robot)', subcommands=(Command('omniretarget', "Robot-only OMOMO → G1 retargeting via OmniRetarget (pass --help after 'omniretarget' for options)", handler=_run_omniretarget, forwards_extra=True), Command('otretarget', "Joint robot + object retargeting from surface interactions via OTRetarget (OMOMO hands/body/object, BONES-SEED BVH; pass --help after 'otretarget' for options)", handler=_run_otretarget, forwards_extra=True), Command('soma', "Batch SMPL/SMPL-H/SMPL-X → robot retargeting via SomaRetargeter (pass --help after 'soma' for options)", handler=_run_soma, forwards_extra=True), Command('dex', "Offline MANO-keypoint → dexterous-hand position retargeting via DexRetargeter (pass --help after 'dex' for options)", handler=_run_dex, forwards_extra=True), Command('flashdex', 'FlashDexRetarget: human hand-object demos (TACO / OakInk2 / HOT3D / DexYCB) → bimanual robot-hand motion.pt', subcommands=(Command('import', "Import a released dataset into <dataset>/human_demo/ and objects/ (pass --help after 'import' for options)", handler=_run_flashdex_import, forwards_extra=True), Command('kinematic', "Support disks, convex parts, MuJoCo physics IK and motion.pt for one robot (pass --help after 'kinematic' for options)", handler=_run_flashdex_kinematic, forwards_extra=True))))), Command('wuji', 'Wuji Glove teleoperation commands', subcommands=(Command('scan', "List every Wuji Glove on the bus (requires the 'wuji' extra)", handler=_run_wuji_scan, forwards_extra=True), Command('monitor', "Watch a Wuji Glove's live hand pose and tactile sheet in the terminal — the first check after plugging one in (requires the 'wuji' extra; pass --help after 'monitor' for options)", handler=_run_wuji_monitor, forwards_extra=True))), Command('gif', 'Convert mp4 video to gif', handler=_run_gif, configure=_configure_gif), Command('view', 'View an entity by name, or a gaussian-splat .ply by path', handler=_run_view, configure=_configure_view), Command('arrange', 'Interactively arrange entities with ImGui sliders (base 6-DoF + joints) and export a SceneOptions snippet', handler=_run_arrange, configure=_configure_arrange), Command('snap', 'Take a snapshot of the scene/entity', handler=_run_snap, configure=_configure_snap), Command('clean', 'Clean up logs', handler=_run_clean))

Functions:

main

main()

Eden CLI entry point: build the parser from COMMANDS and dispatch.

Source code in eden/__main__.py
def main():
    """Eden CLI entry point: build the parser from ``COMMANDS`` and dispatch."""
    # prog="eden" so `python -m eden` prints `usage: eden ...`, not `usage: __main__.py` —
    # the module form is the documented fallback when the console script isn't on PATH,
    # and it matches the `prog="eden <sub>"` the subcommands already set.
    parser = argparse.ArgumentParser(prog="eden", description="Eden - Robot Learning Environments")
    parser.add_argument(
        "--log-theme",
        choices=("auto", "human", "agent"),
        default=None,
        help=(
            "How this command renders its console output: agent (the default — no ANSI, no "
            "greeting box), human (color), auto (color on a TTY, honoring NO_COLOR/FORCE_COLOR). "
            "Goes before the subcommand ('eden --log-theme human train ...'), because the "
            "forwarding subcommands hand everything after their own flags to the task parser. "
            "Sets EDEN_LOG_THEME for this process and anything it launches; setting that "
            "variable directly is the way to pick a theme for a whole shell."
        ),
    )
    subparsers = parser.add_subparsers(dest="command", help="Available commands")
    parsers: dict[Command, argparse.ArgumentParser] = {}
    for cmd in COMMANDS:
        _add_command(subparsers, cmd, parsers)

    args, extra_args = parser.parse_known_args()

    # Before anything renders. Exported rather than threaded through every handler: the theme has
    # to reach `eden.init()` (whose `logger_theme=None` reads it), the argparse help formatters,
    # and any subprocess a command spawns — one variable does all three, and the flag is then
    # exactly the documented environment variable with a shorter spelling. An explicit
    # `--log-theme` beats an inherited EDEN_LOG_THEME, which is the usual precedence for a flag.
    if args.log_theme is not None:
        # `set_theme` exports the variable (for anything this process launches) and applies it.
        # This process itself built `eden.logger` back at `import eden`, and imported the config
        # modules (which seed Genesis' palette) before argv was parsed, so the flag has to be
        # *applied* as well as exported -- otherwise every record up to `eden.init()` (the
        # `--resume` line, the config dump) still renders in the inherited theme.
        eden.logger.set_theme(args.log_theme)

    # A global flag on the root parser is only reachable before the subcommand, and every
    # command after it either rejects it as unknown or forwards it to a task parser that does.
    # Both of those name the flag and nothing else, so say where it goes — this is the one
    # mistake the placement guarantees someone will make.
    # Matched on the prefix, not the exact spelling: argparse accepts any unambiguous abbreviation
    # of a long option, so someone who typed `--log-th` where it works keeps typing it where it
    # does not. `len > 2` excludes a bare `--`, which every prefix check otherwise matches.
    misplaced = [
        arg for arg in extra_args if len(prefix := arg.split("=", 1)[0]) > 2 and "--log-theme".startswith(prefix)
    ]
    if misplaced:
        # `args.command` is None when argparse took the subcommand slot for something else; an
        # error message that says `eden None ...` sends the reader looking for a command by that
        # name, so name the slot instead of printing the miss.
        subcommand = args.command or "<command>"
        parser.error(
            f"{misplaced[0]} came after the subcommand, where it belongs to "
            f"'eden {subcommand}' rather than to eden. Put it first: "
            f"`eden --log-theme <auto|human|agent> {subcommand} ...`, or set "
            f"{LOG_THEME_ENV_VAR} for the whole shell."
        )

    if args.command is None:
        parser.print_help()
        sys.exit(0)

    cmd = next(c for c in COMMANDS if c.name == args.command)

    # Forwarding commands and groups parse leftover args themselves; for everyone
    # else, unknown args are a user error (so typos like `eden view r --collison`
    # aren't silently dropped) — mirrors argparse's parse_args behavior.
    if extra_args and not cmd.consumes_extra:
        parser.error(f"unrecognized arguments: {' '.join(extra_args)}")

    _dispatch(cmd, args, extra_args, parsers)

Command primitives

Command primitives for the eden CLI: the :class:Command shape and the argparse build/dispatch helpers.

This module is pure mechanism — it knows nothing about which commands exist. eden.__main__ declares the COMMANDS table (importing the handlers from the area modules) and drives _add_command / _dispatch over it. Keeping the table out of here means importing these primitives never pulls the command bodies.

Classes

Command dataclass

Command(name: str, help: str, handler: Callable | None = None, configure: Callable[[ArgumentParser], None] | None = None, forwards_extra: bool = False, subcommands: tuple[Command, ...] = ())

One CLI subcommand (or subcommand group). Three mutually exclusive shapes:

  • forwarding (forwards_extra=True) — declares no flags; the raw extra_args are handed to handler(extra_args) for the command to parse itself (the task commands build their flags from the task's config schema). --help is left to the handler, so argparse auto-help is off.
  • self-parsed (configure set, or neither) — declares flags via configure(parser) (omitted for no-arg commands like list); the parsed namespace goes to handler(args).
  • group (subcommands set) — a parent like gello that dispatches to one of its children. Children may be any shape (_dispatch recurses).

__post_init__ enforces the shape contract so a misconfigured entry fails at import, not silently at dispatch.

Attributes

name instance-attribute
name: str
help instance-attribute
help: str
handler class-attribute instance-attribute
handler: Callable | None = None
configure class-attribute instance-attribute
configure: Callable[[ArgumentParser], None] | None = None
forwards_extra class-attribute instance-attribute
forwards_extra: bool = False
subcommands class-attribute instance-attribute
subcommands: tuple[Command, ...] = ()
consumes_extra property
consumes_extra: bool

Whether leftover args are forwarded (True) or rejected as a typo (False).

Methods:

Task commands

The handlers of eden train, eden inference, eden export, eden deploy, eden preprocess and eden smoke.

Task-execution commands: train, inference, export, deploy, preprocess, smoke.

Functions:

train_task

train_task(extra_args: list[str]) -> None

eden train --task <name> [task-mod flags] — train a registered task.

Forwards extra_args (everything after train) to :func:eden.tasks.parser.get_task_argparser. The argparser also uses extra_args (not sys.argv) for its --task peek, so the same arg slice drives both mod-flag registration and the final parse. Dispatches into the right framework (rsl_rl by default, rl_games when the task's runner_options is an RlGamesPpoRunnerOptions).

Source code in eden/utils/cli/tasks.py
def train_task(extra_args: list[str]) -> None:
    """``eden train --task <name> [task-mod flags]`` — train a registered task.

    Forwards ``extra_args`` (everything after ``train``) to
    :func:`eden.tasks.parser.get_task_argparser`. The argparser also uses
    ``extra_args`` (not ``sys.argv``) for its ``--task`` peek, so the same
    arg slice drives both mod-flag registration and the final parse.
    Dispatches into the right framework (rsl_rl by default, rl_games when
    the task's ``runner_options`` is an ``RlGamesPpoRunnerOptions``).
    """
    from eden.tasks.parser import get_task_argparser, get_task_config_from_args

    parser = get_task_argparser(
        description="Train a registered task with TaskMod-driven CLI.",
        argv=extra_args,
    )
    parser.add_argument(
        "--logger",
        type=str,
        default="tensorboard",
        choices=["tensorboard", "neptune", "wandb"],
        help="rsl_rl logger backend (ignored for rl_games).",
    )
    parser.add_argument(
        "--num_envs",
        type=int,
        default=None,
        help="Override the task's training num_envs.",
    )
    parser.add_argument(
        "--resume",
        type=str,
        default=None,
        metavar="REF",
        help="Resume from a prior run of this task, resolving REF under logs/<task>/: "
        "'latest' (most recently modified), a run id, or a run_name substring. Loads that "
        "run's latest checkpoint and forks a new run dir recording its parent. Mutually "
        "exclusive with --checkpoint.",
    )
    parser.add_argument(
        "--wandb_group",
        type=str,
        default=None,
        help="wandb group for this run (defaults to the task name). Ablation arms sharing "
        "a group are compared together in the wandb UI. Ignored unless --logger wandb.",
    )
    args = parser.parse_args(extra_args)
    _validate_task_arg(args, parser)
    if args.num_envs is not None and args.num_envs <= 0:
        parser.error("--num_envs must be a positive integer.")
    if args.resume and args.checkpoint:
        parser.error("--resume and --checkpoint are mutually exclusive (both pick a checkpoint to load).")

    import eden as en
    import genesis as gs

    # Resolve --resume before building anything so a bad ref fails fast. A resume forks a
    # fresh run dir (en.init below) and records its parent in run.json, so wandb/TB
    # continuity is not silently hijacked.
    resume_checkpoint, resumed_from = None, None
    if args.resume:
        from eden.utils.assets import find_latest_checkpoint, resolve_run_dir

        parent_dir = resolve_run_dir(f"logs/{args.task}", args.resume)
        resume_checkpoint = find_latest_checkpoint(parent_dir)
        resumed_from = os.path.basename(os.path.normpath(parent_dir))
        en.logger.info(
            f"↩️  Resuming from ~~<{resumed_from}>~~ ({os.path.basename(resume_checkpoint)}) into a fresh run"
        )

    en.init(
        backend=gs.gpu if not args.cpu else gs.cpu,
        log_root_path=f"logs/{args.task}",
        run_name=args.run_name,
        performance_mode=True,
    )

    config = get_task_config_from_args(args, run_name=args.run_name)
    # Tasks with no in-Eden runner are trained by a standalone script (or only evaluated), not `eden train`.
    # Must fire before any config.runner_options access below (logger/seed/save).
    if config.runner_options is None:
        sys.exit(
            f"Task '{args.task}' declares no runner_options, so `eden train` has no RL backend for it. "
            f"{_no_runner_hint(config)}"
        )
    if args.num_envs is not None:
        config.env_options.num_envs = args.num_envs
    if hasattr(config.runner_options, "logger"):
        config.runner_options.logger = args.logger
    # Group runs in wandb (its natural home for ablation arms; the run_name folded into the
    # log-dir basename above already names each run). Defaults to the task, or --wandb_group
    # to cluster an ablation across tasks. wandb reads WANDB_RUN_GROUP natively, so this
    # needs no rsl_rl/rl_games plumbing; setdefault leaves an explicit env value untouched.
    if args.logger == "wandb":
        os.environ.setdefault("WANDB_RUN_GROUP", args.wandb_group or args.task)
    # config.json/run.json record the *base* seed exactly as configured — it is concrete by
    # construction (negatives are rejected at config load) and `apply_run_seed` adds the
    # per-rank offset later, inside the wrapper's from_config.
    # Main rank only: all ranks share this log_dir, so writing from each would race. Workers
    # build the env from the in-memory config, not the file.
    checkpoint = args.checkpoint or resume_checkpoint  # mutually exclusive (validated above)
    if not _is_rl_games(config):
        _check_rsl_rl_runner(config, checkpoint)
    if en.distributed.is_main_process():
        config.save_as_json()
        _write_run_manifest(config, args, resumed_from=resumed_from)

    if _is_rl_games(config):
        _run_train_rl_games(config, checkpoint=checkpoint)
    else:
        _run_train_rsl_rl(config, checkpoint=checkpoint)

inference_task

inference_task(extra_args: list[str]) -> None

eden inference --task <name> --checkpoint <path> [task-mod flags].

Loads a trained checkpoint and runs the eval loop for episode_length_s (or --steps / --episodes), through rsl_rl or rl_games — whichever the task's runner_options selects, after :func:_check_checkpoint_framework confirms the checkpoint was saved by it.

Frozen extension policies (POLICY_REGISTRY) can be rolled out in sim instead of a checkpoint via --policy NAME — the sim counterpart of eden deploy --policy. With --task, the policy runs on the task's env (the policy's command terms are merged in); without one, the env is built from the policy's own spec methods on an empty scene with the --robot robot.

With --async (a.k.a. --sim2sim) the rollout runs deployment-style instead: policy inference moves to an async background thread and the sim loop is paced to wall-clock env.dt, repeating the last action whenever inference hasn't finished — a sim-side dry run of eden deploy. This applies to both checkpoints and --policy extension policies. See :func:_run_sim2sim_loop.

Source code in eden/utils/cli/tasks.py
def inference_task(extra_args: list[str]) -> None:
    """``eden inference --task <name> --checkpoint <path> [task-mod flags]``.

    Loads a trained checkpoint and runs the eval loop for ``episode_length_s`` (or
    ``--steps`` / ``--episodes``), through rsl_rl or rl_games — whichever the task's
    ``runner_options`` selects, after :func:`_check_checkpoint_framework` confirms the
    checkpoint was saved by it.

    Frozen extension policies (``POLICY_REGISTRY``) can be rolled out in sim
    instead of a checkpoint via ``--policy NAME`` — the sim counterpart of
    ``eden deploy --policy``. With ``--task``, the policy runs on the task's
    env (the policy's command terms are merged in); without one, the env is
    built from the policy's own spec methods on an empty scene with the
    ``--robot`` robot.

    With ``--async`` (a.k.a. ``--sim2sim``) the rollout runs deployment-style
    instead: policy inference moves to an async background thread and the sim
    loop is paced to wall-clock ``env.dt``, repeating the last action whenever
    inference hasn't finished — a sim-side dry run of ``eden deploy``. This
    applies to both checkpoints and ``--policy`` extension policies. See
    :func:`_run_sim2sim_loop`.
    """
    from eden.tasks.parser import get_task_argparser, get_task_config_from_args

    parser = get_task_argparser(
        description="Run inference for a registered task or a frozen extension policy "
        "(add --async for a deployment-style, wall-clock-paced rollout).",
        argv=extra_args,
    )
    _add_shared_policy_cli_args(parser, command="inference")
    parser.add_argument(
        "--rec",
        action="store_true",
        help="Record an mp4 via a 'rec' camera (saved under the run's log dir).",
    )
    parser.add_argument(
        "--steps",
        type=int,
        default=None,
        help="Cap inference at N env steps (default: one episode, i.e. max_episode_length).",
    )
    parser.add_argument(
        "--episodes",
        type=int,
        default=None,
        help="Count each env's first ceil(N / num_envs) episodes, running until every env has finished them: "
        "ceil(N / num_envs) * num_envs episodes in all, at least N (stopping at N resets over all envs would leave "
        "out the longer episodes still running).",
    )
    parser.add_argument(
        "--num_envs",
        type=int,
        default=1,
        help="Number of parallel eval envs (default: 1).",
    )
    parser.add_argument(
        "--async",
        "--sim2sim",
        action="store_true",
        dest="run_async",
        help="Run deployment-style: policy inference in an async background thread, sim "
        "paced to wall-clock dt, last action repeated when inference lags — a sim-side dry "
        "run of 'eden deploy'. Works with both checkpoints and --policy.",
    )
    parser.add_argument(
        "--policy",
        "--extension_policy",
        default=None,
        dest="extension_policy",
        metavar="NAME",
        help="Frozen extension policy to roll out in sim instead of a checkpoint. "
        "Accepts the package name from eden/extensions/policies (e.g. 'holosoma', "
        "'homie'), the POLICY_REGISTRY key, or the key without its '_policy' suffix. "
        "With --policy, --task and --checkpoint become optional: a task-less run "
        "builds the env from the policy's own spec on the --robot scene.",
    )
    parser.add_argument(
        "--clip_name",
        default=None,
        metavar="CLIP",
        help="Clip to track for per-clip extension policies (e.g. beyondmimic_moves). "
        "Ignored by policies that don't take one; they use their default clip.",
    )
    args = parser.parse_args(extra_args)
    if args.extension_policy:
        if args.checkpoint:
            parser.error("--policy and --checkpoint are mutually exclusive; 'eden deploy' supports both as slots.")
        if args.episodes is not None:
            parser.error("--episodes is not supported with --policy (extension policies run a fixed --steps budget).")
        if args.task:
            _validate_task_arg(args, parser)
    else:
        _validate_task_arg(args, parser)
        if not args.checkpoint:
            parser.error("--checkpoint is required for 'eden inference' (or pass --policy).")
    if args.steps is not None and args.steps <= 0:
        parser.error("--steps must be a positive integer.")
    if args.episodes is not None and args.episodes <= 0:
        parser.error("--episodes must be a positive integer.")
    if args.num_envs <= 0:
        parser.error("--num_envs must be a positive integer.")

    import eden as en
    import genesis as gs

    en.init(
        backend=gs.gpu if not args.cpu else gs.cpu,
        log_root_path=_replay_log_root(args, task_less="logs/inference"),
        performance_mode=True,
    )

    # Prefer the checkpoint's own config.json (the exact trained config) over rebuilding
    # from --task + mods; falls back to the rebuild when there's no sibling config.json.
    config = (
        (_config_from_checkpoint(args) or get_task_config_from_args(args, run_name=args.run_name))
        if args.task
        else None
    )
    if config is not None:
        config.env_options.num_envs = args.num_envs
        if hasattr(config.runner_options, "logger"):
            config.runner_options.logger = "tensorboard"  # never wandb-log a checkpoint replay
        if args.rec:
            config.add_recording_camera()

    if args.extension_policy:
        _run_inference_policy(
            config,
            args.extension_policy,
            show_viewer=args.show_viewer,
            record=args.rec,
            max_steps=args.steps or _DEFAULT_POLICY_INFERENCE_STEPS,
            entity_name=args.entity_name,
            robot_name=args.robot,
            motion_file=args.motion_file,
            clip_name=args.clip_name,
            base_command=tuple(args.base_command),
            loop_fn=_run_sim2sim_loop if args.run_async else None,
        )
        return

    config.save_as_json()

    if config.runner_options is None:
        sys.exit(
            f"Task '{args.task}' declares no runner_options, so `eden inference` has no runner to replay it. "
            f"{_no_runner_hint(config)}"
        )

    configured = _check_checkpoint_framework(config, args.checkpoint)
    runner_fn = _run_eval_rl_games if configured == "rl_games" else _run_eval_rsl_rl
    runner_fn(
        config,
        args.checkpoint,
        show_viewer=args.show_viewer,
        record=args.rec,
        max_steps=args.steps,
        max_episodes=args.episodes,
        loop_fn=_run_sim2sim_loop if args.run_async else None,
    )

export_task

export_task(extra_args: list[str]) -> None

eden export --task <name> --checkpoint <path> [--format onnx,jit] [--out DIR].

Writes the checkpoint's policy as a graph a runtime outside Eden can load (ONNX, TorchScript) and a policy_spec.json describing its observations, actions and control rate (:mod:eden.learning.export). The env is built from the checkpoint's own config.json, as eden inference builds it, with one env in eval mode and without the event terms that would randomize what the spec records as nominal: the spec is read off it, and every graph is checked against the policy on its observations.

Source code in eden/utils/cli/tasks.py
def export_task(extra_args: list[str]) -> None:
    """``eden export --task <name> --checkpoint <path> [--format onnx,jit] [--out DIR]``.

    Writes the checkpoint's policy as a graph a runtime outside Eden can load (ONNX, TorchScript) and a
    ``policy_spec.json`` describing its observations, actions and control rate (:mod:`eden.learning.export`).
    The env is built from the checkpoint's own ``config.json``, as ``eden inference`` builds it, with one env in
    eval mode and without the event terms that would randomize what the spec records as nominal: the spec is read
    off it, and every graph is checked against the policy on its observations.
    """
    from eden.learning.export import (
        EXPORT_FORMATS,
        check_export_dependencies,
        disable_contract_randomization,
        export_policy,
    )
    from eden.tasks.parser import get_task_argparser, get_task_config_from_args

    parser = get_task_argparser(
        description="Export a trained checkpoint's policy (ONNX / TorchScript) and its policy_spec.json "
        "for a runtime outside Eden.",
        argv=extra_args,
    )
    parser.add_argument(
        "--format",
        default=",".join(EXPORT_FORMATS),
        help=f"Comma-separated formats to write, from {', '.join(EXPORT_FORMATS)} (default: all of them).",
    )
    parser.add_argument(
        "--out",
        default=None,
        metavar="DIR",
        help="Directory to write into (default: exported/<checkpoint name>/ beside the checkpoint).",
    )
    parser.add_argument(
        "--no_verify",
        action="store_true",
        help="Skip checking each exported graph against the policy on the env's observations "
        f"(the ONNX check needs onnxruntime: {install_hint('onnx')}).",
    )
    args = parser.parse_args(extra_args)
    _validate_task_arg(args, parser)
    if not args.checkpoint:
        parser.error("--checkpoint is required for 'eden export'.")
    if not os.path.isfile(args.checkpoint):
        parser.error(f"--checkpoint {args.checkpoint!r} is not a file.")
    formats = tuple(fmt.strip() for fmt in args.format.split(",") if fmt.strip())
    unknown = sorted(set(formats) - set(EXPORT_FORMATS))
    if unknown or not formats:
        parser.error(
            f"--format {args.format!r} names {unknown or 'nothing'}; choose from {', '.join(EXPORT_FORMATS)} "
            f"(comma-separated, e.g. --format onnx)."
        )
    try:
        check_export_dependencies(formats, verify=not args.no_verify)
    except ImportError as exc:
        parser.error(str(exc))
    out_dir = args.out or os.path.join(
        os.path.dirname(os.path.abspath(args.checkpoint)),
        "exported",
        os.path.splitext(os.path.basename(args.checkpoint))[0],
    )

    import eden as en
    import genesis as gs

    en.init(
        backend=gs.gpu if not args.cpu else gs.cpu,
        log_root_path=_replay_log_root(args, task_less="logs/export"),
        performance_mode=True,
    )
    config = _config_from_checkpoint(args) or get_task_config_from_args(args, run_name=args.run_name)
    if config.runner_options is None:
        sys.exit(
            f"Task '{args.task}' declares no runner_options, so there is no Eden runner to load its checkpoint "
            f"into. {_no_runner_hint(config)}"
        )
    if _check_checkpoint_framework(config, args.checkpoint) == "rl_games":
        sys.exit(
            "eden export reads rsl_rl checkpoints only: an rl_games checkpoint has no export hooks to build the "
            "graph from. Export it with rl_games' own tooling, or train the task with rsl_rl (its default runner)."
        )
    config.env_options.num_envs = 1
    if hasattr(config.runner_options, "logger"):
        config.runner_options.logger = "tensorboard"  # never wandb-log an export
    if disabled := disable_contract_randomization(config):
        en.logger.info(f"📦 Disabled event terms {disabled}: they randomize the default pose the spec records.")

    env, runner = _rsl_rl_env_and_runner(config, args.checkpoint, inference=True, eval_mode=True)
    files = export_policy(
        runner,
        env,
        out_dir,
        formats=formats,
        task=args.task,
        checkpoint=args.checkpoint,
        verify=not args.no_verify,
    )
    checked = "written" if args.no_verify else "checked against the policy and written"
    en.logger.info(f"📦 Exported policy {checked}: " + ", ".join(f"~~<{path}>~~" for path in files.values()))

deploy_task

deploy_task(extra_args: list[str]) -> None

eden deploy --task <name> --checkpoint <path> [task-mod flags].

Run a trained policy on real hardware. The deployment backend is picked automatically from the robot bound to the task's scene entity via :func:eden.extensions.deployment.resolve_deployment_for — to support a new robot, add a single bind_deployment(...) line in its deployment module.

By default (G1 robots) the frozen Holosoma loco policy is preloaded as slot 1 and is the policy active on boot: after INIT → START the robot stands (or walks --base_command) under a known-good controller, and the operator switches to the checkpoint slot(s) — and back — via the remote Select button or keyboard n / 1-9. Pass --base_policy none to restore checkpoint-as-slot-1.

Frozen extension policies (POLICY_REGISTRY) can be preloaded as additional slots via --policy NAME. Either --task or --policy must be given: with a task, extension policies ride along on the task's env and --checkpoint becomes optional; without a task, the env is built on an empty scene with the --robot robot from the spec of the loaded policy that reads it, else of the first policy --policy named (:func:_env_owning_slot) — checkpoints then require --task.

Each slot converts its own actions, through the policy's own controller (:func:_slot_controllers) or, for the env-owning policy, through the env's action manager, so a policy is not required to emit absolute targets.

The function disables training-only randomization (push, friction / COM / encoder-bias randomization) and forces num_envs=1 so the policy drives a single physical robot. The sim env is still built (with the viewer optionally open) so it can mirror robot state for visualization while the policy runs on hardware.

Source code in eden/utils/cli/tasks.py
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
def deploy_task(extra_args: list[str]) -> None:
    """``eden deploy --task <name> --checkpoint <path> [task-mod flags]``.

    Run a trained policy on real hardware. The deployment backend is picked
    automatically from the robot bound to the task's scene entity via
    :func:`eden.extensions.deployment.resolve_deployment_for` — to support a
    new robot, add a single ``bind_deployment(...)`` line in its deployment
    module.

    By default (G1 robots) the frozen Holosoma loco policy is preloaded as
    slot 1 and is the policy active on boot: after INIT → START the robot
    stands (or walks ``--base_command``) under a known-good controller, and
    the operator switches to the checkpoint slot(s) — and back — via the
    remote Select button or keyboard ``n`` / ``1``-``9``. Pass
    ``--base_policy none`` to restore checkpoint-as-slot-1.

    Frozen extension policies (``POLICY_REGISTRY``) can be preloaded as
    additional slots via ``--policy NAME``. Either ``--task`` or ``--policy``
    must be given: with a task, extension policies ride along on the task's
    env and ``--checkpoint`` becomes optional; without a task, the env is
    built on an empty scene with the ``--robot`` robot from the spec of the
    loaded policy that reads it, else of the first policy ``--policy`` named
    (:func:`_env_owning_slot`) — checkpoints then require ``--task``.

    Each slot converts its own actions, through the policy's own controller
    (:func:`_slot_controllers`) or, for the env-owning policy, through the env's
    action manager, so a policy is not required to emit absolute targets.

    The function disables training-only randomization (push, friction /
    COM / encoder-bias randomization) and forces ``num_envs=1`` so the
    policy drives a single physical robot. The sim env is still built (with
    the viewer optionally open) so it can mirror robot state for
    visualization while the policy runs on hardware.
    """
    from eden.tasks.parser import get_task_argparser, get_task_config_from_args

    parser = get_task_argparser(
        description="Deploy a trained task policy to a real robot.",
        argv=extra_args,
    )
    _add_shared_policy_cli_args(parser, command="deploy")
    parser.add_argument(
        "--deploy_kp_kd_scale",
        type=float,
        default=1.0,
        help="Scale on default Kp/Kd at deployment. Ramp up from 0.2 → 1.0 when first commissioning.",
    )
    parser.add_argument(
        "--gain_ramp_steps",
        type=int,
        default=None,
        help="Control steps over which a policy-slot switch ramps Kp/Kd to the new slot's own "
        "(backend default 50 = 1 s at 50 Hz; 0 jumps). Each slot runs at the gains its policy was "
        "trained at, and a stiffness step between two of them can knock the robot over.",
    )
    parser.add_argument(
        "--estimate_lin_vel",
        action="store_true",
        help="Estimate base linear velocity onboard (contact-aided Kalman filter fusing IMU "
        "acceleration with leg-kinematics odometry — the legged_control/BeyondMimic estimator). "
        "Observation terms reading base_lin_vel then see the estimate instead of zeros. "
        "Contact is detected from joint torques (tau_est) via the foot Jacobian.",
    )
    parser.add_argument(
        "--feet_links",
        nargs="+",
        default=None,
        metavar="LINK",
        help="Foot link name(s) the --estimate_lin_vel filter hangs its leg odometry and contact "
        "detection on (e.g. left_ankle_roll_link right_ankle_roll_link). Defaults to the robot's "
        "own feet (RobotOptions.feet_links_name), which the deployable legged robots declare, so "
        "this is only needed for a robot that declares none — a wheeled or retargeting-only body "
        "— or to estimate off a different pair of links.",
    )
    parser.add_argument(
        "--velocity_command",
        choices=("constant", "keyboard", "gamepad", "task"),
        default="constant",
        help="What drives the deployed SE(2) velocity command, if the task declares one. A "
        "policy deployed from its training config otherwise keeps generating the command it "
        "was TRAINED on -- a fresh random (vx, vy, wz) every few seconds -- which on a robot "
        "reads as it wandering off on its own. 'constant' (default) substitutes a term holding "
        "--base_command, so the robot stands until told otherwise. 'keyboard' (arrows for "
        "vx/vy, , / . for yaw; letters and digits belong to the operator state machine and "
        "pynput listens globally) and 'gamepad' (left stick vx/vy, right stick X yaw) hand it "
        "to an operator. 'task' keeps the training generator. Full deflection is the fastest "
        "command the replaced term declares, and the operator cannot exceed it. All of it "
        "works on hardware and with --mujoco.",
    )
    parser.add_argument(
        "--gamepad_layout",
        choices=("ps4", "xbox"),
        default="ps4",
        help="Axis scaling for --velocity_command gamepad (PS4 sticks report 0..255, Xbox "
        "-32768..32767). Default: ps4.",
    )
    parser.add_argument(
        "--velocity_command_scale",
        nargs=3,
        type=float,
        default=None,
        metavar=("VX", "VY", "WZ"),
        help="Symmetric per-axis limit for the substituted velocity command, overriding the "
        "trained ranges of the term it replaces. Lower it to bound the operator (or a constant "
        "--base_command) below what the policy was trained for while commissioning; it is also "
        "the only bound available on a task-less --policy deploy, which has no term to read.",
    )
    parser.add_argument(
        "--extra_checkpoint",
        action="append",
        default=None,
        metavar="PATH",
        help="Additional policy checkpoint(s) to preload for runtime switching "
        "(repeatable). Slots are numbered in load order: the base policy (if any) "
        "is slot 1, then --checkpoint, then extras. Switch live via the G1 remote "
        "Select button or keyboard n / 1-9 (9 policies max).",
    )
    parser.add_argument(
        "--base_policy",
        choices=("auto", "holosoma", "none"),
        default="auto",
        help="Frozen base/safety policy preloaded as slot 1 and active on boot. "
        "'auto' (default) loads the Holosoma G1 velocity-tracking loco policy only when the "
        "deploy robot is a G1 29-DOF (G1Dofs29), and loads nothing otherwise (--checkpoint "
        "becomes slot 1). 'holosoma' forces it (errors on a non-G1 robot); 'none' disables it. "
        "With Holosoma, the robot stands (or walks --base_command) under a known-good "
        "controller until you switch to a checkpoint slot, and you can always switch back.",
    )
    parser.add_argument(
        "--policy",
        "--extension_policy",
        action="append",
        default=None,
        dest="extension_policy",
        metavar="NAME",
        help="Extension policy to preload as an extra switchable slot (repeatable). "
        "Accepts the package name from eden/extensions/policies (e.g. 'holosoma', "
        "'homie'), the POLICY_REGISTRY key, or the key without its '_policy' suffix. "
        "Each slot converts through its own controller, so any joint-position controller "
        "deploys, scaled and pose-offset ones included. Policies that track a reference "
        "motion (e.g. 'sonic') also need --motion_file; their command terms are merged "
        "into the env. With --policy, --task and --checkpoint become optional: a task-less "
        "deploy builds the env from the spec of whichever loaded policy reads it "
        "(the first policy's, when none does) on the --robot scene.",
    )
    parser.add_argument(
        "--disable_joints",
        nargs="+",
        default=None,
        metavar="NAME",
        help="Joint name(s) or fnmatch patterns (e.g. 'left_wrist_*' '*_ankle_*_joint') to disable: "
        "send kp=kd=0 so the joint goes free/limp (the policy still commands it, the motor ignores it). "
        "For bench debugging — isolate a buzzing joint or free the arms. Do NOT disable load-bearing leg "
        "joints on an unsupported robot; it will collapse.",
    )
    parser.add_argument(
        "--ignore_action_terms",
        nargs="+",
        default=None,
        metavar="TERM",
        help="Action term name(s) whose command the payload has no channel for (e.g. a suction "
        "term's vacuum bit) and that you drive over your own channel. RobotCommand carries only "
        "joint dofs_pos/vel/torque/kp/kd, so such a term cannot be sent; naming it here "
        "acknowledges that and lets the deploy proceed. Without it the deployment refuses to "
        "start, rather than dropping the command silently on the robot. Names are validated: a "
        "term the payload does carry is rejected here (it would not stop being sent) — use "
        "--disable_joints to leave joints undriven.",
    )
    parser.add_argument(
        "--debug_record",
        action="store_true",
        help="Record per-step commanded targets (pos/kp/kd) + measured proprioception "
        "(joint pos/vel/torque, base quat/ang-vel/lin-acc) and write a .npz on exit. "
        "For offline diagnosis (commanded-vs-measured, torque saturation, limit cycles).",
    )
    parser.add_argument(
        "--debug_record_path",
        type=str,
        default=None,
        help="Destination .npz for --debug_record (default: <log_dir>/deploy_record.npz).",
    )
    parser.add_argument(
        "--debug_record_flush_steps",
        type=int,
        default=None,
        help="Steps per streamed --debug_record chunk (backend default 100 = 2 s at 50 Hz). The "
        "recording is written as the run goes, so this is the most a Ctrl-C or a crash can cost; "
        "chunks left by a run that never closed are read back by "
        "eden.extensions.deployment.utils.record.load_deploy_record.",
    )
    parser.add_argument(
        "--target_lpf",
        type=float,
        default=1.0,
        help="First-order low-pass on commanded joint targets: sent = a*new + (1-a)*prev "
        "(a in (0,1]; 1.0 = off). Attenuates high-frequency command content (e.g. a single-frame "
        "policy's half-rate limit cycle / motor whine) while leaving kp/kd — and standing "
        "stiffness — intact. Try 0.5 / 0.3 with full --deploy_kp_kd_scale.",
    )
    parser.add_argument(
        "--part_option",
        action="append",
        default=None,
        metavar="PART.OPTION=VALUE",
        help="Composite rigs only: set one option of one part's backend, e.g. "
        "--part_option robot.robot_ip=172.16.0.2 --part_option hand.protocol=EtherCAT. Repeatable. "
        "VALUE is read as the option's declared type: a str option keeps it as typed, a bool takes "
        "true/false, a number or a tuple is read as a Python literal; a value of another type is refused.",
    )
    parser.add_argument(
        "--mujoco",
        action="store_true",
        help="Deploy against MuJoCo instead of hardware (sim-to-sim): run the exact deploy loop "
        "against a MuJoCo model of the robot — a robot-agnostic port of the idea behind "
        "unitree_mujoco, useful to validate a checkpoint against an independent physics engine "
        "before touching a real robot. Overrides the robot→backend binding, so it works for any "
        "--task / --robot. Combine with --vis to open MuJoCo's viewer.",
    )
    parser.add_argument(
        "--gantry",
        action="store_true",
        help="With --mujoco, hang the robot from a simulated overhead hoist — the sim "
        "counterpart of the bring-up rig, so a biped stays upright while you commission a "
        "controller instead of toppling whenever control pauses. The STOP hold latches and holds "
        "its joint pose, but no joint-space hold can balance a biped — the ankles would have to "
        "out-stiffen m*g*h about the ankle, and on a G1 they are about a quarter of it — so an "
        "unsupported robot topples the moment nothing balances it. A tension-only rope to a "
        "fixed anchor, "
        "attached above the centre of mass so it gives a real restoring torque rather than "
        "just cancelling weight. SIM-ONLY: it is the rig, not the robot — anything that only "
        "stands with --gantry does not stand.",
    )
    parser.add_argument(
        "--gantry_body",
        nargs="+",
        default=None,
        metavar="LINK",
        help="Bodies the --gantry ropes are tied to, one rope each. Default: the robot's own "
        "declared mount links — both shoulders for a humanoid that names them (Oli does), so it "
        "hangs from two points like a real spreader bar; a robot that declares none hangs from "
        "its base by one rope. Two points resist roll as a couple, which one rope cannot. "
        "Naming a link the model does not have lists the ones it does.",
    )
    parser.add_argument(
        "--gantry_attach",
        nargs=3,
        type=float,
        default=None,
        metavar=("X", "Y", "Z"),
        help="Attachment point within each rope's body frame, metres. Default depends on the rig: "
        "the link origin for declared mount links (a shoulder origin sits on the pitch axis at "
        "the top of the torso and does not move as the arm swings), or 0 0 0.25 for a single "
        "base rope, which needs the height to have any moment arm at all.",
    )
    parser.add_argument(
        "--gantry_height",
        type=float,
        default=None,
        metavar="Z",
        help="World height of the --gantry anchor (default: 1 m above the attachment point). "
        "Raise it to take up rope and lift the robot, lower it to set the robot down.",
    )
    parser.add_argument(
        "--gantry_step",
        type=float,
        default=None,
        help="Metres the operator's + / - keys move the --gantry anchor per press (default 0.05).",
    )
    parser.add_argument(
        "--gantry_no_draw",
        action="store_true",
        help="Do not draw the --gantry ropes and their force arrows in the --vis viewer. They "
        "are drawn by default: a rope quietly carrying the robot looks exactly like a robot "
        "that is standing, right up until you unrig it.",
    )
    parser.add_argument(
        "--gantry_force_scale",
        type=float,
        default=None,
        help="Metres of --gantry force arrow per newton (default 1e-3, so 1 kN draws as 1 m).",
    )
    parser.add_argument(
        "--gantry_slack",
        type=float,
        default=None,
        help="Metres of slack in the --gantry rope. 0 (default) is taut and holds station; a "
        "positive value is a safety tether that does nothing until the robot drops that far.",
    )
    parser.add_argument(
        "--gantry_support",
        type=float,
        default=None,
        help="Scale on the --gantry rope force, 0-1 (default 1). Ramp down across runs to hand "
        "the robot's weight back to its own controller without re-rigging.",
    )
    parser.add_argument(
        "--imu_noise",
        nargs=2,
        type=float,
        default=None,
        metavar=("ACC", "GYRO"),
        help="With --mujoco, add white noise to the simulated IMU: per-axis standard deviation of "
        "the accelerometer (m/s^2) and the gyro (rad/s). MuJoCo's IMU is otherwise exact, so an "
        "--estimate_lin_vel run cannot show the drift a hardware IMU produces when foot contact "
        "is lost. Try 0.3 0.05.",
    )
    parser.add_argument(
        "--imu_bias",
        nargs=6,
        type=float,
        default=None,
        metavar=("AX", "AY", "AZ", "GX", "GY", "GZ"),
        help="With --mujoco, add a constant body-frame bias to the simulated accelerometer (m/s^2, "
        "first three) and gyro (rad/s, last three).",
    )
    parser.add_argument(
        "--imu_noise_seed",
        type=int,
        default=None,
        help="Seed for --imu_noise, so a noisy --mujoco run can be repeated.",
    )
    parser.add_argument(
        "--mujoco_props",
        nargs="*",
        default=None,
        metavar="ENTITY",
        help="With --mujoco on a composite rig: the scene entities to simulate besides the rig (default: every "
        "colliding one but the ground). Each must be a box or a rigid URDF / MJCF body with no joints; name only "
        "those to leave the rest out, or give none to simulate the rig alone.",
    )
    parser.add_argument(
        "--prop_noise",
        nargs=2,
        type=float,
        default=None,
        metavar=("POS", "ROT"),
        help="With --mujoco on a composite rig: what the policy reads of every free prop is a camera's pose "
        "estimate, not MuJoCo's exact pose -- per-axis position noise (m) and a rotation error (rad), as standard "
        "deviations, with a random quaternion sign. An AprilTag cube: 0.003 0.035.",
    )
    parser.add_argument(
        "--prop_camera",
        nargs=2,
        type=float,
        default=None,
        metavar=("HZ", "LATENCY_S"),
        help="With --mujoco on a composite rig: the free props' observed poses come at the camera's frame rate, "
        "held between frames, each LATENCY_S old when it arrives (rounded to control ticks). E.g. 30 0.04.",
    )
    parser.add_argument(
        "--prop_noise_seed",
        type=int,
        default=None,
        help="Seed for --prop_noise, so a noisy --mujoco run can be repeated.",
    )
    parser.add_argument(
        "--mujoco_record",
        type=str,
        default=None,
        metavar="MP4",
        help="With --mujoco: render the rollout from MuJoCo, from START on, to this mp4 (two views side by side). "
        "Lockstep only (not with --async). Headless, set MUJOCO_GL=egl.",
    )
    parser.add_argument(
        "--mujoco_seconds",
        type=float,
        default=None,
        help="With --mujoco: end the run once the policy has run this many seconds since its latest START, as "
        "KILL does -- a headless rollout ends by itself.",
    )
    parser.add_argument(
        "--mujoco_sim_dt",
        type=float,
        default=None,
        metavar="SECONDS",
        help="With --mujoco, MuJoCo's timestep (default: a single robot keeps its asset's; a composite rig takes the "
        "largest of at most 2 ms that divides the control period; a single robot's asset timestep that does not "
        "divide it gives way to the largest under it that does). In lockstep a given one must divide the control "
        "period. On a composite rig, Genesis' substep (sim_dt / sim_substeps) floors a contact's time constant as "
        "Genesis does.",
    )
    parser.add_argument(
        "--menagerie",
        action="store_true",
        help="With --mujoco, load the robot's community-tuned MuJoCo Menagerie model "
        "(armature/damping/contacts tuned for MuJoCo — higher-fidelity sim-to-sim) from HF "
        "instead of its own Genesis training asset. Errors if the robot has no Menagerie "
        "mapping or the download fails.",
    )
    parser.add_argument(
        "--on_motion_end",
        choices=("continue", "stand", "switch"),
        default=None,
        help="What a motion-imitation deploy does once the clip reaches its last frame. "
        "'continue' (default) keeps running the policy against a reference that has stopped "
        "moving. 'switch' hands control to --motion_end_slot through the same queue the "
        "operator's remote/keyboard uses -- point it at a loco/stand policy (e.g. --policy "
        "limx_oli_loco at zero command) and the robot keeps its balance after the clip. "
        "'stand' instead disengages the policy and ramps the joints to the default stance over "
        "--stand_steps; that holds a shape, not a stand, so a biped topples within seconds -- "
        "it suits a quadruped or a fixed-base arm. All three run off the motion clock and send "
        "ordinary wire commands, so hardware and --mujoco behave identically.",
    )
    parser.add_argument(
        "--motion_end_slot",
        type=int,
        default=None,
        help="1-based slot --on_motion_end switch hands to, numbered like the operator keys 1-9 "
        "(default 1). With --base_policy none, slot 1 is --checkpoint and the --policy "
        "extensions follow, so a single extension policy is slot 2.",
    )
    parser.add_argument(
        "--policy_checkpoint",
        action="append",
        default=None,
        metavar="NAME=PATH",
        help="Point an extension policy at a local checkpoint instead of its released weights, "
        "e.g. --policy_checkpoint limx_oli_loco=/path/model.pt. Repeatable. Required for a "
        "policy that ships no default checkpoint.",
    )
    parser.add_argument(
        "--stand_steps",
        type=int,
        default=None,
        help="Control cycles the --on_motion_end stand ramp takes (default 100 = 2s at 50 Hz). "
        "Longer is gentler on hardware.",
    )
    parser.add_argument(
        "--init_steps",
        type=int,
        default=None,
        help="Control cycles the INIT ramp takes to reach the start pose before the policy takes "
        "over (default 250 = 5 s at 50 Hz, on hardware and --mujoco alike; 0 disables the ramp). "
        "Every backend runs the INIT state.",
    )
    parser.add_argument(
        "--init_target",
        choices=("default", "motion", "motion_rsi"),
        default=None,
        help="Where the INIT ramp drives the joints before the policy takes over. 'default' "
        "(default) targets the robot's default_dofs_pos. 'motion' targets frame 0 of the tracked "
        "clip, so a motion-imitation policy starts from the pose it was trained to continue from "
        "instead of 100+ degrees away -- a joint-space ramp over the wire, identical on hardware "
        "and --mujoco. It is NOT automatically safe: frame 0 of a crouched clip is an open-loop "
        "squat with only the joint PD holding the base, and it can topple -- ramp slowly via "
        "--init_steps and support the robot the first time. 'motion_rsi' additionally teleports "
        "the base to frame 0; that is sim-only and --mujoco is the only backend that accepts it.",
    )
    parser.add_argument(
        "--async",
        action="store_true",
        dest="mujoco_async",
        help="With --mujoco, run the physics on its own thread in wall-clock time, like a robot's "
        "controller, instead of in lockstep with the control loop: the simulated robot keeps "
        "executing the last command while the policy is late and publishes state at 1/sim_dt "
        "regardless of when the policy reads it. Reproduces control-period overruns, command "
        "latency and state age the way hardware does; the default lockstep mode is "
        "deterministic and unaffected by the loop's timing. The physics thread reports if it "
        "cannot keep up with wall time.",
    )
    parser.add_argument(
        "--publish_rate",
        type=float,
        default=None,
        help="LimX Oli / Unitree G1 / Unitree Go2: rate at which the held wire command is re-sent between "
        "control ticks (backend defaults are each vendor's own low-level rate -- 1000 Hz for Oli, 500 Hz for "
        "the G1 and Go2). The Oli and G1 evaluate the PD of their parallel-linkage joints (ankle and waist "
        "roll/pitch) per received packet, so 50 Hz (one packet per tick) makes their damping act on a "
        "20 ms-old velocity and the linkage rings at 25 Hz; the Go2 has no such joints and re-sends for "
        "vendor-rate parity only. Pass 0 to send one packet per tick, for A/B only.",
    )
    parser.add_argument(
        "--power_protect_factor",
        type=int,
        default=None,
        help="Unitree Go1 only: Safety.PowerProtect factor, 1..10 == 10%%..100%% of rated motor "
        "power (backend default 10 = 100%%, no derating). LOWER it for cautious bench bring-up "
        "with the robot supported — but a factor set too low clamps the gait's torque and trips "
        "'Error: Power Protection' (the SDK examples' value of 1 = 10%% starves locomotion "
        "entirely). Ignored by non-Go1 backends.",
    )
    args = parser.parse_args(extra_args)
    if args.power_protect_factor is not None and not 1 <= args.power_protect_factor <= 10:
        parser.error("--power_protect_factor must be an integer in 1..10.")
    # The flags that only a MuJoCo sim-to-sim run reads: flag -> (its argparse attribute, what it does).
    mujoco_only = {
        "menagerie": ("menagerie", "selects the MuJoCo model to load"),
        "gantry": ("gantry", "is a simulated rig"),
        "async": ("mujoco_async", "runs the MuJoCo physics on its own thread"),
        "mujoco_sim_dt": ("mujoco_sim_dt", "sets MuJoCo's timestep"),
        "mujoco_props": ("mujoco_props", "picks the scene entities MuJoCo simulates"),
        **{name: (name, "corrupts the simulated IMU") for name in ("imu_noise", "imu_bias", "imu_noise_seed")},
        **{
            name: (name, "acts on a MuJoCo sim-to-sim run")
            for name in ("prop_noise", "prop_camera", "prop_noise_seed", "mujoco_record", "mujoco_seconds")
        },
    }
    for flag, (attr, what) in mujoco_only.items():
        value = getattr(args, attr)
        if value is not None and value is not False and not args.mujoco:
            parser.error(f"--{flag} {what}; it requires --mujoco.")
    for name in (
        "gantry_body",
        "gantry_attach",
        "gantry_height",
        "gantry_step",
        "gantry_force_scale",
        "gantry_slack",
        "gantry_support",
    ):
        if getattr(args, name) is not None and not args.gantry:
            parser.error(f"--{name} tunes the --gantry rope; it requires --gantry.")
    if args.gantry_no_draw and not args.gantry:
        parser.error("--gantry_no_draw turns off the --gantry overlay; it requires --gantry.")
    if args.gantry_force_scale is not None and args.gantry_force_scale <= 0:
        parser.error("--gantry_force_scale must be positive.")
    if args.gantry_step is not None and args.gantry_step <= 0:
        parser.error("--gantry_step must be positive.")
    if args.gantry_slack is not None and args.gantry_slack < 0:
        parser.error("--gantry_slack must be non-negative.")
    if args.gantry_support is not None and not 0.0 <= args.gantry_support <= 1.0:
        parser.error("--gantry_support must be in 0..1.")
    if args.init_steps is not None and args.init_steps < 0:
        parser.error("--init_steps must be a non-negative integer (0 disables the ramp).")
    if args.stand_steps is not None and args.stand_steps <= 0:
        parser.error("--stand_steps must be a positive integer.")
    if args.stand_steps is not None and args.on_motion_end != "stand":
        parser.error("--stand_steps sets the length of the --on_motion_end stand ramp; it requires it.")
    if args.motion_end_slot is not None and args.on_motion_end != "switch":
        parser.error("--motion_end_slot names the slot --on_motion_end switch hands to; it requires it.")
    if args.motion_end_slot is not None and not 1 <= args.motion_end_slot <= _MAX_DEPLOY_SLOTS:
        parser.error(f"--motion_end_slot must be in 1..{_MAX_DEPLOY_SLOTS}.")
    for spec in args.policy_checkpoint or []:
        if "=" not in spec:
            parser.error(f"--policy_checkpoint takes NAME=PATH; got {spec!r}.")
    if args.policy_checkpoint and not args.extension_policy:
        parser.error("--policy_checkpoint redirects a --policy extension policy; none was given.")
    if args.imu_noise is not None and min(args.imu_noise) < 0:
        parser.error("--imu_noise takes standard deviations; both must be >= 0.")
    try:
        parts_options = _parse_part_options(args.part_option or ())
    except ValueError as exc:
        parser.error(str(exc))
    # The backends refuse these values too; checked here as well so a bad flag fails before the env is built.
    if args.prop_noise is not None and min(args.prop_noise) < 0:
        parser.error("--prop_noise takes standard deviations; both must be >= 0.")
    if args.prop_camera is not None and (args.prop_camera[0] <= 0 or args.prop_camera[1] < 0):
        parser.error("--prop_camera takes a frame rate > 0 and a latency >= 0.")
    if args.mujoco_seconds is not None and args.mujoco_seconds <= 0:
        parser.error("--mujoco_seconds must be > 0.")
    if args.mujoco_sim_dt is not None and args.mujoco_sim_dt <= 0:
        parser.error("--mujoco_sim_dt must be > 0.")
    if parts_options and args.mujoco:
        parser.error("--part_option configures a composite rig's hardware backends; --mujoco has none.")
    if args.feet_links and not args.estimate_lin_vel:
        parser.error("--feet_links only feeds the base-linear-velocity estimator; it requires --estimate_lin_vel.")
    if args.velocity_command_scale and args.velocity_command == "task":
        parser.error(
            "--velocity_command_scale bounds the substituted command; it cannot apply to --velocity_command task."
        )
    if args.velocity_command_scale and any(v <= 0.0 for v in args.velocity_command_scale):
        parser.error("--velocity_command_scale takes three positive limits (VX VY WZ).")
    if args.gamepad_layout != "ps4" and args.velocity_command != "gamepad":
        parser.error("--gamepad_layout only applies to --velocity_command gamepad.")
    if tuple(args.base_command) != (0.0, 0.0, 0.0) and args.velocity_command in ("keyboard", "gamepad"):
        parser.error(
            "--base_command is the constant a substituted command holds; --velocity_command "
            f"{args.velocity_command} hands that command to the operator instead, so it would be "
            "ignored. Drop one of the two."
        )
    extension_policies = args.extension_policy or []
    if not args.task and not extension_policies:
        parser.error("Provide --task and/or --policy (an extension policy from POLICY_REGISTRY).")
    if args.task:
        _validate_task_arg(args, parser)
    elif args.checkpoint or args.extra_checkpoint:
        parser.error("--checkpoint requires --task (rsl_rl checkpoints load against the task's env).")
    if not args.checkpoint and args.extra_checkpoint:
        parser.error("--extra_checkpoint requires --checkpoint.")
    if not args.checkpoint and not extension_policies and args.base_policy == "none":
        parser.error("Provide at least one policy: --checkpoint, --policy, or --base_policy holosoma.")

    checkpoints = [ckpt for ckpt in [args.checkpoint, *(args.extra_checkpoint or [])] if ckpt]
    num_slots = len(checkpoints) + len(extension_policies) + (1 if args.base_policy != "none" else 0)
    if num_slots > _MAX_DEPLOY_SLOTS:
        parser.error(
            f"At most {_MAX_DEPLOY_SLOTS} policy slots (base + checkpoints + extensions) "
            "are supported for deploy switching."
        )

    import eden as en
    import genesis as gs

    en.init(
        backend=gs.gpu if not args.cpu else gs.cpu,
        log_root_path=_replay_log_root(args, task_less="logs/deploy"),
        performance_mode=True,
    )

    if args.task:
        # Prefer the checkpoint's own config.json (the exact trained config); fall back to
        # rebuilding from --task + mods when there's no sibling config.json.
        config = _config_from_checkpoint(args) or get_task_config_from_args(args, run_name=args.run_name)
        if _is_rl_games(config):
            sys.exit("'eden deploy' currently supports rsl_rl checkpoints only.")
        _strip_training_randomization(config)
        config.env_options.num_envs = 1
        config.env_options.episode_length_s = 1e9
        if hasattr(config.runner_options, "logger"):
            config.runner_options.logger = "tensorboard"
        # The deploy config snapshot is saved inside _run_deploy, after the
        # extension policies' command terms are merged in.
    else:
        # Task-less deploy: the env is built from the env-owning extension policy's
        # own spec methods inside _run_deploy (_env_owning_slot).
        config = None

    _run_deploy(
        config,
        checkpoints=checkpoints,
        show_viewer=args.show_viewer,
        entity_name=args.entity_name,
        deploy_kp_kd_scale=args.deploy_kp_kd_scale,
        gain_ramp_steps=args.gain_ramp_steps,
        base_policy=args.base_policy,
        base_command=tuple(args.base_command),
        extension_policies=extension_policies,
        robot_name=args.robot,
        motion_file=args.motion_file,
        estimate_lin_vel=args.estimate_lin_vel,
        feet_links=tuple(args.feet_links or ()),
        velocity_command=args.velocity_command,
        gamepad_layout=args.gamepad_layout,
        velocity_command_scale=tuple(args.velocity_command_scale or ()),
        disable_joints=tuple(args.disable_joints or ()),
        ignore_action_terms=tuple(args.ignore_action_terms or ()),
        debug_record=args.debug_record,
        debug_record_path=args.debug_record_path,
        debug_record_flush_steps=args.debug_record_flush_steps,
        target_lpf=args.target_lpf,
        power_protect_factor=args.power_protect_factor,
        publish_rate=args.publish_rate,
        mujoco=args.mujoco,
        menagerie=args.menagerie,
        mujoco_props=args.mujoco_props,
        init_steps=args.init_steps,
        init_target=args.init_target,
        on_motion_end=args.on_motion_end,
        stand_steps=args.stand_steps,
        motion_end_slot=args.motion_end_slot,
        policy_checkpoints=tuple(args.policy_checkpoint or ()),
        gantry=args.gantry,
        gantry_opts={
            "gantry_body": None if args.gantry_body is None else tuple(args.gantry_body),
            "gantry_attach": None if args.gantry_attach is None else tuple(args.gantry_attach),
            "gantry_height": args.gantry_height,
            "gantry_step": args.gantry_step,
            "gantry_draw": False if args.gantry_no_draw else None,
            "gantry_force_scale": args.gantry_force_scale,
            "gantry_slack": args.gantry_slack,
            "gantry_support": args.gantry_support,
        },
        mujoco_async=args.mujoco_async,
        mujoco_sim_dt=args.mujoco_sim_dt,
        parts_options=parts_options,
        imu_opts={
            "imu_acc_noise": None if args.imu_noise is None else args.imu_noise[0],
            "imu_gyro_noise": None if args.imu_noise is None else args.imu_noise[1],
            "imu_acc_bias": None if args.imu_bias is None else tuple(args.imu_bias[:3]),
            "imu_gyro_bias": None if args.imu_bias is None else tuple(args.imu_bias[3:]),
            "imu_noise_seed": args.imu_noise_seed,
        },
        rig_opts={
            "--prop_noise": {
                "prop_pos_noise": None if args.prop_noise is None else args.prop_noise[0],
                "prop_rot_noise": None if args.prop_noise is None else args.prop_noise[1],
            },
            "--prop_camera": {
                "prop_camera_hz": None if args.prop_camera is None else args.prop_camera[0],
                "prop_latency_s": None if args.prop_camera is None else args.prop_camera[1],
            },
            "--prop_noise_seed": {"prop_noise_seed": args.prop_noise_seed},
        },
        mujoco_record=args.mujoco_record,
        mujoco_seconds=args.mujoco_seconds,
    )

preprocess_task

preprocess_task(extra_args: list[str]) -> None

eden preprocess --task <name> — build the task's dataset cache.

Walks the task's motion_options tree, instantiates every configured DatasetBase once, and triggers the prepare() + save_cached() chain. Use this to pre-warm the v2 mmap cache before launching training, especially when the modifier stack includes expensive offline steps like SmoothStartTransition (per-clip IK).

For libraries large enough that single-process IK preprocessing is too slow, pass --num_batches N --batch_idx K and launch one process per GPU (CUDA_VISIBLE_DEVICES=0 ... batch_idx=0 ... &); each process handles a contiguous slice of the clip list. Caches land at processed_data_path-keyed paths (one per batch when num_batches > 1).

Source code in eden/utils/cli/tasks.py
def preprocess_task(extra_args: list[str]) -> None:
    """``eden preprocess --task <name>`` — build the task's dataset cache.

    Walks the task's ``motion_options`` tree, instantiates every configured
    ``DatasetBase`` once, and triggers the ``prepare()`` + ``save_cached()``
    chain. Use this to pre-warm the v2 mmap cache before launching training,
    especially when the modifier stack includes expensive offline steps like
    ``SmoothStartTransition`` (per-clip IK).

    For libraries large enough that single-process IK preprocessing is too
    slow, pass ``--num_batches N --batch_idx K`` and launch one process per
    GPU (``CUDA_VISIBLE_DEVICES=0 ... batch_idx=0 ... &``); each process
    handles a contiguous slice of the clip list.  Caches land at
    ``processed_data_path``-keyed paths (one per batch when ``num_batches > 1``).
    """
    from eden.tasks.parser import get_task_argparser, get_task_config_from_args

    parser = get_task_argparser(
        description="Build the v2 dataset cache for a registered task.",
        argv=extra_args,
    )
    parser.add_argument(
        "--num_batches",
        type=int,
        default=1,
        help="Split the clip list into this many contiguous batches. >1 enables "
        "per-process sharding; launch one process per GPU.",
    )
    parser.add_argument(
        "--batch_idx",
        type=int,
        default=0,
        help="Which batch (0-based, < --num_batches) this process owns. Default: 0.",
    )
    args = parser.parse_args(extra_args)
    _validate_task_arg(args, parser)
    if args.num_batches < 1:
        parser.error("--num_batches must be >= 1.")
    if args.batch_idx < 0 or args.batch_idx >= args.num_batches:
        parser.error(f"--batch_idx must be in [0, {args.num_batches}).")

    import eden as en
    import genesis as gs

    # GPU by default: preprocessing runs Genesis FK / IK over one env per clip,
    # which is intractable on CPU at multi-thousand-clip scale (the docstring's
    # per-GPU sharding recipe assumes a CUDA backend). --cpu opts out.
    en.init(backend=gs.cpu if args.cpu else gs.gpu, performance_mode=False)

    config = get_task_config_from_args(args, run_name=args.run_name)
    motion_options = getattr(config, "motion_options", None)
    if motion_options is None:
        parser.error(f"Task '{args.task}' has no motion_options — nothing to preprocess.")
        return  # for type-checkers; parser.error exits

    # Walk all configured MotionTerm entries; each carries a dataset_options.
    dataset_options_list = []
    extras = getattr(motion_options, "model_extra", None) or {}
    for term_options in extras.values():
        dataset_options = getattr(term_options, "dataset_options", None)
        if dataset_options is not None:
            dataset_options_list.append(dataset_options)

    if not dataset_options_list:
        en.logger.warning(
            f"Task '{args.task}' has motion_options but no MotionTerm declares "
            f"a dataset_options — nothing to preprocess."
        )
        return

    import torch

    # Importing eden.datasets.base first executes the eden.datasets package
    # __init__, which imports every dataset module and populates DATASET_REGISTRY.
    from eden.datasets.base import DATASET_REGISTRY

    for dataset_options in dataset_options_list:
        dataset_cls = DATASET_REGISTRY.get(dataset_options.name)
        en.logger.info(f"Preprocessing dataset '{dataset_options.name}'...")
        ds = dataset_cls(options=dataset_options, device=torch.device("cpu"))

        if args.num_batches > 1:
            # Split the loaded clip list into N contiguous slices and keep
            # only the requested one. ``select_trajectories`` rebuilds the
            # per-key BatchMotionSequence in place.
            n_clips = len(ds)
            base = n_clips // args.num_batches
            rem = n_clips % args.num_batches
            start = args.batch_idx * base + min(args.batch_idx, rem)
            end = start + base + (1 if args.batch_idx < rem else 0)
            keep_idx = list(range(start, end))
            if not keep_idx:
                en.logger.warning(
                    f"Batch {args.batch_idx}/{args.num_batches} is empty (library has {n_clips} clips); skipping save."
                )
                continue
            en.logger.info(f"Batch {args.batch_idx}/{args.num_batches}: clips [{start}, {end}) out of {n_clips}")
            ds.select_trajectories(keep_idx)
            # Save to a batch-suffixed path so multiple processes don't race
            # on the same file. Read off the instance, not the options: a dataset
            # may derive its own default cache path (BvhCsvG1Dataset keys one by
            # the settings that shape the prepared motion), and that derived path
            # is the one ``_initialize`` will look for on next load.
            if ds.processed_data_path:
                suffix = f".batch_{args.batch_idx}_of_{args.num_batches}"
                root, ext = os.path.splitext(ds.processed_data_path)
                batch_path = f"{root}{suffix}{ext or '.pt'}"
                ds.save_cached(batch_path)
                en.logger.info(f"Saved batch cache to {batch_path}")
        elif ds.processed_data_path:
            # Single-process path saves to the canonical location (which is what
            # the dataset's ``_initialize`` will look for on next load).
            ds.save_cached(ds.processed_data_path)

smoke_task

smoke_task(extra_args: list[str]) -> None

Build a registered task and step it a few times with no action (end-to-end smoke check).

Source code in eden/utils/cli/tasks.py
def smoke_task(extra_args: list[str]) -> None:
    """Build a registered task and step it a few times with no action (end-to-end smoke check)."""
    from eden.tasks.parser import get_task_argparser, get_task_config_from_args

    parser = get_task_argparser(
        description="Build a task by name and step it a few times — a one-call end-to-end sanity check.",
        argv=extra_args,
    )
    parser.add_argument("--num_envs", type=int, default=2, help="Number of parallel envs to build (default: 2).")
    parser.add_argument("--steps", type=int, default=2, help="Number of no-action steps to run (default: 2).")
    parser.add_argument("--eval_mode", action="store_true", help="Build in eval mode (training/randomization off).")
    parser.add_argument("--show_viewer", action="store_true", help="Open the Genesis viewer.")
    parser.add_argument(
        "--profile",
        action="store_true",
        help="Profile per-term compute cost and write term_cost.json (instead of a plain sanity step).",
    )
    parser.add_argument(
        "--profile_warmup", type=int, default=10, help="Warmup steps before measuring, with --profile (default: 10)."
    )
    parser.add_argument("--profile_steps", type=int, default=50, help="Measured steps, with --profile (default: 50).")
    args = parser.parse_args(extra_args)
    _validate_task_arg(args, parser)
    if args.num_envs <= 0:
        parser.error("--num_envs must be a positive integer.")
    if args.steps <= 0:
        parser.error("--steps must be a positive integer.")
    if args.profile and (args.profile_warmup < 0 or args.profile_steps <= 0):
        parser.error("--profile_warmup must be >= 0 and --profile_steps must be a positive integer.")

    import eden as en
    import genesis as gs

    en.init(
        backend=gs.gpu if not args.cpu else gs.cpu,
        log_root_path=f"logs/smoke/{args.task}",
        performance_mode=True,
    )

    config = get_task_config_from_args(args, run_name=args.run_name)

    if args.profile:
        from eden.utils.profiling import run_smoke_profile

        report, path = run_smoke_profile(
            config,
            num_envs=args.num_envs,
            warmup=args.profile_warmup,
            measure=args.profile_steps,
            eval_mode=args.eval_mode,
        )
        print(report.render())
        print(f"📊 wrote {path}")
        return

    from eden.utils.smoke import build_and_step

    build_and_step(
        config,
        num_envs=args.num_envs,
        num_steps=args.steps,
        eval_mode=args.eval_mode,
        show_viewer=args.show_viewer,
    )
    print(f"✅ smoke OK: built '{args.task}' with {args.num_envs} env(s) and ran {args.steps} step(s).")

See also