feat(observability): combined host-attribution coverage (#356 + #413 + #415) + scraper label fix - #417
feat(observability): combined host-attribution coverage (#356 + #413 + #415) + scraper label fix#417nv-yna wants to merge 10 commits into
Conversation
…n metrics Extends the /proc host sampler from orchestrator-node-only to every allocated node, and adds the scheduler-level fields needed to attribute host-CPU interference to a remedy from a single baseline run: - per-process run_delay_ns (/proc/pid/schedstat): cumulative run-queue wait — the direct contention signal that CPU pinning remedies - per-process nr_migrations (/proc/pid/sched): cross-core churn, near-zero when pinned - per-process affinity_ncpus (sched_getaffinity): direct pinning-state observable (144 = floating, 36 = pinned rank on GB200/GB300) - host procs_running/procs_blocked and a t_mono companion timestamp (cross-node wall clocks have been observed seconds apart) Collection: host_sampler.py gains a stdlib-only standalone CLI mode (one file per node: host_samples_<node>.jsonl); benchmark_stage launches it on all non-orchestrator nodes via one srun --overlap per het group, gated by the new observability.host_sampler_all_nodes knob (default true, follows observability.enabled). This closes the gaps where worker nodes had no per-process host telemetry and a dedicated frontend node had none at all. Ingest: host_series.json gains a per-node hosts map plus run_delay_ms_per_s / migrations_rate / affinity_ncpus / procs_runnable series; rate denominators prefer the monotonic clock so NTP steps can't skew them. docs/host-attribution-metrics.md documents the metric set and the pinning-vs-placement attribution logic. Teardown hardening from adversarial review: benchmark proc creation moved inside the try so sampler sruns can't leak when placement/launch raises; sampler teardown escalates via terminate_and_reap and logs early-exited samplers; the standalone mode exits nonzero if its sampler thread dies; the sampler never samples itself. Signed-off-by: Yuewei Na <[email protected]>
…idation matrix Replaces the draft rubric with measured thresholds from 7 validation runs (2x baseline, 2x pinned, 3x dedicated-frontend at DSV4 c1010 on GB300): clean-node run-delay floor 0.00-0.01 ms/s; shared-node asymmetry 140-350x (persisting at 190x under pinning, collapsing to 1x in all three dedicated-frontend runs); affinity_ncpus as the direct pinning observable (full-width vs CPUs-per-GPU), unchanged by placement. Adds the operator three-step decision flow and expected-gain guidance (+4.0% pinning on 288-CPU nodes / +1.1% on 144-CPU; +0.5-0.85% placement). Signed-off-by: Yuewei Na <[email protected]>
…ault The Prometheus surface Tachometer captures says nothing about the processes underneath it: Dynamo publishes no process_* or thread metrics and node_exporter only sees the machine. The AgentX/VR200 frontend spin (fastokens 0.3.1 sizing its BPE rayon pool to all 352 CPUs -> 352 idle workers burning ~110 cores) was invisible in every scraped family and was found with sacct plus an out-of-band per-thread /proc sampler. Add ncabatoff/process-exporter (pinned multi-arch image, port 9256) as a third built-in exporter, launched on every node that hosts a backend rank OR a frontend replica -- the frontend node is the one the per-node exporters skip when the frontend is head-placed or dedicated, and it is where frontend CPU pathologies live. Groups come from <log_dir>/process-exporter.yml written at launch (frontend, dynamo_trtllm/sglang/vllm handlers + ranks, the MPI launcher, the benchmark client, infra daemons; first match wins, no catch-all). -threads=true publishes per-thread-name CPU and thread counts, so a runaway pool shows up as a step in thread_count and a CPU cluster on one thread name. Endpoints are scraped unfiltered so groupname/threadname/mode labels pass through to the parquet and to server_metrics_export.jsonl unchanged. node_exporter additionally gets --collector.processes (host-wide thread total, process states, procs_running/blocked). An explicit process_exporter block overrides the image/port/command, and default_exporters: false still disables all built-ins. Dry-run lists the new exporter; docs/config-reference.md documents it. Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Yuewei Na <[email protected]>
The tachometer node_exporter launch enabled only cpu/infiniband/meminfo, and
the host sampler read /proc/stat + /proc/meminfo but no PSI. The result: none
of the scheduler-pressure signal the retired steady_probe.sh sampler carried
(PSI stall time, procs_running/blocked, context-switch and page-fault/reclaim
counters, per-NUMA free memory) was collected anywhere -- exactly the class of
signal that distinguishes a busy box from one whose work is blocked waiting.
Two complementary changes, both cheap procfs/sysfs reads with no measured
decode-latency cost (unlike dense NVML sampling):
- node_exporter (all worker nodes): add the stat, vmstat, pressure, and
meminfo_numa collectors. The vendored NodeExporterFilter passes the new
families through its default arm, so no scraper change is needed. Extracted
into tachometer_node_exporter_command_template() mirroring the DCGM template;
an explicit recipe node_exporter.command still wins.
- host_sampler (orchestrator/head node, which gets no exporter): add a
/proc/pressure/{cpu,memory,io} PSI read to each sample. This is that node's
only PSI source and covers the frontend node the exporters never reach.
Tests: node-exporter template asserts the four new collectors and
explicit-command override; host_sampler PSI parse + CONFIG_PSI-absent cases.
Signed-off-by: Yuewei Na <[email protected]>
…-setup binary The upstream ncabatoff/process-exporter image is FROM scratch: no shell, no /root. pyxis/enroot on hecate refuses to start it (`enroot-switchroot: failed to change directory: /root`, then `/bin/sh: No such file or directory` once the home is mounted), so the first run from this branch (hecate 565854) had to relaunch the exporter by hand as a bare binary. The exporter is a static Go executable that needs neither a container nor privileges, so ship it the way nats-server and etcd are shipped: `make setup ARCH=<compute_arch>` downloads the release tarball into configs/process-exporter and start_tachometer runs it under plain srun with host paths for the binary and its group file. - TelemetryExporterConfig.binary: host-native launch mode. Relative paths resolve against SRTCTL_SOURCE_DIR / the checkout root, where make setup installs host binaries; container_image is ignored. - DEFAULT_PROCESS_EXPORTER now points at configs/process-exporter with an empty container_image. Validation requires binary or container_image for every exporter, process_exporter included. - Missing binary (checkout whose make setup predates this) skips the process-exporter leg with a warning instead of failing the run; `srtctl apply` warns at submit time as well. - An explicit process_exporter.container_image with binary unset keeps the container launch (group file via the /logs mount). - Makefile setup step (PROCESS_EXPORTER_VERSION ?= 0.8.7), .gitignore, dry-run row shows "host binary configs/process-exporter :9256", docs/config-reference.md, tests for both launch modes, the skip path, binary resolution and validation. Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Yuewei Na <[email protected]>
# Conflicts: # src/srtctl/cli/mixins/telemetry_stage.py
…xporterFilter
Two node_exporter families enabled by the combined observability branch lost
their only distinguishing label in the vendored scraper filter, so every series
of the family collapsed into one metric name in the parquet:
- node_memory_numa_*{node="N"} (--collector.meminfo_numa, NVIDIA#415): the generic
`memory_` arm emitted the bare metric name, folding all NUMA nodes together.
Now `memory_numa_<x>{numa_node=N}`; host-wide meminfo stays label-free.
- node_processes_state{state="R"|"S"|"D"|...} and
node_processes_threads_state{thread_state=...} (--collector.processes, NVIDIA#413):
the default arm keeps only a fixed label whitelist that had neither key.
`state` and `thread_state` are added to the whitelist.
Label-free stat/vmstat/pressure families were already passed through unchanged;
a test pins that too.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Yuewei Na <[email protected]>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #417 +/- ##
=======================================
Coverage ? 74.10%
=======================================
Files ? 101
Lines ? 14291
Branches ? 0
=======================================
Hits ? 10591
Misses ? 3700
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…docs/comments for the collector union Post-merge fixups from the three-PR integration review: - benchmark_stage._start_remote_host_samplers hardcoded `--interval 2` (NVIDIA#356 predates b83dc4a, which made the in-process sampler follow observability.tachometer.collect_interval_ms). Derive the remote interval from the same knob, clamped to >= 1 s like HostSampler, so head-node and remote host_samples rows share one cadence. Test covers default, 2 s, 5 s, sub-second clamp and 1.5 s. - docs/config-reference.md node_exporter row lists the eight collectors of the union and the widened vmstat field filter (it still named NVIDIA#413's four). - telemetry_stage.py NODE_EXPORTER_COLLECTORS comment no longer claims "no scraper change is needed": the filter keeps numa_node / state / thread_state since the previous commit. Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Yuewei Na <[email protected]>
First run from this branch: hecate 567529 (
|
567529 finished (COMPLETED 02:50:17, 2:04:18 of the 2:10 limit) — results and a cost observationCoverage held for the whole run. Final shard 101,125,428 rows, Control run on Cost of the collector union, measured against that
The extra volume is almost entirely |
567533 finished — after-fix arm on this branch (COMPLETED 04:16:11, 1:59:57 of 2:10; ingest 840.8 s; dashboard rendered in-job)Same recipe with
Client side: 41,897 tok/s vs 39,320 (+6.6 %), ITL avg 17.19 vs 18.30 ms, TTFT avg 9.80 vs 11.91 s. Cross-branch, same arm, same day: Both runs of this branch completed inside 2h10 with the render; the meminfo_numa volume note above still stands (17 and 14 min ingests vs 8.5–9.5 on |
What
Integration branch carrying the full host-attribution coverage of three open observability PRs on top of
main, plus the scraper fix that two of their metric families need to survive into the parquet:ncabatoff/process-exporteras a third built-in Tachometer exporter (host-native binary frommake setup), per-process CPU / thread count / per-thread-name CPU / ctx switches / RSS / fds for frontend, handlers, ranks, client;node_exporter --collector.processes.node_exporterscheduler-pressure collectors (stat,vmstatwithpgsteal,pressure,meminfo_numa) on the tachometer launch;psifield in the host sampler.observability.host_sampler_all_nodes, default on) with per-process schedstat (run_delay_ns,nr_migrations,affinity_ncpus) and hostprocs_running/procs_blocked; ingesthostsmap; attribution rubric doc.tachometer-scraperNodeExporterFilterfix: thememory_arm dropped every label, sonode_memory_numa_MemFree_bytes{node="N"}collapsed all NUMA nodes into one series (feat(observability): collect host scheduler-pressure metrics (PSI, procs, reclaim, per-NUMA) #415's per-NUMA metric never reached the parquet), and the default arm's label whitelist droppedstate/thread_state, sonode_processes_state{state="R"|"D"|...}(feat(observability): scrape a process exporter with tachometer by default #413) collapsed too. Nowmemory_numa_*{numa_node=N}andprocesses_state{state=X}/processes_threads_state{thread_state=X}. Three unit tests.Merge order
main← #413 ← #415 ← #356. One textual conflict, instart_tachometer: #413 appended--collector.processesto the inline node_exporter command, #415 replaced that literal withtachometer_node_exporter_command_template(). Resolved to the function withprocessesadded toNODE_EXPORTER_COLLECTORS:#356 merged clean (its host_sampler.py hunks and #415's
_pressure()do not overlap).Post-merge fixups (third commit): the remote host samplers from #356 hardcoded
--interval 2while the in-process sampler followsobservability.tachometer.collect_interval_mssince b83dc4a (after #356's base) — both now use the knob, clamped to ≥ 1 s; thenode_exporterrow indocs/config-reference.mdlists the eight collectors of the union; the collectors comment no longer claims that no scraper change is needed.Verified overlap table
The table circulated for these three PRs was checked row by row against the diffs (one reader per PR, then a synthesis and an adversarial pass; the adversarial pass upheld every reader verdict and found no unflagged overlap — pairwise file overlap is #356×#413 schema.py, #356×#415 host_sampler.py, #413×#415 telemetry_stage.py + tests/test_telemetry.py). Corrections are marked.
_pressure()only; no textual conflict with #356)/proc/stat--collector.processesexportsnode_processes_{threads,state}, notnode_procs_*)--collector.statobservability.enabledthe all-node sampler ships its pre-existing per-process cpu/threads/ctx/rss fields from every node, so coverage overlaps #413's)psi; the sampler field is written, not ingested)So "3× redundant" for
procs_running/blockedis 2× (sampler on every node via #356, node_exporter on worker nodes via #415); both are kept, they are cheap and cover different node sets.What runs in which mode
observability:block): tachometer + DCGM + node_exporter with the eight collectors above + host-native process-exporter on backend ∪ frontend nodes.observability.enabled: true: additionally the/prochost sampler on every node (schedstat, procs_running/blocked, PSI) and the span / request-trace legs. The sampler is gated onobservability.enabledin all three PRs; this branch does not change that.CONFIG_PSI(hecate's6.17.0-nvidia-64k); node_exporter simply omits the family.Validation
pytest tests/→ 1778 passed, 2 skipped (union of the three PRs' tests; exporter-count assertions still hold).cargo testinsrc/tachometer→ all green incl. the 3 new filter tests;cargo fmt --checkclean.ruff check: no new findings beyond those in the source PRs (PLW1510intests/test_host_sampler_all_nodes.py:52from feat(observability): host sampler on every node + scheduler-contention metrics #356) and pre-existing ones onmain.srtctl dry-runon hecate from this branch shows the union node_exporter command andprocess_exporter | host binary configs/process-exporter :9256.567529baseline arm and567533after-fix arm (FASTOKENS_BPE_THREADS=8), both COMPLETED inside 2h10 with the dashboard rendered in-job. Every family of the union reached the parquet with its labels (memory_numa_*{numa_node=N},processes_state{state=X},procs_running/blocked,vmstat_pgsteal_*;pressure_*absent on hecate's kernel), and the process exporter launched host-native on all nodes without manual steps. Frontend group: 83.5 avg cores / 168k ctx-switches/s baseline vs 2.44 cores / 5.5k/s with the fix; client throughput 39,320 vs 41,897 tok/s. Details and the cost table are in the comments below.Why draft
Integration branch: it should land only after (or instead of) its three sources; the filter fix could be split out to its own PR if reviewers prefer. Known gaps carried over from the sources: the sampler
psifield is not ingested; the scraper still writes nofinal.parquet.🤖 Generated with Claude Code