Skip to content

[logging] Account for env setup and overhead in trajectory time#1925

Draft
eicherseiji wants to merge 9 commits into
NovaSky-AI:mainfrom
eicherseiji:seiji/trajectory-time-accounting
Draft

[logging] Account for env setup and overhead in trajectory time#1925
eicherseiji wants to merge 9 commits into
NovaSky-AI:mainfrom
eicherseiji:seiji/trajectory-time-accounting

Conversation

@eicherseiji

@eicherseiji eicherseiji commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Accounts for everything inside generate/trajectory_completion_time explicitly:

e2e = llm_time + env_time + env_setup_time + overhead_time

Stacked on #1900, which adds the llm/env split this PR extends. Until #1900 merges this diff includes its base; the reviewable delta is the last commit.

Two new bands:

  • generate/trajectory_env_setup_time_{mean,p90,max}: a direct bracket around skyrl_gym.make plus env.init. Both run inside the e2e clock but outside env_time, which only sums env.step(). For sandboxed tool envs setup can take seconds and was previously indistinguishable from tokenization overhead.
  • generate/trajectory_overhead_time_{mean,p90,max}: the derived remainder. Tokenization, chat templating, output assembly, env teardown (env.close() runs inside the e2e window but outside both env brackets), and event-loop scheduling. Computed only when all four raw lists are present, so a generator reporting a partial split never silently inflates it. With this band the stack sums to the trajectory's end-to-end time, so a growing unaccounted cost is visible instead of silent.

Both are computed on the per-generate() path and recomputed from raw per-trajectory lists in concatenate_generator_outputs, the path every logger uses.

The engine-side subdivision of llm_time (queue, prefill, decode per turn) intentionally stays out: vLLM already exports those per request to Prometheus, so they are a dashboard join, not new trainer fields.

Tests

  • test_llm_vs_env_time_split_metrics: the env now sleeps in init, and the test asserts env_setup_time catches it, overhead_time is non-negative, and llm + env + setup <= e2e per trajectory.
  • The step-wise structural test and the GeneratorOutput field guardrail cover the new field.
  • uv run --isolated --extra dev --extra skyrl-train pytest tests/train/generators/ passes, 49 tests. ruff and black clean.

eicherseiji and others added 9 commits July 14, 2026 20:38
… 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.
The async-trainer observability items bundled here (RayGpuMonitor
wiring, training-phase gauge, buffer depth) move to their own PRs so
each is a reviewable unit. This restores fully_async_trainer.py to base
and removes phase_metrics.py and its tests. The rollout LLM vs env time
split and its aggregation across concatenate_generator_outputs stay.
Review feedback pass:
- Add trajectory_llm_times/env_times to the GeneratorOutput field
  guardrail test, which this branch had broken.
- Extract _add_time_stats for the mean/p90/max blocks (completion, llm,
  env) and reuse its sums for frac_time_in_env.
- Extract _optional_times for the omit-if-any-None per-prompt lists.
- Use _concat_field for stop_reasons and rollout_logprobs too.
- Cut duplicated comments; the llm/env split was explained in six
  places, now documented once per surface.
- Assert the step-wise per-step replication of the split in the
  existing step-wise timing test.
Closes the trajectory-time stack so everything inside
trajectory_completion_time is explicit:

  e2e = llm_time + env_time + env_setup_time + overhead_time

env_setup_time brackets skyrl_gym.make plus env.init, which run inside
the e2e clock but outside env_time; for sandboxed tool envs setup can be
seconds and was indistinguishable from tokenization overhead.
overhead_time is the derived remainder: tokenization, chat templating,
output assembly, and event-loop scheduling. Emitted as
generate/trajectory_{env_setup,overhead}_time_{mean,p90,max} on both the
per-generate and concatenated paths.
- Require all four raw lists before computing overhead instead of
  zero-filling a missing env_setup list, which silently folded real
  setup time into overhead for any generator reporting only llm and env.
- Name env teardown in the overhead comment; env.close() runs inside the
  e2e window but outside the env_time and env_setup_time brackets.
- Vectorize the overhead computation to match the rest of the function.
- Assert setup_times length and the new bands on the concatenated path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant