Logging¶
The terminal panel and TensorBoard / W&B are not separate logging systems. An
algorithm runner submits each metric once to the same training logger. The terminal
refreshes on a fixed 2 Hz clock and shows a two-second sliding average;
training.logger=tensorboard (the default) or training.logger=wandb selects the
persistent backend, which keeps each unsmoothed iteration value.
This page first covers the log directory shared by all algorithms and the
Manager-Based reward metric contract, then documents the off-policy terminal used by
SAC / FlashSAC and APPO. Persisted backend tags follow the canonical schema
owned by unilab.rl.logging.metric_schema: millisecond timing fields end in _ms,
while Perf/learning_time, Perf/collection_time, and Perf/iteration_time are
seconds. A few terminal fields (Other, phase percentages, collector cycle total,
Rows/s) are derived in memory for the display and are intentionally not persisted.
Log Directory and Backend¶
Run training with the default TensorBoard backend:
uv run train --algo ppo --task go2_joystick_flat --sim mujoco
Run directories default to logs/<algo.algo_log_name>/<task>/ unless the selected
stack overrides training.log_root or training.log_dir:
Algorithm |
Log root |
|
|---|---|---|
PPO |
|
|
APPO |
|
|
SAC |
|
|
FlashSAC |
|
|
A run directory is named YYYY-MM-DD_HH-MM-SS_<sim_backend>, for example
2026-03-09_18-30-00_mujoco. Common artifacts include run_config.json,
run_summary.json, checkpoints, and play_video.mp4 when that run produced one.
Enable W&B with:
uv run train --algo ppo --task go2_joystick_flat --sim mujoco \
training.logger=wandb \
training.wandb_project=unilab
Shared fields include training.wandb_project, training.wandb_entity,
training.wandb_group, training.wandb_name, training.wandb_tags,
training.wandb_notes, and training.wandb_mode. ExperimentTracker writes
run_config.json and run_summary.json; RSL-RL PPO also connects its writer in W&B
mode. A MuJoCo play_video.mp4 is uploaded when the run produces one.
Manager-Based Reward Metrics¶
Manager-Based environments place structured metrics in info["log"]. The RSL-RL PPO
logger forwards slash-prefixed entries in that dictionary to TensorBoard / W&B and also
writes its own native aggregate training metrics. The reward-related keys have the
following contract:
Key |
Owner |
Meaning |
|---|---|---|
|
UniLab |
Per-term diagnostic for the current reward computation: the mean across environments of |
|
Native RSL-RL PPO logger |
Aggregate mean completed-episode return; use this for overall training progress |
|
Native RSL-RL PPO logger |
Aggregate mean completed-episode length |
|
UniLab |
Count of reset environments for which that termination term fired; this is a distinct diagnostic and is not a reward |
|
Removed |
Historical normalized cumulative per-term value; it is intentionally no longer emitted because it duplicated |
Historical TensorBoard event files are immutable, so runs made before this change can
still display Episode_Reward/<term>. New Manager-Based runs should not emit it.
Off-policy Terminal¶
The terminal has explicit width tiers:
Below 96 columns, the metrics / rewards / timing sections stack vertically and timing uses a single
Timingcolumn. Short headers are used, and reward peak precision is reduced rather than truncating core labels.From 96 through 119 columns, metrics and rewards share one row and timing uses compact
Learner/Collectorheaders.At 120 columns and above, timing uses the full three columns:
Learner (Iter Wall)is one learner-main-thread iteration; every percentage usesIter Wallas its denominator.Collector (own clock)is measured in the collector subprocess and runs in parallel with the learner.Systemcontains the buffer size, timeout rate, environment count, per-rank batch size, and the rank-local CUDA MPS status (onfor a validated request,ambientfor a transparent environment-level daemon,offotherwise).
Metric rendering separates semantic groups: losses and policy state use stable short
labels, training diagnostics are indented, and replay-ingress gauges render as
Ingress Backpressure, Ingress Depth, and related labels. They are not placed in
the loss group and are distinct from the learner timeline’s Collector Wait.
The panel-border title reports GPUs N. In multi-GPU training, only rank 0 owns the
terminal and persistent logger. Learner metrics and timings are averaged across
ranks first and then across the terminal’s two-second window. Steps/s and
Rows/s are exceptions to the rank average: per-rank collector step rates and
learner replay-row rates are summed, so both fields are total job throughput. Only
Steps/s is persisted (as Perf/total_fps); Rows/s is terminal-only. The
Avg 2s (n=...) header field gives the number of already rank-reduced learner
samples in the current time window.
Do not add times across the learner and collector columns. Only the learner rows from
Collector Wait through Other are mutually exclusive main-thread phases. The
implementation fills unnamed intervals with Other = max(Iter Wall - accounted, 0).
Normally the rows sum to Iter Wall, so their displayed percentages sum to about
100% (per-row rounding can introduce a small difference). Algorithm-specific phases
neither occupy terminal rows nor get persisted for other algorithms; for example,
Replay Stage and Weight Publish exist only for APPO.
Learner Main Timeline¶
Terminal field |
TensorBoard / W&B key |
Path |
Meaning |
|---|---|---|---|
Collector Wait |
|
All |
Learner-main-thread wait to reach this iteration’s update boundary; SAC-like paths serve inference requests and continue until replay is ready and the |
Inference |
|
Learner-owned inference |
Total wall time for observation H2D, actor forward, and action D2H; nested details are listed below |
Collector Release |
|
Learner-owned inference |
Publishing the action response token; normally short, while blocking means the response queue has not drained |
Replay Batch Wait |
|
Device replay |
Waiting for the prefetched device batch to finish ingress commit and gather; near zero on a prefetch hit |
Replay Stage |
|
APPO |
Sequentially materializing newly arrived NumPy rollouts from the ring into the learner staging pool; this is an exclusive learner-main-thread phase |
Replay Sample |
|
All |
Acquiring the ready batch; usually hot/cold swap plus views on CUDA, with possible slot-event waiting on MPS |
Learning |
|
All |
Learner update-phase wall time |
Weight Publish |
|
APPO |
Writing fresh actor / critic weights to shared memory |
Other |
terminal-only |
All |
Residual after subtracting the phases above from |
Iter Wall |
|
All |
Wall time from learner-loop iteration start through update completion; always shown as 100%; emitted only when the runner measured the complete iteration wall time |
Other and the per-row percentages are derived in memory for the terminal and are
not persisted as backend charts. Accounted time contains only the mutually exclusive
main-thread phases above, never nested or background work. Normally
accounted + other = 100%. If a clock anomaly or a future overlapping timer makes
accounted > Iter Wall, Other is clamped to zero and accounted exceeding 100% is a
contract violation to fix, not an interpretable parallel-work percentage.
Learner Nested and Background Diagnostics¶
The following keys are persistent-backend diagnostics, not additional Iter Wall
slices:
TensorBoard / W&B key |
Parent or execution thread |
Meaning |
|---|---|---|
|
Child of |
Observation copy from the shared CPU slot to the learner device |
|
Child of |
|
|
Child of |
Action copy into the shared CPU slot |
|
Replay ingress; CUDA daemon or MPS learner thread |
CPU-side duration of the latest transition-span submission into the authoritative device ring; it can occur inside any main phase and must not be added to learner percentages |
The three inference details should approximately compose Inference; small gaps come
from Python work between timers. Replay H2D Submit overlaps the learner timeline:
CUDA submits non-blocking copies from the replay_device_sync daemon, while MPS
advances them from existing learner calls. Use a trace for the real asynchronous GPU
copy / gather interval instead of treating submit wall time as extra iteration share.
Schema versions and compatibility¶
Metric metadata and runtime manifests are separate version namespaces. The producer
source of truth is unilab.rl.logging.metric_schema and
unilab.rl.logging.runtime_manifest_schema:
Namespace |
Current value |
Storage |
|---|---|---|
TensorBoard / W&B metric schema |
|
top level of |
Runtime process/device/IPC manifest |
|
embedded runtime manifest |
Long-soak monitor artifact |
|
soak JSON artifact |
The three values are intentionally independent: bumping the soak monitor artifact does
not change metric semantics, and a metric-schema change does not invalidate every
runtime-manifest field. The current soak monitor accepts only metric schema 1,
runtime-manifest schema 1, and soak artifact 0.3.0.
Runtime-manifest v1 freezes the public configuration and lifecycle fields already
consumed by UniLab: inference ring capacity, collector metric interval,
collector_backend_device, inference-flight counters, replay-ingress counters/timing,
and the CUDA inference IPC event count when that budget exists. collector_backend_device
is required for completed manifests and is null only when the backend has no
accelerator binding. inference_flight.max_* and
replay_ingress.high_water_occupancy have distinct window semantics: inference maxima
are since the previous successfully published collector report, while replay-ingress
high-water occupancy is cumulative for the run. All fields not listed as stable,
including runtime_manifest.shutdown, are experimental diagnostics in v1. One such
diagnostic is runtime_manifest.backend_diagnostics, the physics-backend tensor
runtime snapshot (per-optimization requested/enabled/disable_reason, including
CUDA graph state) serialized by TorchEnv.get_backend_runtime_diagnostics(); the
manifest schema validates the section fail-closed when it is present, but its
contents remain producer-owned. The PPO path has no runtime manifest, so its training
script writes the same payload as a top-level backend_diagnostics key in
run_summary.json. See Tensor Runtime Production Guide for the payload shape and
troubleshooting.
Soak artifact 0.3.0 adds two top-level abnormal-shutdown fields. On a passed
run, failure_classification is null and shutdown.classification must be
normal_completion with non-empty string owner and phase fields and no
cleanup errors. On a failed run, a valid producer classification
(learner_failure, collector_failure, backend_worker_failure,
timeout/stale_tick, external_cancellation, or unknown_failure) takes
precedence because it identifies the initiating owner; otherwise UniLab records
the monitor classification (for example timeout/stale_tick or
resource_leak). The top-level shutdown value is the raw host-only
runtime_manifest.shutdown mapping when available; it is null when teardown
could not produce diagnostics. Producer shutdown snapshots are best-effort:
they are appended to, and never replace, the original soak failure reason. The
runtime-manifest v1 shutdown mapping remains opaque so that this soak
artifact contract does not make producer-owned diagnostic extensions stable.
Within a major schema version, producers may add optional fields with schema, test, and documentation updates. A stable field cannot be renamed, removed, retyped, or changed in unit or null semantics without incrementing the schema version. Consumers reject missing and unsupported versions rather than guessing.
Historical event files are immutable. The only approved compatibility path is
extract_offpolicy_metrics.py --legacy-unversioned. It accepts only recognized
pre-schema legacy tags, requires that no canonical tag or schema stamp be present,
and is not a fallback for future unversioned logs. The executable DP scaling
benchmark generates and overwrites current runs, so it consumes schema-v1
canonical tags only and has no historical mode. Canonical tags, mixed
canonical/legacy files, or an unsupported metric schema version fail closed in
default metric readers.
Collector Timeline¶
SAC / FlashSAC record mutually exclusive hot-path phases per vectorized env tick. Terminal percentages use their sum (the collector cycle), which is derived in memory and not persisted as a backend chart:
Terminal field |
TensorBoard / W&B key |
Meaning |
|---|---|---|
Inference Request |
|
Publish observations / dones to the shared slot and notify the learner |
Learner Action Wait |
|
Barrier wall time from request publication until the learner publishes this tick’s action |
Env Step |
|
|
Replay Write |
|
Transition post-processing, packing, and bounded-ingress write |
The terminal keeps the core cycle rows above visible. Inference Request,
Transition Extract, Metrics Publish, and the nested Action Validate /
Apply Action / Action+Backend details are quiet diagnostics: they remain
canonical TensorBoard / W&B fields, but appear in the terminal only when they
reach at least 1% of the collector cycle. Backend Step, Update State, and
Reset Done follow the same materiality rule because they are nested diagnostics
rather than additional cycle slices. For an event-backed asynchronous backend
(mjwarp), the device physics wait surfaces at the first host read of the step
outputs — the update-state termination drain, or the autoreset done-mask gate
when update_state performs no host read — and is attributed to Backend Step,
so the row reports real device work instead of only the kernel-launch overhead.
Learner Action Wait is deliberately not named “Inference Wait”: it is not pure
inference latency. If the collector finishes Env Step + Replay Write and submits its
next request while the learner is still updating, the value includes the remaining
update, a small scheduling part of the next learner Collector Wait, and the next
Inference + Collector Release. A long value therefore agrees with parallel
execution: it means the collector reached the next barrier before the learner.
The collector active-throughput diagnostic (num_envs / (Inference Request + Env Step + Replay Write), intentionally excluding Learner Action Wait) is no
longer persisted as a backend chart; it can be derived from the canonical collector
timing fields and the run configuration when needed. The terminal Steps/s field
instead reports total synchronized collector throughput, persisted as
Perf/total_fps. The indented Env Step detail rows do not enter the cycle sum,
though their displayed percentages use the same collector-cycle denominator.
APPO has a different collection contract and reports:
Terminal field |
TensorBoard / W&B key |
Basis |
|---|---|---|
MLP Infer |
|
Per-step policy-inference EMA |
Env Step |
|
Single- |
Rollout Wall |
|
Whole |
These are not one percentage breakdown: the first two are per-step EMAs and Rollout Wall is a whole-rollout total, so the terminal shows milliseconds only. The backend
active-throughput diagnostic (num_envs * steps_per_env) / Rollout Wall is likewise
no longer persisted and can be derived from the fields above.
SAC Dual Timeline¶
With the default training.env_steps_per_sync=1, this sequence starts at learner
iteration k. Horizontal messages are synchronization points; the par branches are
the actual overlap:
sequenceDiagram
autonumber
participant C as Collector (CPU / NumPy Env)
participant I as Inference Slot + Queue
participant L as Learner Main Thread
participant D as Device Replay / CUDA Daemon
Note over C,L: iteration k starts with request(t) already published
C->>I: Inference Request(t)
I-->>L: Collector Wait ends
rect rgb(235, 245, 255)
L->>I: Inference: observation H2D
L->>L: learner.actor forward
L->>I: action D2H
L-->>C: Collector Release / response(t)
end
par Collector iteration t
C->>C: Env Step(t)
C->>D: Replay Write(t)
C->>I: Inference Request(t+1)
Note over C,I: Learner Action Wait(t+1) starts here
and Learner iteration k
L->>D: Replay Batch Wait + Replay Sample(k)
L->>L: Train(k): fixed updates_per_step
Note over D,L: replay-ingress H2D / gather may overlap Train in background
end
I-->>L: iteration k+1 Collector Wait ends
L->>I: Inference(t+1)
L-->>C: response(t+1), ending Learner Action Wait
The important relationships are:
Learner
Inference(t)is the final part of collectorLearner Action Wait(t); they are not expected to have equal duration.Env Step(t) + Replay Write(t)overlaps learner replay work andTrain(k).The next request may arrive before
Train(k)finishes, but the learner serves it only after the complete update.Learner Action Wait(t+1)can therefore be long while the next learnerCollector Waitremains near zero.Inference does not overlap learner updates; each tick uses a complete
policy_version.
APPO Dual Timeline¶
The APPO collector continuously produces rollouts while the learner consumes complete ring-buffer slots:
gantt
title One APPO learner iteration (schematic scale)
dateFormat x
axisFormat %S
section Collector (separate process)
rollout N+1: MLP Infer + Env Step + other collection work :active, c0, 0, 18000
section Ring Buffer
rollout N ready :milestone, r0, 0, 0
rollout N+1 ready :milestone, r1, 18000, 18000
section Learner (Iter Wall)
Collector Wait :done, l0, 0, 1000
Replay Stage : l1, 1000, 3000
Replay Sample : l2, 3000, 4000
Train :active, l3, 4000, 16000
Weight Publish :crit, l4, 16000, 18000
section Learner Total
Iter Wall : l5, 0, 18000
Collector rollout and learner Iter Wall are independent, overlapping timelines and
must not be added. Collector Wait is near zero when the ring already contains a
slot. Collector Rollout Wall may be greater than, equal to, or less than one learner
Iter Wall, depending on ring backlog and relative throughput.
Replay Stage is the main-thread work that inserts every newly arrived slot into the
staging pool. Replay Sample then combines a training batch from that pool. They are
adjacent, additive learner phases, not background H2D diagnostics.
Diagnosing Bottlenecks¶
Observation |
Direct meaning |
Check first |
|---|---|---|
Learner |
The collector’s data / request is not ready when the learner reaches iteration start |
Env step, transition post-processing, collector liveness, and IPC |
Collector |
Collector reaches the next barrier first and waits for update completion plus inference |
|
Learner |
The learner-owned action path itself is slow |
H2D / forward / D2H children |
Learner |
Device-replay prefetch misses the consumption point |
Ingress commit, side-stream gather, and GPU contention |
Collector |
Bounded-ingress write or transition post-processing slows down |
Exhausted ingress slots and delayed device commit |
|
Background submission diagnostic grew; it did not add the same wall time to the iteration |
GPU copy, ingress commit, and overlap in Perfetto |
Trace Options¶
Off-policy configs expose training.trace_enabled, training.trace_output_dir,
training.trace_thread_time, and training.trace_cuda_events. Scalar logs answer how
long each iteration took. Use the generated Perfetto timeline to determine whether
asynchronous H2D, device gather, collector, and learner work actually overlap.