Skip to content

telemetry: native multi-process Prometheus aggregation (prometheus_mp) - #1920

Merged
e-eygin merged 76 commits into
ai-dynamo:mainfrom
e-eygin:eeygin/nix-1614-native-mp-prometheus-aggregation
Aug 6, 2026
Merged

e-eygin merged 76 commits into
ai-dynamo:mainfrom
e-eygin:eeygin/nix-1614-native-mp-prometheus-aggregation

Conversation

@e-eygin

@e-eygin e-eygin commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What?

Adds prometheus_mp, a native telemetry exporter plug-in that exposes the telemetry of all processes of a multi-process NIXL run (TP/DP/PP) behind a single Prometheus scrape endpoint — with no external dependency (no DOCA/DTS, not built on Python's prometheus_client).

Enable with NIXL_TELEMETRY_EXPORTER=prometheus_mp and point every rank at one shared directory via NIXL_TELEMETRY_MULTIPROC_DIR.

Why?

The existing native Prometheus exporter binds one scrape port per process. Under multi-process parallelism every rank creates its own agent and tries to bind the same port; only one wins, so every other rank's telemetry is lost by design (reported for a Dynamo + SGLang TP=8 deployment, issue #1838). The existing exporter can only make that collision benign (WARN and continue) — it can't recover the lost ranks.

This makes every rank's telemetry available behind one endpoint, natively — no external aggregator (the users run the native exporter specifically to avoid extra infrastructure) and no parent process to coordinate (NIXL is loaded independently per rank, so ownership is self-elected at runtime). It complements, and does not replace, the DOCA/CollectX exporter (which aggregates via an external service).

Tracking: #1838 (#1838 / NIX-1614).

How?

Every process writes its own metric state to a fixed-slot, memory-mapped file in the shared dir. One of them is elected to serve: the processes race for an exclusive flock on a lock file named after the address they were configured to serve, and only the winner (owner) binds the scrape port, running the Exposer plus a custom prometheus::Collectable that, on each scrape, reads a snapshot of every live peer and republishes it. The losers run writer-only and never bind, so losing is benign and no rank is dropped. Each process is its own series (no cross-process summing, so counters stay monotonic).

The owner is not a single point of failure: the kernel frees the lock when it dies, and a writer re-running the election takes the endpoint — and the reaping — over.

The exported metric set is at full parity with the single-process prometheus exporter: counters, last-operation gauges, error counters, and the transfer-duration histograms.

Design detail (election, crash-safety, histograms, labels, cleanup, config, limitations, tests)

Owner election & failover

The lock, not the bind, is what elects: two ranks binding concurrently cannot tell which of them got there first, so gating the bind on an exclusive lock is what makes exactly one process serve. That guarantee holds as long as the lock is usable — if the lock file cannot be opened, is not a regular file owned by the run's user, or sits on a filesystem without flock, every process warns and falls back to the port bind deciding.

Naming the lock file after the address keeps it contentless and scopes the election to the ranks that would actually collide, which turns two otherwise silent misconfigurations into warnings. An owner that cannot bind reports the port as held from outside the run (a foreign service, or a rank pointed at a different directory) and concedes the election rather than holding it, so the next rank to win takes the address over once the port frees. A directory served on more than one address means the ranks disagree on the port; each serves what it was configured with, but every one of them exports every rank, so a Prometheus scraping more than one target sees the same series twice. Owners find each other by trying the directory's other lock files: one that can be locked is a leftover from an earlier run, one that cannot is a live second owner.

A process that is not serving re-runs the election as it exports, throttled to a few times a second, and binds if it now wins. The endpoint is therefore unreachable for that gap plus up to one scrape interval when the owner dies, rather than for the rest of the run. Two consequences: a rank that exports nothing never re-elects, so a run that goes fully idle at the wrong moment stays down until any rank produces telemetry again; and when the port is held from outside the run, the retry backs off to every few seconds instead of hammering a bind that cannot succeed. Alert on the scrape target's up metric rather than on absent series.

Crash- & race-safety

The store is a raw mmap of a fixed POD layout, so it must survive concurrent readers, processes appearing mid-scrape, and processes killed mid-update:

  • Hot path writes only the numeric value, as a single 8-byte aligned __atomic op — SIGKILL can't tear it; a process killed mid-batch just leaves the store a few increments behind (metrics are snapshots, not a ledger).
  • Identity is fixed & positional — metric names are never stored (the collector supplies them); slots are indexed by event type. No variable-length key/label on the hot path.
  • Per-process labels are written once at startup, and the store is not linked into the shared directory until it is initialized and locked — so a reader never finds a half-built store in the first place. The single atomic magic, published last, still guards the staging path used on filesystems without O_TMPFILE.
  • A store that can't be read is not assumed abandoned. Only genuinely invalid content (bad/zero magic, wrong schema, truncated) whose lock nobody holds is reapable; a transient open/mmap failure leaves a live peer's store untouched.
  • Collect() cannot take down the process. prometheus-cpp calls it on its HTTP handler thread, so directory iteration is non-throwing and the body is wrapped in a catch-all that degrades to an empty scrape.
  • The on-disk layout is a contract: it lives in its own header with a static_assert on its size, so a reordered field or a changed cap fails the build instead of shifting offsets under a peer that still validates the header.

Duration histograms

agent_xfer_time_us / agent_xfer_post_time_us are exported per process as the usual _bucket{le="..."} / _sum / _count series, with the same names and default bounds as the single-process exporter. Keeping them in a fixed mmap layout has two consequences:

  • Bucket counts are stored non-cumulative and accumulated at scrape time. Cumulative slots would let a reader racing a writer observe a non-monotonic histogram, which Prometheus treats as malformed; deriving both the cumulative sequence and sample_count from one read makes consistency structural rather than timing-dependent.
  • Bucket bounds travel in each store file, because every process resolves NIXL_TELEMETRY_HISTOGRAM_BUCKETS_US from its own environment and the collector cannot know what a peer was configured with. Give every rank the same value, or the family ends up with series carrying different le sets. A fixed layout also has to cap the list: an override longer than 32 bounds is rejected at construction rather than silently truncated — the only behavioural difference from the single-process exporter.

Series & labels

Labeled hostname, agent_name, pid (cross-process uniqueness; deliberately not the reserved instance), agent_instance (distinguishes multiple same-name agents in one process; 0 in the common case), and optional local_rank (local/per-GPU rank, only when the rank env is set). agent_errors_total additionally carries the bounded status label. Names/types/semantics are identical to the single-process prometheus exporter.

Cleanup & lifecycle

A departing process leaves its store behind, cleanly or not: its last values are usually unscraped, and unlinking on exit would drop everything recorded since the previous scrape. The owner reaps lazily during Collect(), and liveness is a lock rather than a pid — each process flocks its store before the file has a name (created nameless with O_TMPFILE, linked into the directory once initialized) and holds it for its lifetime, so a store the owner can lock has no writer left and will never change again. Such a store is published until its last update ages past the TTL, then reaped: the same path for a clean exit and a SIGKILL, with no pids, no /proc, and no shared PID namespace. Whole-run cleanup is left to the deployment (e.g. a per-pod emptyDir).

Configuration

Variable Description Default
NIXL_TELEMETRY_EXPORTER=prometheus_mp Select this exporter
NIXL_TELEMETRY_MULTIPROC_DIR Shared local folder, same for all ranks (required; use tmpfs//dev/shm to stay in RAM)
NIXL_TELEMETRY_PROMETHEUS_PORT / _LOCAL Scrape port / bind scope 9090 / public
NIXL_TELEMETRY_RANK_ENV Env var holding the rank for the optional local_rank label LOCAL_RANK
NIXL_TELEMETRY_MP_STALE_TTL Seconds after a departed process's last update before its store is reaped 30
NIXL_TELEMETRY_HISTOGRAM_BUCKETS_US Histogram bucket bounds, shared with the other exporters; capped at 32 bounds here built-in µs defaults

Must be a local filesystem (mmap MAP_SHARED coherence) — not NFS. Ranks must also agree on a host-wide CLOCK_MONOTONIC (a shared time namespace); there is no PID namespace requirement, since liveness is the store's own lock. A missing directory is created 0700; an existing one is left alone, with a warning when it is group- or world-writable or owned by another user, and store files planted by another user are ignored rather than read. Unlike a bind collision, a configuration error is fatal: a missing NIXL_TELEMETRY_MULTIPROC_DIR, or a bucket override longer than 32 bounds, fails nixlAgent construction rather than degrading.

Scope / limitations (intentional)

Purpose-built for NIXL's fixed, low-cardinality, per-process label model — it can't represent a metric with a dynamic/high-cardinality label that varies per observation (none exist today; that would need a keyed store). The pid/agent_instance labels that keep per-process counters monotonic also make a restarted rank a new series, so a crash-looping deployment grows TSDB cardinality at the restart rate (scrape size still follows the live process count).

Testing

Runs under CTest/meson: 46 unit tests over the store, the collector and the exporter — including histogram bucket-edge semantics, cumulative/+Inf emission, rejection of an over-long bucket override, each election outcome, a writer promoting itself when the owner exits, the two invariants that keep failover safe (re-electing while already serving cannot drop the held lock, and a conceded election really does free the endpoint), and the lock-liveness contract (a writer holds its own store and releases it on destruction, a live writer outlives a TTL that expires everything, and a departed writer's final values are published once more before its file is reaped) — plus a forking multi-process e2e that scrapes the owner over HTTP and asserts all ranks aggregate, the histogram _bucket/_sum/_count series are served, and a killed rank is dropped and reaped. All prometheus_mp tests pass; changed lines are clang-format-19 clean; every commit is DCO-signed.

Summary by CodeRabbit

  • New Features
    • Added experimental prometheus_mp multiprocess Prometheus exporter that exposes a single /metrics endpoint and aggregates per-process telemetry.
    • Added multi-process scrape ownership election, per-process store sharing, and labels to disambiguate series (pid, agent_instance, optional local_rank).
  • Documentation
    • Expanded guidance for multi-process aggregation, stale TTL/reaping behavior, and histogram parity/limits (including a 32-bucket cap).
  • Bug Fixes
    • Standardized the shared agent_errors_total metric family naming/help across Prometheus exporters.
    • Standardized hostname handling across telemetry exporters.
  • Tests
    • Added unit and end-to-end tests covering the multiprocess store, collector semantics (including histograms), and scrape ownership behavior.
  • Chores
    • Improved test setup to avoid repeated plugin registration issues.

e-eygin added 9 commits July 9, 2026 18:39
First component of the native multi-process Prometheus aggregation
(prometheus_mp) plugin. mp_store is a per-process, fixed-slot memory-mapped
store holding current metric state (cumulative counters and last-operation
gauges) indexed by nixl_telemetry_event_type_t, for scrape-time aggregation
by a single exporter process.

It is a raw mmap of a fixed POD layout (no serialization): writers update
slots in place with lock-free __atomic ops; a reader validates the
magic/schema/size header and returns a consistent snapshot. Per-process
identity (pid + /proc start_time, agent name, hostname, optional dp_rank)
lives in the header for liveness and labeling. Built as a small
prometheus-cpp-free static library with CTest coverage.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Second component of the prometheus_mp plugin. nixlMultiprocessCollector is a
prometheus::Collectable that, on each scrape, globs the shared telemetry
directory for peer store files, reads a snapshot of each, drops (and optionally
reaps) stale ones, and returns per-process metric families.

Series are emitted per (metric, process) with no cross-process aggregation:
cumulative counters and last-operation gauges keyed by
nixl_telemetry_event_type_t, plus agent_errors_total with a status label,
labeled by hostname, agent_name and (when present) dp_rank. Staleness combines
pid liveness (kill(pid,0) + /proc start_time to guard PID reuse) with a
last-update TTL. The family-building and liveness logic are split into pure
functions for unit testing; a shared store file-naming helper is added to
mp_store. Collector library and tests are gated on prometheus-cpp.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
The collector's per-process series were disambiguated only by agent_name
(plus optional dp_rank). If two processes share an agent name and have no
dp_rank, they would emit an identical label set -> a duplicate Prometheus
series and a rejected scrape.

Add an unconditional pid label so every live process is a distinct series
regardless of how callers name agents. pid (not the reserved "instance"
target label) also avoids the exported_instance renaming trap.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Third component of the prometheus_mp plugin: the exporter that ties the store
and collector together, selectable via NIXL_TELEMETRY_EXPORTER=prometheus_mp.

Every process writes its own metric state to a per-process store in
NIXL_TELEMETRY_MULTIPROC_DIR (unique file per pid/instance). On construction
each process races to bind the scrape port: the winner runs a prometheus-cpp
Exposer plus a nixlMultiprocessCollector that aggregates all peers on each
scrape; losers fall back to writer-only mode. A bind collision is caught
internally and never rethrown as nixlTelemetryBindFailed, so every process --
owner or writer -- gets a valid telemetry sink and all ranks are exported
behind the single owner endpoint.

Config: NIXL_TELEMETRY_MULTIPROC_DIR (required), NIXL_TELEMETRY_RANK_ENV
(optional dp_rank source, default LOCAL_RANK), NIXL_TELEMETRY_MP_STALE_TTL;
reuses NIXL_TELEMETRY_PROMETHEUS_PORT / _LOCAL. The exporter is built both as
the installable plugin .so and as a static library for direct unit testing
(owner mode, writer-mode-on-collision, missing-dir). Gated on prometheus-cpp.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Fourth component of the prometheus_mp plugin: an end-to-end test that proves
cross-process aggregation with real processes. The parent becomes the
bind-race owner and serves the endpoint; N children fork (while the parent is
still single-threaded) and run as writers against the shared dir. After a
single HTTP scrape of the owner port, all N+1 processes appear as distinct
per-process series. One child is then killed and reaped, and a second scrape
confirms its series is dropped and its store file removed (stale TTL 0).

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Handle processes that appear or die mid-scrape more robustly:

- Reader treats a zero-magic store (a process still initializing its file, or
  an orphan from a process killed mid-creation) as a quiet skip instead of a
  "bad magic" WARN; a genuinely wrong non-zero magic still warns.
- Collector now reaps unparseable store files (zero/bad magic, wrong schema,
  truncated) once they are older than max(stale TTL, 2s). The floor protects a
  store a live process is actively creating from being deleted out from under
  it, even when the TTL is 0.

This closes a slow file leak: a process killed during store creation left a
zero-magic file that was correctly skipped every scrape but never cleaned up.
Cleanup is performed by the bind-race owner during Collect(), since a killed
process cannot clean up after itself.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Describe the native single-endpoint multi-process aggregation exporter in
docs/telemetry.md: add it to the architecture components, update the
multi-process scrape note to point ranks needing full aggregation at it, and
add a "Multi-process aggregation" section covering its model, configuration
(NIXL_TELEMETRY_MULTIPROC_DIR/RANK_ENV/MP_STALE_TTL), labels, and the explicit
limitation that it is hardcoded to NIXL's fixed metric model and cannot carry
dynamic per-observation labels.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Document the multi-process Prometheus exporter plug-in alongside the sibling
prometheus and doca plugin READMEs: how bind-race owner election and the
per-process mmap stores work, configuration (NIXL_TELEMETRY_MULTIPROC_DIR and
the optional RANK_ENV / MP_STALE_TTL / port vars), the per-process labels
(hostname, agent_name, pid, optional dp_rank), and the explicit scope note that
it is purpose-built for NIXL's fixed metric model (no dynamic per-observation
labels) rather than a generic Prometheus multiprocess store.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Clarify that NIXL_TELEMETRY_MULTIPROC_DIR follows Dynamo's
PROMETHEUS_MULTIPROC_DIR convention: a shared local folder (not NFS), one per
pod/process-family, treated as ephemeral. Note the key difference from Dynamo:
NIXL is loaded independently per rank with no parent to propagate the path, so
the launcher/operator must set the same directory for every rank (hence it is
required, not auto-defaulted). tmpfs is optional, not required.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
@e-eygin e-eygin self-assigned this Jul 9, 2026
@e-eygin
e-eygin requested a review from a team as a code owner July 9, 2026 20:05
@copy-pr-bot

copy-pr-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

👋 Hi e-eygin! Thank you for contributing to ai-dynamo/nixl.

Your PR reviewers will review your contribution then trigger the CI to test your changes.

🚀

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds the experimental prometheus_mp telemetry plugin with per-process mmap stores, scrape-time collection, owner election, Prometheus serving, tests, build integration, shared hostname and metric definitions, and documentation.

Changes

Prometheus MP exporter

Layer / File(s) Summary
Per-process mmap metric store
src/plugins/telemetry/prometheus_mp/mp_store.{h,cpp}, src/plugins/telemetry/prometheus_mp/mp_store_layout.h, test/gtest/telemetry_mp_store_test.cpp
Defines fixed-layout mmap storage, atomic metric updates, process identity and heartbeat tracking, snapshot validation, histogram handling, and cleanup tests.
Scrape-time multiprocess collector
src/plugins/telemetry/prometheus_mp/mp_collector.{h,cpp}, test/gtest/telemetry_mp_collector_test.cpp
Adds PID/TTL liveness checks, per-process Prometheus metric families, directory scanning, invalid-file handling, and stale-file reaping.
Owner-elected exporter and plugin entrypoints
src/plugins/telemetry/prometheus_mp/{owner_election.h,scoped_fd.h,prometheus_mp_exporter.{h,cpp},scrape_endpoint.{h,cpp},prometheus_mp_plugin.cpp}, test/gtest/telemetry_mp_{exporter,e2e}_test.cpp
Adds environment configuration, per-process event recording, flock-based ownership selection, Prometheus serving, plugin lifecycle hooks, and exporter tests.
Build wiring, shared definitions, and documentation
src/plugins/telemetry/..., src/utils/common/hostname.h, test/gtest/meson.build, test/gtest/prometheus_telemetry_fixture.h, src/plugins/telemetry/prometheus_mp/README.md, docs/telemetry.md
Adds build targets and test dependencies, centralizes hostname and error metric definitions, guards plugin registration, and documents configuration, labels, aggregation, and limitations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: colinnv

Sequence Diagram(s)

sequenceDiagram
  participant Process
  participant nixlTelemetryPrometheusMpExporter
  participant ownerElection
  participant storeWriter
  participant PrometheusExposer
  participant nixlMultiprocessCollector

  Process->>nixlTelemetryPrometheusMpExporter: construct exporter
  nixlTelemetryPrometheusMpExporter->>storeWriter: create per-process mmap store
  nixlTelemetryPrometheusMpExporter->>ownerElection: attempt shared-directory election
  ownerElection-->>nixlTelemetryPrometheusMpExporter: owner or writer-only result
  nixlTelemetryPrometheusMpExporter->>PrometheusExposer: bind endpoint when elected
  Process->>nixlTelemetryPrometheusMpExporter: exportEvent
  nixlTelemetryPrometheusMpExporter->>storeWriter: write metric update
  PrometheusExposer->>nixlMultiprocessCollector: scrape /metrics
  nixlMultiprocessCollector->>storeWriter: read live snapshots
  nixlMultiprocessCollector-->>PrometheusExposer: return MetricFamily list
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: native multi-process Prometheus aggregation via the new prometheus_mp exporter.
Description check ✅ Passed The description fully covers what, why, and how, with detailed design, configuration, limitations, and testing information.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

CI pre-commit (codespell) flagged the local nowNs() helper (read as
"knowns/nouns") and "unparseable".

- Replace the duplicated per-file nowNs() helpers with the existing
  nixlTime::getNs() (CLOCK_MONOTONIC, host-wide so comparable across
  processes; also skew-free vs wall clock for the staleness delta). The
  last-update timestamp is only ever compared against another getNs() reading,
  so switching from system_clock to steady_clock is safe; the orphan-file mtime
  path is independent and still uses wall-clock time().
- Spell "unparseable" as "unparsable" in comments.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
@e-eygin

e-eygin commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai help

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai rate limit to show your current review rate limit status.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
  • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
  • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai emit path instructions to emit generated path-specific review instructions for this repository.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@e-eygin

e-eygin commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugins/telemetry/prometheus_mp/mp_collector.cpp`:
- Around line 138-191: The reads in buildMetricFamilies are indexing fixed-size
snapshot arrays directly from telemetry_metric_event_types and
telemetry_error_event_types, so add bounds protection before accessing
snap.counters or snap.gauges. Update the buildMetricFamilies logic to use a
checked access path (such as .at() or an explicit range guard) for the slot
derived from the enum, and make sure the error-family loop uses the same
protection. Keep the existing telemetryMetricDescriptor, counterMetric, and
gaugeMetric flow unchanged otherwise.
- Around line 200-235: The Collect() scan still relies on the range-based for
over std::filesystem::directory_iterator, so an implicit iterator increment can
throw even though the constructor uses std::error_code. Update
nixlMultiprocessCollector::Collect() to handle iterator advancement errors
explicitly, either by switching to a manual directory_iterator loop that uses
the non-throwing increment overload with an error_code or by wrapping the
iteration in a try/catch that safely returns an empty result on
filesystem_error. Keep the existing dir_ scan, readStoreSnapshot, and reapStale_
logic intact while making the traversal resilient to mid-scan directory changes.

In `@src/plugins/telemetry/prometheus_mp/mp_store.cpp`:
- Around line 22-32: mp_store.cpp is using std::min and std::istream_iterator
without directly including their standard headers. Update the file’s include
list near the top to add both <algorithm> and <iterator> so MpStore’s
implementation does not rely on transitive includes.

In `@src/plugins/telemetry/prometheus_mp/mp_store.h`:
- Around line 41-47: The slot-count derivation in MP_STORE_SLOT_COUNT is fragile
because it assumes AGENT_TELEMETRY_EVENTS_DROPPED remains the last enum value.
Add a compile-time safeguard in mp_store.h, near the MP_STORE_SLOT_COUNT
definition, using a static_assert that validates the telemetry event enums fit
within the array bounds. Make the check cover the relevant
nixl_telemetry_event_type_t values used by mp_collector.cpp’s
buildMetricFamilies so future enum extensions fail at compile time instead of
causing out-of-bounds access.

In `@src/plugins/telemetry/prometheus_mp/prometheus_mp_exporter.cpp`:
- Around line 40-49: The telemetry constant declarations in
prometheus_mp_exporter.cpp use constexpr char[] while the matching constants in
mp_store.h use inline constexpr std::string_view, so make them consistent.
Update the identifiers such as defaultRankEnvName, prometheusPortVar,
prometheusLocalVar, multiprocDirVar, rankEnvVar, and staleTtlVar to use the same
string_view style as the sibling component, keeping the existing names and
usages intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 249ccc98-927b-47c3-95a4-ea65420aac52

📥 Commits

Reviewing files that changed from the base of the PR and between 424a60c and b4093af.

📒 Files selected for processing (16)
  • docs/telemetry.md
  • src/plugins/telemetry/meson.build
  • src/plugins/telemetry/prometheus_mp/README.md
  • src/plugins/telemetry/prometheus_mp/meson.build
  • src/plugins/telemetry/prometheus_mp/mp_collector.cpp
  • src/plugins/telemetry/prometheus_mp/mp_collector.h
  • src/plugins/telemetry/prometheus_mp/mp_store.cpp
  • src/plugins/telemetry/prometheus_mp/mp_store.h
  • src/plugins/telemetry/prometheus_mp/prometheus_mp_exporter.cpp
  • src/plugins/telemetry/prometheus_mp/prometheus_mp_exporter.h
  • src/plugins/telemetry/prometheus_mp/prometheus_mp_plugin.cpp
  • test/gtest/meson.build
  • test/gtest/telemetry_mp_collector_test.cpp
  • test/gtest/telemetry_mp_e2e_test.cpp
  • test/gtest/telemetry_mp_exporter_test.cpp
  • test/gtest/telemetry_mp_store_test.cpp

Comment thread src/plugins/telemetry/prometheus_mp/mp_collector.cpp Outdated
Comment thread src/plugins/telemetry/prometheus_mp/mp_collector.cpp
Comment thread src/plugins/telemetry/prometheus_mp/mp_store.cpp
Comment thread src/plugins/telemetry/prometheus_mp/mp_store.h
Comment thread src/plugins/telemetry/prometheus_mp/prometheus_mp_exporter.cpp
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

… includes)

- mp_store.h: add a compile-time static_assert that MP_STORE_SLOT_COUNT covers
  every telemetry event type the collector indexes, so extending the enum past
  AGENT_TELEMETRY_EVENTS_DROPPED fails the build instead of causing an
  out-of-bounds counter/gauge access.
- mp_collector.cpp: iterate the shared directory with the non-throwing
  increment(ec) instead of the range-for's throwing operator++, since peer
  writers and this collector's own reaping mutate the directory concurrently
  and a mid-iteration filesystem error would otherwise escape Collect().
- mp_store.cpp: add explicit <algorithm> and <iterator> includes (std::min,
  std::istream[buf]_iterator) rather than relying on transitive includes.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
@e-eygin

e-eygin commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

/build

@e-eygin

e-eygin commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 3bf2139

The optional per-process rank label was named dp_rank, but its value is sourced
from the local/per-GPU (tensor-parallel) rank env (default LOCAL_RANK), not the
data-parallel rank. That misdescribed the value and collided with Dynamo's own
data-parallel dp_rank series.

Rename the emitted label and the internal identifiers (store field, writer
param, exporter helper) to local_rank. The NIXL_TELEMETRY_RANK_ENV var and its
LOCAL_RANK default are unchanged; the label is still optional and emitted only
when the env is set. Docs and tests updated.

Part of NIX-1614.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
@e-eygin

e-eygin commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@ColinNV @ovidiusm please review

e-eygin added 2 commits July 10, 2026 15:31
The prometheus_mp store writer only unmapped its file on destruction, so an
agent destroyed while its process keeps running left a store behind. Because
liveness is pid-based, the collector kept publishing that dead agent's frozen
series indefinitely.

Have ~mpStoreWriter() best-effort remove its own file: clean shutdown is the
deterministic "producer gone" signal, while crash/kill (destructor never runs)
still falls back to the owner's liveness/TTL reaping. Adjust the store
round-trip tests to read while the writer is alive (the real cross-process
pattern) and add a test for the new cleanup.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Multiple agents created in one process with the same agent name shared every
series label (hostname, agent_name, pid, local_rank), producing duplicate
Prometheus series that make the whole scrape fail. The store filename already
carried a per-process instance counter, but it never reached the exposed labels.

Persist the instance in the store header and emit it as an agent_instance label,
so same-name same-process agents get distinct series (agent_instance=0 for the
common single-agent case). Schema version is unchanged: prometheus_mp has not
shipped, so no on-disk format compatibility is at stake.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
@svc-nixl

svc-nixl commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-test-sanitizers · commit 548b07b7

TL;DR: The tsan variant of the "Test Sanitizer" stage failed because the earlier meson test ThreadSanitizer suite exited non-zero (recording meson sanitizer suite in the FAILURES accumulator) — i.e. TSan detected a data race under the tsan build; all subsequent smoke-test binaries passed. Fix: open the archived sanitizer-logs-x86_64-tsan.tar.gz (meson-logs/testlog.txt) to find the TSan race report and fix the flagged threading bug.

Full analysis

Summary: x86_64/tsan/... "Test Sanitizer" stage (nodes 124 & 177) exited 1; the asan_ubsan variant of the same commit passed.

Root cause: The sanitizer runner accumulates failures into a variable, and at the end of the tsan run that variable held ' meson sanitizer suite', so it printed ==== Sanitizer test FAILURES: meson sanitizer suite ==== and exit 1. Every individually-invoked smoke binary (agent_example, nixl_example, ucx_backend_test, nixl_posix_test, serdes_test, test_plugin) reported PASSED, so the failure came from the earlier meson test sanitizer suite (which runs before the smoke tests and whose output is above the captured log window). Since the identical code passed under ASan/UBSan and only failed under ThreadSanitizer, this is a TSan-detected data race / threading violation in the meson unit-test suite, not a real functional break in the smoke tests. This is a genuine test failure (non-zero exit), not a timeout/hang — the log shows continuous activity with no large gaps.

Implicated commit: unknown — the specific racing component is in the TSan report inside the archived meson-logs/testlog.txt, which is not reproduced in the truncated console. Commit under test: 548b07b (PR #1920).

File: Failure marker at end of the tsan "Test Sanitizer" stage log; detailed race report in artifact sanitizer-logs-x86_64-tsan.tar.gzmeson-logs/testlog.txt (not the smoke-test binaries, which all passed).

Suggested fix:

  1. Download the build's archived artifact sanitizer-logs-x86_64-tsan.tar.gz and open meson-logs/testlog.txt to read the WARNING: ThreadSanitizer: data race report (stack traces + offending file:line).
  2. Fix the flagged race (add proper locking / atomics on the shared state the two threads touch). Given TSan-only failure and the multithreaded UCX progress-thread paths exercised here, the UCX backend / progress-thread or notification handling is the likely area.
  3. To confirm locally: meson test -C nixl_build --suite sanitizer in the tsan container, or re-run the job to check for flakiness. Do not simply raise a timeout — this is a hard test failure, not a slow/hung run.

Related: PR #1920 (under test); possibly related threading/test-isolation work: PR #1743 ("Run in single process"), PR #1610 ("PLUGINS/UCX: Do not allow emulated RMA protocols"). CI sanitizer harness introduced in #1709.

@svc-nixl

svc-nixl commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-gpu · commit 548b07b7

TL;DR: The "Run CPP tests" stage (#242) was killed with exit code 143 (SIGTERM) after ~61 minutes because the ucx_threadpool/* gtest variants run 2–3× slower than the plain ucx variants, roughly doubling the suite runtime and pushing the stage past its wall-clock limit. The tests were making continuous progress up to the kill, so this is genuine slowness/test-matrix bloat rather than a single hang.

Full analysis

Summary: nixl-ci-gpu #3052 stage "Run CPP tests" (node 242) aborted (exit 143) mid-way through the gtest suite at test [150/198] ucx_threadpool/TestTransfer.remoteMDFromSocket.

Root cause: The gtest suite is parametrized over multiple UCX configurations, and the ucx_threadpool instantiations are dramatically slower than their non-threadpool counterparts — e.g. TestTransfer.RandomSizes took 56,018 ms for ucx but 139,538 ms for ucx_threadpool, and the ucx_threadpool/TestErrorHandling.* cases each ran 90k–145k ms. This doubled the CPP stage runtime (stage 203 on the other lane finished in ~27 min; stage 242 ran ~61 min before being killed). The log shows tests completing in sequence with reasonable per-test durations right up until the SIGTERM at 18:42:45 — the largest silent gap (~2 min between test 149 finishing and the kill) is consistent with a single slow threadpool test still running, not a stuck process. So the job hit the stage/build wall-clock limit while still legitimately (but slowly) progressing. PR #1920 is a telemetry-only change and is not the functional cause; it just triggered a full CI run that exposed the slow threadpool test matrix.

Implicated commit: No single functional regression commit; the slow variants trace to the threadpool test parametrization introduced/derived in 209abbe9b98e (Ilia Yastrebov, "Refactoring: derive nixlUcxThreadPoolEngine from nixlUcxThreadEngine" #1906). The abort itself is a wall-clock kill, not a code bug in PR #1920 (Author: e-eygin).

File: test/gtest/ UCX transfer/error-handling parametrized suites (the ucx_threadpool/TestTransfer and ucx_threadpool/TestErrorHandling instantiations); stage runner for "Run CPP tests" in the Jenkins pipeline.

Suggested fix: Reduce the wall-clock cost of the ucx_threadpool test matrix rather than blindly raising the timeout: (1) investigate why threadpool transfers take 2–3× longer — likely progress-thread polling/backoff or per-iteration thread setup/teardown overhead in nixlUcxThreadPoolEngine; (2) lower the iteration count / random-size range for the threadpool-parametrized cases, or run the threadpool variants as a separate/parallel Jenkins stage so they don't serialize behind the plain UCX variants; and (3) if the slowdown is expected, split "Run CPP tests" so gtest runs in its own stage with a budget sized for the full parametrized matrix. Confirm against a known-good build's stage-242 timing to set the budget.

Related: PR #1920 (trigger, telemetry — not the cause); #1906 (threadpool engine refactor). none other directly matching.

@svc-nixl

svc-nixl commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-dl-gpu-ep · commit 548b07b7

TL;DR: The build compiled fine; the job failed in "Allocate DL EP Environment" because a Slurm salloc for a gb200nvl72_cx8 node timed out after the 3600s --immediate window ("Unable to allocate resources: Connection timed out"). This is a cluster capacity/queue issue, not a nixl code bug — retry the build or increase/queue the allocation.

Full analysis

Summary: Stage 200 "Allocate DL EP Environment" failed with salloc: error: Unable to allocate resources: Connection timed out after waiting the full 1-hour immediate window for a GB200 GPU node.

Root cause: Slurm resource allocation timeout. At 17:45:10 the pipeline ran salloc -N 1 -p gb200nvl72_cx8 --immediate=3600 --time=01:30:00 ... --account=blackwell. The job was accepted and queued (Pending job allocation 1736892 ... queued and waiting for resources) but no node became free within --immediate=3600 seconds; at 18:45:19 (exactly ~3600s later) salloc gave up and exited non-zero, so the shell (set -e) returned exit code 1. The ~1-hour gap is Slurm legitimately waiting in queue, not a nixl process hang — all preceding stages (UCX build, NIXL/nixlbench compile, Docker image build + push) completed successfully.

Implicated commit: none — this is a CI infrastructure/cluster-capacity failure, unrelated to commit 548b07b or PR #1920.

File: Jenkins pipeline Slurm allocation step (slurm.allocation call: partition:gb200nvl72_cx8, immediateTimeout:3600, account=blackwell) — no repository source file implicated.

Suggested fix: Re-run the build; the gb200nvl72_cx8 partition was saturated at the time. If this recurs, either (a) raise --immediate/immediateTimeout or drop --immediate so the job blocks in the queue instead of bailing at 3600s, (b) check the blackwell account's queue/priority and node availability on dlcluster.nvidia.com, or (c) add automatic retry-on-allocation-timeout logic to the pipeline. No code change to PR #1920 is needed.

Related: none found.

@ColinNV
ColinNV requested a review from ovidiusm August 4, 2026 06:35
@e-eygin
e-eygin requested a review from ColinNV August 5, 2026 12:03
Comment thread src/plugins/telemetry/prometheus_mp/mp_collector.cpp Outdated
Comment thread src/plugins/telemetry/prometheus_mp/mp_collector.cpp Outdated
e-eygin added 2 commits August 5, 2026 12:53
The repo builds with cpp_std=c++20, so the prefix/suffix checks on store and
lock file names no longer need positional compare() calls.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
A store is now flock-ed before it has a name -- created nameless with O_TMPFILE
and linked in once initialized, or staged under a .staging name and renamed
where the filesystem has no O_TMPFILE -- and stays locked for its writer's
lifetime. A store whose lock can be taken therefore has no writer left and will
never change again, which is the whole liveness test.

That replaces kill(pid, 0), the /proc start-time reuse guard and the heuristics
that protected a store mid-creation (the zero-magic grace period and its
two-second floor), and drops the requirement that ranks share a PID namespace
with the collector. The stale TTL still decides how long a departed writer's
final values are published before its file is reaped.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
@e-eygin
e-eygin requested a review from ColinNV August 5, 2026 14:11
The warning promised that such a store would "linger for the stale TTL after it
exits", which is not what happens in either direction. A reader that can lock the
store reads it as abandoned, so the rank loses its series and its file if it goes
quiet for longer than the TTL while still running; where no process can lock at
all, the reader's probe fails too and nothing is reaped, so departed ranks stay
published indefinitely.

Say both, and give the README's "never dropped for being idle" the same
lock-is-usable qualifier the election already carries.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
@e-eygin

e-eygin commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

/build

@e-eygin

e-eygin commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 572458f

@ColinNV ColinNV left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find any more race conditions.

Comment thread src/plugins/telemetry/prometheus_mp/mp_store.cpp Outdated
Comment thread src/plugins/telemetry/prometheus_mp/mp_store.cpp
Comment thread src/plugins/telemetry/prometheus_mp/mp_store.cpp Outdated
An mmap owner in src/utils/common replaces the writer's raw pointer plus
size pair and the reader's unique_ptr-with-deleter guard, which removes the
unmap-and-rethrow path from the writer's constructor and empties its
destructor.

The store file name needs nothing more from /proc than a value two runs
sharing a pid cannot both have, so a per-process constant taken from the
wall clock replaces parsing field 22 of /proc/<pid>/stat.

Also states why an unprobable store reads as held.

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
@e-eygin

e-eygin commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

/build

@e-eygin

e-eygin commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 8f25980

@e-eygin
e-eygin merged commit 407b69b into ai-dynamo:main Aug 6, 2026
19 checks passed
e-eygin added a commit that referenced this pull request Aug 20, 2026
## What?

Three gtest cases for the `prometheus_mp` telemetry-directory hardening
in `resolveMultiprocDir()`: a directory the exporter creates is 0700,
and an existing group-writable or world-writable one warns and still
serves. Tests only, no production change.

## Why?

Deferred from the diff freeze on #1920 (NIX-1683). None of it could be
observed before: the mp fixture hands the exporter a directory it
already created at 0700, so the warning can never fire and the
production `chmod` is indistinguishable from the fixture's own. Dropping
the `created` guard, or reordering `permissions` before
`create_directories`, broke nothing visible.

The foreign-owner branch (`st_uid != geteuid()`) is left uncovered by
design, not by oversight — see below.

## How?

All three tests point `NIXL_TELEMETRY_MULTIPROC_DIR` at a
**subdirectory** of the fixture's directory. That child path is free to
be absent or loose without any fixture surgery, and the fixture still
needs its own 0700 chmod to stop a permissive CI umask from reddening
the suite.

<details>
<summary>Mutation testing, the two loose cases, and the uncovered
branch</summary>

Each test was verified against the mutation it exists to catch, and
confirmed to fail on that one alone:

| Mutation | Result |
|---|---|
| Drop the `permissions(..., owner_all, replace)` call |
`CreatedTelemetryDirIsPrivate` fails: mode is the umask default 0755 |
| Drop the `created` guard (chmod unconditionally) |
`WorldWritable...`/`GroupWritable...` fail: the loose directory is
silently fixed before `stat()`, so the warning count falls to 0 |
| Narrow the mask to `S_IWGRP` alone | `WorldWritableTelemetryDirWarns`
fails; the group case still warns |
| Narrow the mask to `S_IWOTH` alone | `GroupWritableTelemetryDirWarns`
fails; the world case still warns |

The last two are why the loose case is 0720 and 0702 rather than a
single 0777: 0777 sets both bits, so it cannot tell the halves of the
mask apart. Either single-bit case subsumes 0777. They are two `TEST_F`s
rather than a `TEST_P` because the fixture derives its directory from
the suite and test name, and an instantiated parameterised suite puts a
`/` in both, nesting that directory and leaving the intermediate level
behind `TearDown`.

`CreatedTelemetryDirIsPrivate` also asserts the subdirectory is absent
first: otherwise `create_directories()` reports `created == false`, the
`chmod` never runs, and the 0700 assertion is satisfied by whatever left
the directory behind — which a failed `TearDown` under `--gtest_repeat`
could do, since every iteration reuses the same path.

All mutations were reverted. Verified with `--gtest_repeat=5`: 93 tests
per iteration, all passing, no un-ignored warnings.

**Foreign owner.** Covering it needs `chown`, so the test would run only
as root and skip on every unprivileged lane — an assertion whose meaning
depends on the environment, which is the shape NIX-1710 just removed
from the tracing suite, and which NIX-1623 is trying to make fatal. The
prerequisite is a second unprivileged uid in the test container; that is
CI infrastructure and belongs in its own change.

</details>

---------

Signed-off-by: Efraim Eygin <eeygin@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants