-
Notifications
You must be signed in to change notification settings - Fork 313
[WIP] miles dashboard #1654
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
Draft
yueming-yuan
wants to merge
25
commits into
main
Choose a base branch
from
miles-dashboard
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
[WIP] miles dashboard #1654
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
fd19f5b
dashboard: metric store — JSONL schemas, write/load/follow
yueming-yuan 001b379
refactor: extract transport-free cores from dump/split/process helpers
yueming-yuan 60a6e1f
dashboard: dump reader join + dummy-dump fixture via real dumping pip…
yueming-yuan 421c6db
dashboard: reader views — summary/groups/step aggregates, L2 tokens, …
yueming-yuan 710896a
dashboard: FastAPI server, serve entry point, [dashboard] extra
yueming-yuan 5be8fe8
dashboard: training-dynamics SPA (L0 metrics / L1 rollout / L2 tokens)
yueming-yuan d7a36b6
dashboard: timeline read path — store queries + /api/timeline/*
yueming-yuan fb16d88
dashboard: efficiency timeline view + serve --demo
yueming-yuan 71ef755
dashboard: sglang scraper — engine metrics to EngineSample records
yueming-yuan 7f60e8a
dashboard: per-node GPU sampler
yueming-yuan 59db9cc
dashboard: collector hub — ingest, persist, scraper lifecycle, promet…
yueming-yuan 32b5878
timer: interval event sinks
yueming-yuan 44234fb
dashboard: backend init matrix + process hooks
yueming-yuan 182fcbe
dashboard: activate — registry entry, CLI args, core hooks, engine to…
yueming-yuan 669ef9b
dashboard: lane selection grammar, binary heatmap, outlier ranking
yueming-yuan 7adf8d6
dashboard: carpet view + lane selection UI
yueming-yuan 0db1f49
dashboard: columnar in-memory store for high-rate streams
yueming-yuan 0e33f81
dashboard: hourly partitions, lazy windowed reads, lane catalog, 4h w…
yueming-yuan 381cdc7
dashboard: 4h viewport clamp, windowed lazy fetches, default lane budget
yueming-yuan 16562cd
rollout: trajectory lifecycle probes
yueming-yuan d8d669e
dashboard: trajectory event sink + stream
yueming-yuan 6d38006
dashboard: staleness, turns and tool columns from the dump
yueming-yuan 4312a25
dashboard: trajectory views — batch anatomy + run-wide carpet
yueming-yuan 16a9fff
rollout: dump raw per-sample trajectories next to rollout data
yueming-yuan d918901
dashboard: conversation view from the trajectory dump
yueming-yuan 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
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 @@ | ||
| # miles dashboard | ||
|
|
||
| Training-dynamics and efficiency dashboards for miles runs, built on top of | ||
| `--dump-details` plus a live telemetry collector. | ||
|
|
||
| ## Collect | ||
|
|
||
| ```bash | ||
| python train.py ... \ | ||
| --dump-details /path/to/dump \ | ||
| --use-miles-dashboard \ | ||
| --use-rollout-entropy # optional: per-token entropy in the token view | ||
| ``` | ||
|
|
||
| `--use-miles-dashboard` adds a named collector actor on the driver node, one | ||
| NVML sampler per GPU node, per-rank phase interval sinks on the existing | ||
| `Timer` instrumentation, and an sglang engine-metric scraper. Everything is | ||
| appended under `{dump-details}/dashboard/` (append-only JSONL). Overhead on | ||
| the training path is a few milliseconds per step; all pushes are | ||
| fire-and-forget and a dead collector never affects training. | ||
|
|
||
| ## View | ||
|
|
||
| ```bash | ||
| pip install -e .[dashboard] # fastapi/uvicorn; already in the training image | ||
| python -m miles.dashboard.serve --dump-details /path/to/dump [--follow] [--port 7788] | ||
| ``` | ||
|
|
||
| Works on any machine that can see the directory (login node over NFS, the | ||
| training node itself); `--follow` tails a still-running job. Typical remote | ||
| usage is an SSH port-forward. Runs recorded *without* `--use-miles-dashboard` | ||
| still get the full training-dynamics views (metrics fall back to dump-derived | ||
| aggregates under the `dump/` namespace); only the timeline is absent. | ||
|
|
||
| Views: | ||
|
|
||
| - **metrics** — every logged metric plus `dump/*` per-step aggregates; click | ||
| a rollout-axis point to drill into that step. | ||
| - **timeline** — per-GPU lanes: phase band (rollout / train / update_weights, | ||
| hatched `train_wait` idle), NVML utilization, sglang engine overlay | ||
| (running requests, throughput, KV usage), bubble strip with click-to-zoom. | ||
| - **step drill-down** — per-trajectory table and scatter, GRPO group | ||
| degeneracy (`zero_std`), generation-time columns, eval tab. | ||
| - **token view** — per-token importance ratio / entropy / advantage strips | ||
| with loss-masked regions dimmed. | ||
|
|
||
| ## Develop | ||
|
|
||
| ```bash | ||
| python -m miles.dashboard.serve --demo # generated demo data, no cluster needed | ||
| python -m pytest tests/fast/dashboard/ -q | ||
| MILES_DASHBOARD_REALDATA_DIR=/path/to/real/dump python -m pytest tests/fast/dashboard/ -q | ||
| ``` | ||
|
|
||
| Design and implementation-plan documents live with the dashboard workspace | ||
| (`miles-dashboard-design.md`); the short version of the architecture: | ||
|
|
||
| ``` | ||
| producers (Timer sinks / rollout hooks / NVML samplers / sglang scraper) | ||
| -> DashboardCollector (named actor, driver node) [writes JSONL streams] | ||
| dump_details .pt files (existing dump path) [written by training] | ||
| -> serve.py: MetricStore + DumpReader -> FastAPI -> static SPA | ||
| ``` |
Empty file.
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,92 @@ | ||||||||
| """CLI arguments and configuration plumbing for the miles dashboard.""" | ||||||||
|
|
||||||||
| from __future__ import annotations | ||||||||
|
|
||||||||
| import logging | ||||||||
|
|
||||||||
| from miles.dashboard.collector import CollectorConfig | ||||||||
| from miles.dashboard.sglang_scraper import DEFAULT_METRIC_WHITELIST | ||||||||
|
|
||||||||
| logger = logging.getLogger(__name__) | ||||||||
|
|
||||||||
| # curated subset of args persisted into meta.json for the dashboard header | ||||||||
| _SNAPSHOT_KEYS = ( | ||||||||
| "wandb_group", | ||||||||
| "colocate", | ||||||||
| "num_gpus_per_node", | ||||||||
| "actor_num_nodes", | ||||||||
| "actor_num_gpus_per_node", | ||||||||
| "rollout_num_gpus", | ||||||||
| "rollout_num_gpus_per_engine", | ||||||||
| "rollout_batch_size", | ||||||||
| "n_samples_per_prompt", | ||||||||
| "hf_checkpoint", | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| def add_dashboard_arguments(parser) -> None: | ||||||||
| group = parser.add_argument_group("miles dashboard") | ||||||||
| group.add_argument( | ||||||||
| "--use-miles-dashboard", | ||||||||
| action="store_true", | ||||||||
| default=False, | ||||||||
| help="Collect dashboard telemetry (phases, GPU util, engine metrics) under {dump-details}/dashboard/. " | ||||||||
| "Requires --dump-details. View with `python -m miles.dashboard.serve`.", | ||||||||
| ) | ||||||||
| group.add_argument("--dashboard-flush-interval", type=float, default=5.0, help="collector disk flush cadence (s)") | ||||||||
| group.add_argument("--dashboard-gpu-sample-interval", type=float, default=1.0, help="NVML sampling cadence (s)") | ||||||||
| group.add_argument("--dashboard-sglang-scrape-interval", type=float, default=2.0, help="engine scrape cadence (s)") | ||||||||
| group.add_argument( | ||||||||
| "--dashboard-sglang-scrape-mode", | ||||||||
| type=str, | ||||||||
| choices=["auto", "router", "direct"], | ||||||||
| default="auto", | ||||||||
| help="auto scrapes {router}/engine_metrics, or each engine's /metrics under --use-miles-router", | ||||||||
| ) | ||||||||
| group.add_argument( | ||||||||
| "--dashboard-sglang-metrics", | ||||||||
| type=str, | ||||||||
| default=None, | ||||||||
| help="comma-separated override of the scraped sglang metric whitelist", | ||||||||
| ) | ||||||||
| group.add_argument( | ||||||||
| "--dashboard-forward-prometheus", | ||||||||
| action="store_true", | ||||||||
| default=False, | ||||||||
| help="also push dashboard gauges to the --use-prometheus collector for external Grafana", | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| def validate_dashboard_args(args) -> None: | ||||||||
| if not args.use_miles_dashboard: | ||||||||
| return | ||||||||
| assert args.dump_details is not None, ( | ||||||||
| "--use-miles-dashboard writes telemetry under {dump-details}/dashboard/ and the " | ||||||||
| "trajectory views read the rollout/train dumps, so --dump-details is required" | ||||||||
| ) | ||||||||
| if not args.use_rollout_entropy: | ||||||||
| logger.warning( | ||||||||
| "--use-miles-dashboard without --use-rollout-entropy: per-token entropy " | ||||||||
| "will be missing from the dashboard token view" | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| def collector_config_from_args(args, *, start_ts: float) -> CollectorConfig: | ||||||||
| if args.dashboard_sglang_metrics is not None: | ||||||||
| whitelist = tuple(m for m in args.dashboard_sglang_metrics.split(",") if m) | ||||||||
| assert whitelist, f"empty --dashboard-sglang-metrics: {args.dashboard_sglang_metrics!r}" | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
References
|
||||||||
| else: | ||||||||
| whitelist = DEFAULT_METRIC_WHITELIST | ||||||||
| snapshot = {key: getattr(args, key) for key in _SNAPSHOT_KEYS if hasattr(args, key)} | ||||||||
| return CollectorConfig( | ||||||||
| dashboard_dir=f"{args.dump_details}/dashboard", | ||||||||
| run_name=args.wandb_group or "miles-run", | ||||||||
| start_ts=start_ts, | ||||||||
| args_snapshot=snapshot, | ||||||||
| flush_interval_seconds=args.dashboard_flush_interval, | ||||||||
| gpu_sample_interval_seconds=args.dashboard_gpu_sample_interval, | ||||||||
| scrape_interval_seconds=args.dashboard_sglang_scrape_interval, | ||||||||
| scrape_mode=args.dashboard_sglang_scrape_mode, | ||||||||
| metric_whitelist=whitelist, | ||||||||
| forward_prometheus=args.dashboard_forward_prometheus, | ||||||||
| ) | ||||||||
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,171 @@ | ||
| """Dashboard tracking-backend internals and Ray lifecycle glue. | ||
|
|
||
| Call matrix of ``init_dashboard`` (design doc §6.1), reached through the | ||
| ``MilesDashboardBackend`` adapter in ``tracking_utils``: | ||
|
|
||
| - ``train.py`` driver (``primary=True``): builds the config, creates the | ||
| named ``DashboardCollector`` actor pinned to the driver node (same pattern | ||
| as ``prometheus_utils``), starts its flush loop, and spawns one | ||
| ``GpuSampler`` actor per GPU node. | ||
| - train actor main rank (``primary=False``): resolves the named actor so | ||
| ``dashboard_log`` works there. | ||
| - rollout manager (``primary=False, router_addr=...``): additionally attaches | ||
| a Timer phase sink for ``rollout`` / ``eval_rollout`` events. The router | ||
| itself is registered later through ``hooks.register_router`` — at | ||
| init_tracking time the router has not started yet. | ||
|
|
||
| All pushes are fire-and-forget; when the collector cannot be resolved the | ||
| module warns once and every later call is a no-op — the dashboard must never | ||
| take a training run down with it. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import time | ||
|
|
||
| from miles.dashboard import hooks | ||
| from miles.dashboard.args import collector_config_from_args | ||
| from miles.dashboard.collector import COLLECTOR_ACTOR_NAME | ||
| from miles.dashboard.logging_utils import RateLimitedWarner | ||
| from miles.dashboard.store import MetricsRecord, Role | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| GET_ACTOR_TIMEOUT_SECONDS = 60.0 | ||
| GET_ACTOR_INTERVAL_SECONDS = 2.0 | ||
|
|
||
| _handle = None | ||
| _is_primary = False | ||
| _resolution_failed = False | ||
| _warner = RateLimitedWarner(logger) | ||
|
|
||
|
|
||
| def init_dashboard(args, *, primary: bool = True, router_addr: str | None = None, **kwargs) -> None: | ||
| global _handle, _is_primary | ||
| import ray | ||
|
|
||
| if primary: | ||
| _is_primary = True | ||
| from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy | ||
|
|
||
| from miles.dashboard.collector import DashboardCollector | ||
|
|
||
| config = collector_config_from_args(args, start_ts=time.time()) | ||
| _handle = ( | ||
| ray.remote(DashboardCollector) | ||
| .options( | ||
| name=COLLECTOR_ACTOR_NAME, | ||
| # driver node, hard-pinned: the dump volume and the run's other | ||
| # observability live there, and driver death ends the job anyway | ||
| scheduling_strategy=NodeAffinitySchedulingStrategy( | ||
| node_id=ray.get_runtime_context().get_node_id(), soft=False | ||
| ), | ||
| ) | ||
| .remote(config, prometheus_handle_factory=_prometheus_factory if config.forward_prometheus else None) | ||
| ) | ||
| ray.get(_handle.ping.remote()) | ||
| _handle.start.remote() | ||
| logger.info( | ||
| "miles dashboard: telemetry -> %s | view live: python -m miles.dashboard.serve " | ||
| "--dump-details %s --follow --port 7788", | ||
| config.dashboard_dir, | ||
| args.dump_details, | ||
| ) | ||
| return | ||
|
|
||
| if resolve_collector() is None: | ||
| return | ||
| if router_addr is not None: | ||
| # the kwarg marks the rollout-manager process. It cannot be used for | ||
| # scraping: init_tracking runs before start_rollout_servers, so the | ||
| # address is still "http://None:None" here — the real registration | ||
| # happens via hooks.register_router once the router is up. | ||
| hooks.attach_phase_sink(_handle, Role.ROLLOUT_MANAGER) | ||
| hooks.attach_trajectory_sink(_handle) | ||
|
|
||
|
|
||
| def resolve_collector(): | ||
| """This process's collector handle, resolving the named actor on first | ||
| use (train ranks never run init_dashboard themselves). Returns None — | ||
| permanently, after one warning — when the actor cannot be found.""" | ||
| global _handle, _resolution_failed | ||
| if _handle is not None or _resolution_failed: | ||
| return _handle | ||
| import ray | ||
|
|
||
| deadline = time.monotonic() + GET_ACTOR_TIMEOUT_SECONDS | ||
| while True: | ||
| try: | ||
| _handle = ray.get_actor(COLLECTOR_ACTOR_NAME) | ||
| return _handle | ||
| except ValueError: | ||
| if time.monotonic() >= deadline: | ||
| logger.warning( | ||
| "dashboard collector actor not found after %.0fs; telemetry from this process is disabled", | ||
| GET_ACTOR_TIMEOUT_SECONDS, | ||
| ) | ||
| _resolution_failed = True | ||
| return None | ||
| time.sleep(GET_ACTOR_INTERVAL_SECONDS) | ||
|
|
||
|
|
||
| def current_collector(): | ||
| """The already-resolved handle, or None. Never blocks (hot paths).""" | ||
| return _handle | ||
|
|
||
|
|
||
| def dashboard_log(metrics: dict, *, step: int | None = None, step_key: str | None = None) -> None: | ||
| if _handle is None: | ||
| return | ||
| try: | ||
| _handle.push_metrics.remote( | ||
| MetricsRecord(ts=time.time(), step_key=step_key, step=step, metrics=_scalars_only(metrics)) | ||
| ) | ||
| except Exception: | ||
| _warner.warn("dashboard metric push failed; dropping this log call") | ||
|
|
||
|
|
||
| def finish_dashboard() -> None: | ||
| global _handle, _is_primary, _resolution_failed | ||
| hooks.detach_and_flush() | ||
| if _handle is not None and _is_primary: | ||
| import ray | ||
|
|
||
| try: | ||
| # synchronous: the final flush must land before the driver exits | ||
| ray.get(_handle.shutdown.remote(), timeout=30) | ||
| except Exception: | ||
| logger.warning("dashboard collector shutdown incomplete", exc_info=True) | ||
| ray.kill(_handle) | ||
| _handle = None | ||
| _is_primary = False | ||
| _resolution_failed = False | ||
|
|
||
|
|
||
| # ------------------------------ ray helpers --------------------------------- | ||
|
|
||
|
|
||
| def _prometheus_factory(): | ||
| """Resolves the miles prometheus collector from inside the dashboard | ||
| collector actor (its module-global handle is per-process).""" | ||
| import ray | ||
|
|
||
| from miles.utils.tracking_utils.prometheus_utils import _COLLECTOR_ACTOR_NAME | ||
|
|
||
| try: | ||
| return ray.get_actor(_COLLECTOR_ACTOR_NAME) | ||
| except ValueError: | ||
| return None | ||
|
|
||
|
|
||
| def _scalars_only(metrics: dict) -> dict: | ||
| """The JSONL store persists plain scalars; tracking payloads can carry | ||
| tensors or numpy values, which are converted or dropped here.""" | ||
| out = {} | ||
| for key, value in metrics.items(): | ||
| if isinstance(value, (int, float, str, bool)): | ||
| out[key] = value | ||
| elif hasattr(type(value), "__float__"): # numpy/torch zero-dim scalars | ||
| out[key] = float(value) | ||
| return out |
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.
Using
assertfor validating command-line arguments or function arguments is discouraged because assertions can be optimized away when Python is run with the-Oflag. Raising aValueErroris preferred for argument validation.References