-
Notifications
You must be signed in to change notification settings - Fork 389
[logging] Publish the training loop phase as a Prometheus gauge #1923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
pcmoritz
merged 6 commits into
NovaSky-AI:main
from
eicherseiji:seiji/training-phase-gauge
Jul 24, 2026
+130
−12
Merged
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
65af227
[logging] Publish the training loop phase as a Prometheus gauge
eicherseiji 2713e30
[logging] Phase gauge review pass
eicherseiji 803032e
[logging] Drop the enable_training_phase_gauge toggle
eicherseiji da400b4
[logging] Drop the generating phase, unify Timer and phase, rename to…
eicherseiji 105fc01
Merge remote-tracking branch 'origin/main' into seiji/training-phase-…
eicherseiji fde6ff2
Merge remote-tracking branch 'origin/main' into seiji/training-phase-…
eicherseiji File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| """Publishes the training loop's current macro-phase to Prometheus via ``ray.util.metrics``. | ||
|
|
||
| Ray exports these to the same Prometheus that scrapes node GPU metrics, under the ``ray_`` prefix, | ||
| so loop phase joins GPU utilization on one wall-clock axis and survives a cluster restart, e.g. | ||
|
|
||
| avg(ray_node_gpus_utilization) and on() (ray_skyrl_training_phase{phase="eval"} == 1) | ||
| """ | ||
|
|
||
| from contextlib import contextmanager | ||
|
|
||
| from loguru import logger | ||
|
|
||
| # Macro-phases of one async training step. Names match the paired Timer keys so the wandb timing/* | ||
| # keys and the Prometheus phase label share one vocabulary. "generating" is the default between | ||
| # blocks, when the trainer is not blocking and generation proceeds in the background. | ||
| PHASES = ( | ||
| "generating", | ||
| "wait_for_generation_buffer", | ||
| "convert_to_training_input", | ||
| "run_training", | ||
| "sync_weights", | ||
| "eval", | ||
| "save_checkpoints", | ||
| ) | ||
|
|
||
|
|
||
| class TrainingPhaseGauge: | ||
| """Sets ``skyrl_training_phase{phase=...}`` to 1.0 for the active phase and 0.0 for the rest. | ||
|
|
||
| Best-effort: silently no-ops when Ray metrics are unavailable, so it is always safe to | ||
| construct and call. | ||
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
| self._gauge = None | ||
| self._current = "generating" | ||
| try: | ||
| from ray.util.metrics import Gauge | ||
|
|
||
| self._gauge = Gauge( | ||
| "skyrl_training_phase", | ||
| description="1.0 for the active training-loop macro-phase, 0.0 otherwise.", | ||
| tag_keys=("phase",), | ||
| ) | ||
| # Seed every series once so PromQL selectors never hit a missing series. | ||
| for phase in PHASES: | ||
| self._gauge.set(1.0 if phase == self._current else 0.0, tags={"phase": phase}) | ||
| except Exception as e: | ||
|
eicherseiji marked this conversation as resolved.
Outdated
|
||
| logger.warning(f"TrainingPhaseGauge disabled ({e}); the training-phase metric will not be published.") | ||
| self._gauge = None | ||
|
|
||
| def set_phase(self, name: str) -> None: | ||
| if name not in PHASES: | ||
| logger.warning(f"TrainingPhaseGauge: unknown phase {name!r}; keeping {self._current!r}.") | ||
| return | ||
| prev, self._current = self._current, name | ||
| if self._gauge is None or prev == name: | ||
| return | ||
| try: | ||
| self._gauge.set(0.0, tags={"phase": prev}) | ||
| self._gauge.set(1.0, tags={"phase": name}) | ||
| except Exception: | ||
| pass | ||
|
|
||
| @contextmanager | ||
| def phase(self, name: str): | ||
| """Mark ``name`` active for the duration of the block, restoring the prior phase on exit.""" | ||
| prev = self._current | ||
| self.set_phase(name) | ||
| try: | ||
| yield | ||
| finally: | ||
| self.set_phase(prev) | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """ | ||
| uv run --isolated --extra dev --extra skyrl-train pytest tests/train/utils/test_phase_metrics.py | ||
| """ | ||
|
|
||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| from skyrl.train.utils.phase_metrics import PHASES, TrainingPhaseGauge | ||
|
|
||
|
|
||
| def _make_gauge(): | ||
| """Build a TrainingPhaseGauge over a mock ray Gauge; return (obj, {phase: latest value}).""" | ||
| values = {} | ||
| mock_gauge = MagicMock() | ||
| mock_gauge.set.side_effect = lambda v, tags: values.__setitem__(tags["phase"], v) | ||
| with patch("ray.util.metrics.Gauge", return_value=mock_gauge): | ||
| obj = TrainingPhaseGauge() | ||
| return obj, values | ||
|
|
||
|
|
||
| def _active_phase(values): | ||
| # every phase series exists (seeded at construction) and exactly one is 1.0 | ||
| assert set(values) == set(PHASES) | ||
| active = [p for p, v in values.items() if v == 1.0] | ||
| assert len(active) == 1, f"expected exactly one active phase, got {active}" | ||
| return active[0] | ||
|
|
||
|
|
||
| def test_construction_defaults_to_generating(): | ||
| _, values = _make_gauge() | ||
| assert _active_phase(values) == "generating" | ||
|
|
||
|
|
||
| def test_set_phase_marks_exactly_one_active(): | ||
| obj, values = _make_gauge() | ||
| obj.set_phase("run_training") | ||
| assert _active_phase(values) == "run_training" | ||
| obj.set_phase("eval") | ||
| assert _active_phase(values) == "eval" | ||
|
|
||
|
|
||
| def test_phase_context_manager_restores_prior_phase(): | ||
| obj, values = _make_gauge() | ||
| obj.set_phase("run_training") | ||
| with obj.phase("save_checkpoints"): | ||
| assert _active_phase(values) == "save_checkpoints" | ||
| # restored to whatever was active before the block, not hard-coded to a default | ||
| assert _active_phase(values) == "run_training" | ||
|
|
||
|
|
||
| def test_unknown_phase_is_rejected(): | ||
| obj, values = _make_gauge() | ||
| obj.set_phase("run_training") | ||
| obj.set_phase("bogus_phase") | ||
| assert _active_phase(values) == "run_training" | ||
|
|
||
|
|
||
| def test_disabled_when_ray_metrics_unavailable(): | ||
| # Gauge() raising must not propagate | ||
| with patch("ray.util.metrics.Gauge", side_effect=RuntimeError("ray not initialized")): | ||
| obj = TrainingPhaseGauge() | ||
| obj.set_phase("run_training") | ||
| with obj.phase("eval"): | ||
| pass | ||
|
eicherseiji marked this conversation as resolved.
Outdated
|
||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is currently no equivalent to
generatingin the wandbtiming/*metrics, right? Is there any way to align the metrics better?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah true; fixed. Dropped
generating. In fully async RL it doesn't really align well.