[logging] Rollout time split + async-trainer GPU/phase/buffer observability#1900
Draft
eicherseiji wants to merge 5 commits into
Draft
[logging] Rollout time split + async-trainer GPU/phase/buffer observability#1900eicherseiji wants to merge 5 commits into
eicherseiji wants to merge 5 commits into
Conversation
… env.step Rollout timing is currently reported only as a total per trajectory (`generate/trajectory_completion_time_*`, added in NovaSky-AI#1804). That total cannot distinguish an engine-bound rollout from an environment-bound one, which is the first question to ask when GPU utilization sags during generation. Accumulate, per trajectory in `agent_loop`, the wall-clock time spent awaiting `inference_engine_client.generate()` and the time spent in `env.step()`, and report: generate/trajectory_llm_time_{mean,p90,max} generate/trajectory_env_time_{mean,p90,max} generate/frac_time_in_env `frac_time_in_env` is time-weighted (sum over sum) so long trajectories count proportionally rather than each trajectory contributing equally. The two components need not sum to `e2e_time`; the remainder is tokenization, chat-template rendering and other in-loop bookkeeping. Both are plumbed like the existing `e2e_time`: optional, and omitted entirely if any trajectory in the batch did not record them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… gauge, buffer depth Three observability additions to the fully-async RL trainer, all motivated by a post-mortem where reconstructing GPU idle time required correlating Prometheus (GPU/disk, wall-clock keyed) against W&B (phase timings, step keyed) by hand. 1. Wire RayGpuMonitor into the async loop. It is constructed in the base RayPPOTrainer and flushed in the base loop, but FullyAsyncRayPPOTrainer overrides train() and never started or flushed it -- so runs on the async trainer logged zero `ray/` GPU keys despite `enable_ray_gpu_monitor=True`. Start it at loop entry, flush per step into the committed payload, stop it in the finally. 2. Publish a `skyrl_training_phase` gauge via `ray.util.metrics` (skyrl/train/utils/phase_metrics.py). Ray exports these to the same Prometheus that scrapes node GPU metrics, so `avg(ray_node_gpus_utilization) by (Phase)` becomes a single-store query instead of a manual cross-tracker correlation -- and it works after a cluster restart, when the experiment tracker may be unreachable. Set at each existing Timer() phase boundary (waiting_for_buffer / converting / training / weight_sync / eval / checkpoint); best-effort, never raises. 3. Log `async/gen_buffer_qsize` and `async/gen_buffer_maxsize` at step start. Run-ahead depth was previously only in a tqdm postfix; as a metric it distinguishes a throughput-limited generator (buffer low) from a gate-limited one (buffer at the staleness-bounded maxsize). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Item 1's llm/env time split was computed only at the per-generate()
get_rollout_metrics call. Every logging path (async trainer, eval,
step-wise) re-aggregates through concatenate_generator_outputs, which had
no raw llm/env lists and fell back to the substring-based extra_keys
aggregator. There p90 and frac_time_in_env hit the sum() branch, so a
16-group mini-batch logged frac_time_in_env near 12 and p90 near 16x its
real value.
Store the raw per-trajectory llm/env lists on GeneratorOutput like
e2e_time, thread them through concatenate_generator_outputs with the same
step-wise last-step filter, and recompute all seven metrics there so the
extra_keys fallback leaves them alone.
Also in this commit:
- Extend test_llm_vs_env_time_split_metrics to assert the split on a
concatenated output, the path that actually broke.
- Fix the phase_metrics module docstring query. The gauge has a lowercase
`phase` tag and no shared label with the GPU metric, so
`avg(ray_node_gpus_utilization) by (Phase)` does not run; replace it
with a vector match against `ray_skyrl_training_phase{phase="eval"}`.
- Deduplicate the last-step filter and the frac-block array construction.
Add ScalarGauges, a best-effort ray.util.metrics scalar-gauge helper that
no-ops without Ray, like TrainingPhaseGauge. Publish the async run-ahead
buffer levels through it so buffer pressure sits in the same Prometheus
store as the training-phase gauge and node GPU metrics, not only in the
experiment tracker:
skyrl_gen_buffer_qsize completed groups buffered at step start
skyrl_gen_buffer_maxsize staleness-bounded buffer capacity
skyrl_mini_batch_size groups consumed per training step
skyrl_gen_group_keep_rate kept / (kept + dropped) for the last
mini-batch, the zero-variance drop rate
under sample_full_batch
Rename the run-ahead phrasing to plain buffer depth in the buffer-metric and gauge comments; no behavior change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds observability for the async RL trainer. After a run, working out where GPU time went meant lining up two places that do not share an axis: Prometheus, which holds GPU and disk metrics by wall-clock time, and W&B, which holds phase timings by training step. Four changes each fill part of that gap.
1. Split rollout time into engine wait vs
env.step()Adds
generate/trajectory_llm_time_{mean,p90,max},generate/trajectory_env_time_{mean,p90,max}, andgenerate/frac_time_in_env. Until now only the total per-trajectory time existed,trajectory_completion_time_*from #1804, which cannot tell whether a slow rollout is waiting on the inference engine or running the environment. That is the first thing you want to know for multi-turn and agentic envs.agent_loopadds up the two parts per trajectory. They are stored as raw per-trajectory lists onGeneratorOutput, the same ase2e_time, and re-aggregated inconcatenate_generator_outputs. The raw lists have to reach that step. Every logging path goes through it, and its fallback aggregator sums any key it does not recognize, which is right for a total but wrong for a p90 or a ratio.frac_time_in_envis env time over total busy time, so long trajectories weigh proportionally. The two parts need not add up toe2e_time. The rest is tokenization, chat-template rendering, and other in-loop work. Both are omitted if any trajectory in the batch did not record them.2. Fix:
RayGpuMonitornever ran on the async trainerRayGpuMonitorfrom #1712 is built inRayPPOTrainer.__init__and flushed by the base loop.FullyAsyncRayPPOTraineroverridestrain()and never called.start(),.flush(), or.stop(). So async runs logged noray/GPU keys at all, even thoughenable_ray_gpu_monitor=Trueby default. Confirmed on real runs. It now starts at loop entry, flushes each step into the logged payload, and stops in thefinally.3.
skyrl_training_phasegauge to PrometheusNew module
skyrl/train/utils/phase_metrics.py. It reports which phase the loop is in (waiting_for_buffer,converting,training,weight_sync,eval,checkpoint, orgenerating) throughray.util.metrics. Ray sends that to the same Prometheus that already scrapes node GPU metrics, under theray_prefix. Soray_skyrl_training_phase{phase="eval"} == 1marks the eval windows on the GPU timeline, and you can read GPU use per phase from one place. It also keeps working after a cluster restart, when the experiment tracker may be gone. The gauge is set at each existingTimer()boundary. It is best-effort and does nothing when Ray metrics are unavailable, so it never breaks training or tests.4. Log the rollout buffer depth
Generation and training run at the same time. Generators produce rollout batches into a buffer, and the trainer pulls
mini_batch_sizebatches per step. The buffer depth, how many batches are waiting, tells you which side is the bottleneck. Near zero means the trainer is waiting on generation. Near the cap means generation is paused at the staleness limit. Same idle-GPU symptom, opposite cause. This was only visible in a live tqdm bar before and then lost.Logs
async/gen_buffer_qsize, the current depth, andasync/gen_buffer_maxsize, the cap, to W&B each step. The same two values, plusskyrl_mini_batch_sizeandskyrl_gen_group_keep_rate, also go to Prometheus as gauges throughScalarGauges, so buffer depth sits next to the phase gauge and GPU metrics.keep_rateiskept / (kept + dropped)for the last batch, the share of rollouts not thrown out as zero-variance undersample_full_batch. A deep buffer with a low keep rate is mostly rollouts that will be dropped.Scope
Item 1 was the original scope. Items 2 through 4 grow it into a small async-observability PR. I can split them into a follow-up if preferred. A
skyrl_node_role{train|inference}gauge is left out because inference node ids are not cleanly reachable from the trainer. Noted as a follow-up.Tests
tests/train/utils/test_phase_metrics.py, new: one active phase at a time, context-manager restore, no-op when Ray is unavailable, and theScalarGaugescreate-once and disable-on-failure behavior.tests/train/generators/test_skyrl_gym_generator.py: addstest_llm_vs_env_time_split_metrics. It checks the split on a singlegenerate()output and on aconcatenate_generator_outputsresult, sofrac_time_in_envstays in range and p90 is not inflated on the path every logger uses.uv run --isolated --extra dev --extra skyrl-train pytest tests/train/utils/test_phase_metrics.py tests/train/generators/test_skyrl_gym_generator.pypasses, 36 tests. ruff and black clean on changed files.Trainer wiring for items 2 through 4 follows the base-trainer pattern and is covered by unit tests and lint. It is not run end-to-end here, which needs a GPU cluster.