Tensor Runtime Production Guide¶
This guide is the reproduction and operations entry point for the scoped single-GPU G1 Motion Tracking / FlashSAC / MJWarp tensor runtime. It covers the canonical owner-routed long soak, runtime artifacts, and failure triage. The settings contract is described separately in Tensor runtime; the generated Support Matrix is the authority for every backend, task, entrypoint, and platform support claim.
The production path is deliberately narrow:
Linux with one NVIDIA CUDA GPU;
FlashSAC;
g1_motion_tracking;the
mjwarpowner selected through the unified task-owner route;a tensor-native Manager runtime.
It is not a multi-GPU guide, does not publish packages, and does not generalize to every MJWarp task. macOS and ROCm/HIP do not provide a fallback for this device-resident lifecycle; invalid requests fail before backend construction.
Owner Selection and Effective Combinations¶
Use --algo, --task, and --sim together. They compose the owner YAML under
src/unilab/conf/<algo>/task/<task>/<sim>.yaml. That owner, rather than the CLI
spelling alone, sets training.sim_backend; tensor execution is a Manager invariant.
For the canonical combination:
uv run train --algo flashsac --task g1_motion_tracking --sim mjwarp
the MJWarp owner inherits the MuJoCo FlashSAC owner and resolves to:
Effective setting |
Value |
|---|---|
Task semantic name |
|
Backend identity |
|
Tensor runtime |
invariant of the Manager lifecycle |
Inference slots |
|
Replay-ingress depth |
|
Replay-ingress slot rows |
|
Collector metric interval |
|
CUDA process sharing |
|
Scene |
|
Motion |
|
The MuJoCo owner uses the same 2048-environment training budget, but publishes collector metrics every step. The MJWarp task is also a DR-free lifecycle baseline: its selected-row tensor reset does not negotiate reset randomization or interval wrenches.
At the current generated evidence grade, FlashSAC /
g1_motion_tracking / MJWarp is Configured. A successful local soak does not
silently promote it to Benchmarked or Recommended; support promotion is a
separate repository decision made through the support-matrix generator and its
validation inventory.
Reproducible Runtime¶
The gated profile resolves UniSim and MJBatch from the committed
registry lock files (the RL stack ships in-tree under unilab.rl):
UV_FROZEN=1 uv sync --extra mujoco --extra mjwarp
make setup is useful for broad repository development, but it does not install
the mjwarp extra. The command above matches the gated CUDA CI profile. CI
uses Python 3.11 as the reference version even though the package supports a
wider Python range.
On a host with one visible GPU, no CUDA_VISIBLE_DEVICES mask is required.
The automatic single-device topology binds the learner, collector, MJWarp
physics, inference ring, and replay ingress to the same current CUDA device.
Set the variable only to select one physical GPU on a multi-GPU host or to
preserve a launcher/debug/MPS mask:
export CUDA_VISIBLE_DEVICES=<single-host-cuda-ordinal>
All CUDA ordinals inside trainer processes are relative to that mask. Do not add more devices: M11 is single-GPU only.
CUDA MPS Execution Sharing¶
training.cuda_process_sharing is an explicit execution-sharing mode for the
already-required rank-local topology. It is not a backend switch and does not
replace --sim mjwarp or owner YAML selection.
The default remains null: learner and collector keep independent CUDA contexts.
The supported initial topology is Linux, NVIDIA CUDA, one physical GPU, MJWarp,
SAC/FlashSAC, and world_size=1. Request shared GPU execution with:
training.cuda_process_sharing=mps
Managed daemon lifecycle¶
UniLab now provides an explicit user-owned lifecycle command, while training itself still never starts or stops a host service implicitly:
On a single-GPU host, the defaults are already complete:
uv run uni-cumps start
uv run uni-cumps doctor
uv run uni-cumps stop
After start, a trainer request using training.cuda_process_sharing=mps
selects the sole live UniLab-recorded daemon covering its GPU. Shell
integration is optional:
uv run uni-cumps start
uv run --extra mjwarp train --algo flashsac \
--task g1_motion_tracking --sim mjwarp \
training.cuda_process_sharing=mps
An explicit CUDA_MPS_PIPE_DIRECTORY in the caller environment still takes
precedence. If no matching live daemon exists, training fails closed before
environment/learner/collector construction and points to uni-cumps start.
Default values:
Setting |
Default |
|---|---|
GPU |
the sole visible physical GPU, resolved to its canonical UUID |
daemon name |
|
pipe directory |
|
log directory |
|
|
the sole live UniLab-recorded daemon |
If more than one GPU is visible, start and doctor require
--gpus <index-or-uuid>. Explicit overrides remain available for deployments
that need stable labels or service-owned paths:
uv run uni-cumps start \
--gpus <gpu-index-or-uuid> \
--name <daemon-name> \
--pipe-dir /absolute/path/mps/pipe \
--log-dir /absolute/path/mps/log
eval "$(uv run uni-cumps env --name <daemon-name>)"
uv run uni-cumps stop --name <daemon-name>
Read-only status works without arguments:
uv run uni-cumps status
status and doctor are read-only. start creates a user-owned daemon and a
record under the current user’s UniLab cache. stop uses only that recorded
ownership evidence: UID, host, control PID, and Linux process start-time. It
never sends quit to an unmanaged pipe. env prints exports for a launcher or
an explicit shell integration; it does not mutate UniLab’s parent process.
A daemon is not exclusively owned by a training run and may serve multiple clients. A stale record is quarantined before reuse, and logs are retained after stop.
Topology modes and current limits¶
The command reports three topology-mode names so future launch contracts remain additive:
Mode |
Meaning |
Initial status |
|---|---|---|
|
one task uses one physical GPU |
Implemented |
|
one task has one rank per GPU |
Parsed, fail-closed |
|
independent tasks are packed one per GPU |
Parsed, fail-closed |
Selectors resolve through canonical physical GPU UUIDs. MIG UUIDs are rejected
explicitly. A comma-separated multi-GPU request fails closed and references the
multi-GPU gate rather than silently choosing DP or task packing. all is useful
in read-only diagnostics but cannot start a daemon in this release.
Training still validates before environment probing, learner construction, and collector spawn; there is no silent multi-context fallback. The error names the first unmet prerequisite and points to the CLI lifecycle commands.
run_config.json records the configured owner value. A valid run’s
run_summary.json embeds runtime_manifest.cuda_process_sharing with the
configured/effective mode, learner and collector devices, physical UUID
evidence, control pipe, server PID, and validation state. An unconfigured
(null) run still records ambient host state on CUDA ranks — the raw
CUDA_MPS_PIPE_DIRECTORY value, the effective pipe directory and control-pipe
kind, the answering server PID, and best-effort learner/collector GPU UUIDs —
with ambient_server=true when an environment-level daemon answered at the
effective control pipe, so a transparent-MPS run is identifiable from its own
manifest. Ambient observation never fails closed; unresolvable facts record
null. The section is a runtime-manifest v1 producer diagnostic, not a
stable scalar contract.
MPS changes only GPU execution sharing. It does not change
env_steps_per_sync, learner/collector placement, or training semantics.
Multi-GPU DP remains unsupported until its separate gate and daemon topology
decision are completed. CUDA MPS remains host- and deployment-dependent: in
shared containers, multi-user hosts, or restricted runners, control may be
unavailable, and an explicit request fails closed rather than silently
degrading.
Restricted deployments that cannot use the UniLab CLI may still operate an
existing daemon manually with nvidia-cuda-mps-control; training validation
does not depend on which explicit deployment tool started it.
Check the runtime that will execute the benchmark:
uv run --no-sync python -c "import torch; print(torch.__version__, torch.version.cuda, torch.version.hip, torch.cuda.is_available(), torch.cuda.device_count(), torch.cuda.current_device())"
torch.version.hip must be None, CUDA must be available, and the visible
device count must be one.
Backend CUDA Graph Diagnostics¶
Training artifacts record the physics backend’s tensor runtime diagnostics.
TorchEnv.get_backend_runtime_diagnostics() (src/unilab/base/torch_env.py)
is the collection boundary: a public cold-path method that serializes the
backend’s SimBackend.get_tensor_runtime_diagnostics() (the unisim public
contract) into plain JSON. The off-policy collector
(unilab.rl.runtime.offpolicy) calls it at startup
and publishes the payload into runtime_manifest.backend_diagnostics; the PPO
training script (src/unilab/scripts/train_rsl_rl.py) writes the same payload
as a top-level backend_diagnostics key in run_summary.json because PPO has
no runtime manifest.
Payload shape:
"backend_diagnostics": {
"available": true,
"backend_type": "mjwarp",
"diagnostics": {
"cuda_graph": {"requested": true, "enabled": false, "disable_reason": "..."},
"selected_reset_sensor_refresh": {"requested": true, "enabled": true, "disable_reason": null},
"device_reset_randomization": {"requested": true, "enabled": true, "disable_reason": null}
}
}
requested: the optimization was requested (MJWarp always requests CUDA graph).enabled: the optimization is actually active (for example, fixed-address CUDA graph replay).disable_reason: the backend-owned fallback cause; always present and non-empty whenenabledis false, alwaysnullwhen true.
When the env or backend exposes no diagnostics, the payload is an explicit
unavailable marker, never a bare null:
"backend_diagnostics": {"available": false, "backend_type": "...", "unavailable_reason": "..."}
Like runtime_manifest.cuda_process_sharing, this section is a
runtime-manifest v1 producer diagnostic, not a stable scalar contract; the
runtime manifest schema (unilab.rl.logging.runtime_manifest_schema)
validates the section fail-closed when it is
present. The diagnostics are observability only: they do not change CUDA graph
enablement policy and are not an acceptance gate. They are a cold-path snapshot
taken at collector/env startup; they describe initialization, not per-step
behavior.
Observation |
First action |
|---|---|
|
Read |
|
Check |
Known limitation: unisim does not yet expose the CUDA graph version or eager-fallback event counts. The serializer forwards future dataclass fields automatically, so the section grows when unisim adds them.
Assets¶
G1 meshes and textures are downloaded from the asset hub. Pre-fetch them before an offline benchmark:
uv run --no-sync unilab-pull-assets --robot g1
The motion clip is also external. It is downloaded lazily on first use and must be present at:
src/unilab/assets/motions/g1/dance1_subject2_part.npz
For a completely offline run, first materialize the required motion files, then
set HF_HUB_OFFLINE=1. Do not set offline mode before the first
materialization.
Canonical Long Soak¶
The long soak is the canonical production benchmark. It launches the real owner-routed FlashSAC trainer, then monitors schemas, progress, CUDA state, processes, file descriptors, shared memory, inference flight, replay ingress, and shutdown.
Choose new absolute paths for the run directory and JSON artifact. Do not reuse a dirty run directory: the monitor uses file changes in that directory as its liveness signal.
uv run --no-sync scripts/benchmark/torch_env/g1_flashsac_soak.py \
--num-envs 1024 \
--iterations 65000 \
--min-duration-seconds 1800 \
--sample-interval-seconds 5 \
--startup-timeout-seconds 900 \
--stale-progress-seconds 300 \
--post-shutdown-grace-seconds 10 \
--extra-override algo.save_interval=10000 \
--log-dir /absolute/path/to/g1-flashsac-mjwarp-run \
--output /absolute/path/to/g1-flashsac-mjwarp-soak.json
The wrapper owns and rejects overrides for algo, task,
training.sim_backend, play mode, log directory, environment count, and
iteration count. It constructs the low-level owner route for
flashsac / g1_motion_tracking / mjwarp; users do not pass
training.sim_backend=mjwarp as an independent switch.
The M11 acceptance budget is intentionally distinct from the owner production default:
Budget |
Environments |
Iterations |
Use |
|---|---|---|---|
MJWarp owner default |
2048 |
25000 |
Owner training budget |
M11 long soak |
1024 |
65000 |
Lifecycle, resource, replay, and shutdown acceptance |
The wrapper also sets training.no_play=true, so a passing soak does not create
playback video or playback-driven ONNX output.
Required Artifacts¶
The run directory must contain:
run_config.json;run_summary.json;TensorBoard event files;
checkpoints at the selected
algo.save_interval;progress files consumed by the soak monitor.
run_summary.json must carry metric schema 1 and an embedded runtime-manifest
schema 1. The manifest records effective runtime limits, inference and tensor
memory budgets when a budget decision is made, the collector/backend device,
inference-flight state, replay-ingress state, shutdown diagnostics, and the
backend tensor runtime diagnostics (runtime_manifest.backend_diagnostics; PPO
runs carry the same payload as a top-level backend_diagnostics). See
Logging for the field and compatibility contract.
The soak JSON is self-contained monitor evidence with artifact schema 0.3.0.
A current-head pass requires:
Evidence |
Passing value |
|---|---|
Monitor status |
|
Trainer return code |
|
Run status |
|
Failure reason and classification |
both |
Final inference queue depth and publication lag |
both |
Replay final occupancy |
|
Replay dropped batches and early returns |
both |
Replay release sequence |
equal to published sequence |
Published sequence |
|
Shutdown classification |
|
Shutdown cleanup errors |
|
Post-shutdown residual processes |
|
resource_summary also reports peak process count, RSS, GPU memory, eventfds,
shared-memory FDs, replay high-water occupancy, inference event count, and
maximum in-flight requests. Peaks are expected; look for unbounded or sustained
growth across post-startup samples, not merely a nonzero maximum.
The artifact context records the UniLab commit and dirty state, GPU
UUID/name/driver, Python/Torch metadata, and CUDA_VISIBLE_DEVICES. Preserve
the JSON and run directory together. The repository intentionally does not
track these machine-generated artifacts.
Extract the last 20 schema-v1 TensorBoard windows after the run:
uv run --no-sync scripts/benchmark/rl/extract_offpolicy_metrics.py \
/absolute/path/to/g1-flashsac-mjwarp-run \
--last 20 \
--json
The helper fails closed on a missing or unsupported metric schema. Historical
unversioned event files require the explicit --legacy-unversioned mode and
are never an implicit fallback.
Historical Soak Evidence¶
The M11 lifecycle review reached a passing v6 long soak on a local RTX 5090: 65,000 iterations, 66,660,352 environment steps, 1987.55 seconds, no dropped replay batch, final replay occupancy zero, final inference queue/lag zero, a normal shutdown, and no residual process. Resource sampling showed bounded process, RSS, GPU-memory, eventfd, and shared-memory-FD behavior.
That v6 artifact is historical evidence, not a current-head benchmark claim. It
used soak artifact schema 0.2.0 and older UniLab, unilab-rl, and UniSim
commits before current schema-v1 consumer enforcement. Reproduce from the exact
checked-out head with the command above when current-head evidence is required.
Optional Phase-Local Probe¶
g1_flashsac_backend.py is a diagnostic probe, not the canonical end-to-end
benchmark. It deliberately excludes inference IPC, replay ingestion, learner
updates, and production Manager-Based dispatch.
uv run --no-sync scripts/benchmark/torch_env/g1_flashsac_backend.py \
--backends mjwarp,mujoco \
--num-envs 2048 \
--warmup 20 \
--iters 100 \
--acceptance \
--output scripts/benchmark/outputs/g1-flashsac-backend/current-host.json
Acceptance mode requires available nvidia-smi evidence and an idle GPU before
and after each isolated backend process. It also rejects Isaac worker profiler
environment variables.
The JSON schema is 0.3.0. Use
throughput_env_control_steps_per_s and synchronized phases.iteration_ms for
cross-backend comparison. Individual phase boundaries may be stream ordered, so
do not add phases into a new wall-clock total or treat one backend’s
update_state_ms as directly comparable to another backend family’s phase.
MuJoCo results also include the packed H2D/D2H inventory and transfer counters.
Troubleshooting¶
Symptom |
First action |
|---|---|
MJWarp import fails |
Re-run the explicit |
CUDA unavailable |
Check the driver/container runtime and the Torch sanity command; do not change the task owner to force a fallback. |
Wrong physical GPU |
Set |
CUDA IPC or worker mismatch |
Keep learner, collector, and payload on the same physical GPU. External Isaac workers inherit visibility but not host Python paths. |
Asset startup failure |
Pre-fetch G1 assets, verify the motion NPZ exists, and remove |
Startup timeout |
Inspect |
Stale progress |
Open the latest run files and console log; no file change for |
Replay backpressure or drops |
Read occupancy, high-water, waits, early returns, and dropped batches; do not shrink ingress slot rows without a workload benchmark. |
Nonzero final replay occupancy |
Keep the artifact and console log; this is a lifecycle/finalize failure even if the trainer otherwise appeared to finish. |
Eventfd/shared-memory growth |
Compare post-startup samples; bounded peaks are normal, sustained growth is a leak signal. |
Abnormal shutdown |
Use |
Residual process |
Keep the artifact, which records known PIDs and start ticks; do not reuse the GPU until those processes are explained and gone. |
Unsupported capability |
Read the exact fail-closed error and generated support matrix; there is no hidden NumPy or host-bridge fallback for a CUDA-only lifecycle. |
For full schema definitions and metric interpretation, see Logging and Tensor runtime.
Breaking Tensor Runtime Migration¶
env.tensor_runtime and env.tensor_runtime_device were removed without a
compatibility alias. The Manager runtime is tensor-native by construction:
placement is derived from the backend’s declared data plane and rank process
device. Owner YAMLs and runner overrides that previously set either field must
delete those keys. Unsupported tensor lifecycles fail during binding rather
than falling back to a NumPy wire.