Skip to content

feat(data-plane): track data-plane time, latency percentiles and byte volume - #3616

Merged
ZhiyuLi-Nvidia merged 96 commits into
mainfrom
zhiyul/data_plane_observability_metrics
Sep 17, 2026
Merged

ZhiyuLi-Nvidia merged 96 commits into
mainfrom
zhiyul/data_plane_observability_metrics

Conversation

@ZhiyuLi-Nvidia

@ZhiyuLi-Nvidia ZhiyuLi-Nvidia commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What this adds

Per-step visibility into what the TransferQueue data plane costs, so "is the
data plane my bottleneck, and which part of it" is answerable from a dashboard
instead of a profiler.

Off by default at the top level (data_plane.enabled: false); when the data
plane is on, observability.enabled: true wraps the client in
MetricsDataPlaneClient. Measured overhead is ~15-20 µs per op, and the
wrapper bills itself so that cost is visible rather than asserted.

The metrics

Everything is logged under data_plane/cluster/ (the driver plus every policy
worker, summed) or data_plane/driver/ when the fan-out reaches one process.

Series Answers
step/frac_of_step Is the data plane worth optimising at all? The busiest process's time over the step's own wall clock. Processes run concurrently inside one step, so summing them exceeds it and averaging dilutes the busiest — the step waits on one process
step/percent_of_dataplane/by_op/{put,get,clear,register} Which call is expensive? Sums to 100
step/percent_of_dataplane/by_cause/{fixed_overhead,transfer} Fixed per-request cost, or bandwidth? Sums to ≤100; only ops with an identifiable fit can be split
step/volume_mb/by_op/{get,put} Which direction the traffic went
step/wall_ms, step/comm_volume_mb How much time and traffic
now/bytes_outstanding_mb Occupancy — bytes put and not yet cleared. Rising is the leak signal
step/self/{overhead_ms,frac} What measuring cost
step/hash/* Only with verify_tensor_hash on

Plus a per-op breakdown table (data_plane/{cluster,driver}/breakdown),
ordered worst-first, carrying the detail that would be noise as 32 line charts:
calls, mean/max/p50/p90 ms, the overhead/transfer split, and MB.

Two denominators, deliberately. frac_of_step divides by the step;
percent_of_dataplane divides by the data plane. A workload can be 43% put
and still not be worth touching — you need both.

Reading the numbers correctly

Three things are easy to misread, and are documented in
nemo_rl/data_plane/README.md:

  • Volume counts transfers, not data. A byte written then read counts on
    both sides, and every process's transfers are summed. Correct for "what
    crossed the wire"; useless for "how big was the batch".
  • put reads small. The rollout actor builds its own client and is not on
    the worker group, so its kv_first_write of the whole batch is in neither
    volume_mb nor comm_volume_mb.
  • by_cause is often absent. Request sizes in RL are frequently uniform,
    which makes the affine fit unidentifiable. It reports nothing rather than an
    arbitrary split.

Optional wire-hash guard

verify_tensor_hash: false by default, in the recipes as well — the suites
that gate on it pass data_plane.observability.verify_tensor_hash=True on the
command line, so a recipe you copy does not silently opt into it.

When on, it fingerprints every tensor row with torch.hash_tensor on the way
in and out and reports divergence — the one failure class nothing else
notices, because a mis-sharded read trains happily on the wrong rows. Catches a
row served from the wrong sample, two rows swapped between DP ranks,
truncation, a bf16→fp32 change, and a single element in any dtype.

The wire-in reading travels with the row. Each field is mirrored by a
<field>_hash int64 column, written by the same put and declared alongside the
field. Holding the reading in the putting process only ever verified a
same-process round trip, and the transfer worth checking is not one: the
rollout actor writes what the policy workers read. The mirror is per top-level
field, so a multimodal images reduces its leaves to one images_hash.

Measured on an interleaved same-node A/B — off/on/off/on, 20 steps each,
all four arms in one allocation (Llama-3.2-1B, 1 node x 4 GB300, TQ simple,
24.4 MB/step). Medians over steps >= 5, which drops vLLM warm-up and the first
refit.

off on
hash/rows_recorded 1536
hash/rows_checked 2560 - more than it wrote, i.e. it verifies rows it did not produce
hash/rows_unverified 0 - was the normal case before the reading travelled
hash/mismatches, guard_failures, fields_skipped 0
step/wall_s 0.219 / 0.216 s 0.273 / 0.274 s
step/self/overhead_ms 3.8 / 3.9 ms 112 / 113 ms
step/frac_of_step 1.42 % 1.73 / 1.83 %
step/comm_volume_mb 24.45 24.45
accounted cost of the guard +164 ms/step

wall_s and self/overhead_ms are disjoint — the first is the RPC, the second
is the wrapper with the RPC subtracted out — so the guard's cost is their sum:
+55 ms of extra <field>_hash columns on the wire, +109 ms of hashing.

The control is the two off arms, ~40 minutes apart in the same allocation.
They differ by 2.9 ms on wall_s and 0.005 pp on frac_of_step, against a
guard effect of 164 ms — a 50:1 ratio of signal to drift. Both pairs, (b-a)
and (d-c), agree within 2%. comm_volume_mb is identical across arms to four
decimals, which is the check that the two arms moved the same data.

Its effect on step time is not measurable, and that is not evidence it is
free.
total_step_time reads 15.36 / 15.69 / 15.22 / 15.21 s; the two
identical off arms differ by 0.14 s and per-arm MAD runs 0.16-0.56 s, so a
0.16 s effect sits inside the noise. The data-plane counters are the quiet
channel and the step clock the loud one, which is why the cost is reported from
the counters. An earlier revision of this section claimed the two agreed "to
within 4 ms" — that was a single pair read against this floor, and it is
withdrawn.

This supersedes the 10.2 ms this section previously claimed, measured when the
digest stayed process-local and most rows were abstained on rather than
compared.

Blind spot, measured rather than assumed: the row fold is an XOR reduction, so
it cannot see a permutation within a row (0/200 for a two-token swap). The
per-field fold over leaves uses * 31 + rather than XOR, so identical leaves
cannot cancel there.

Verification

  • 369 unit tests pass, 3 skipped (tests/unit/data_plane, run inside the
    nightly container so TransferQueue is real - simple and mooncake_cpu
    round trips both included), plus the two test_grpo.py cases covering both
    logging scopes. Re-run at the current head, 45 s. That includes 23/23
    adversarial hash-guard checks (10 corruption modes caught, zero false alarms
    over a 500-row soak, every shard grouping) and 48/48 metric-audit checks
    against an independently computed ground truth.

  • Both accepted limits of the guard are pinned by name, so neither can
    become a surprise: a within-row permutation, and a constant-valued row of
    even length, whose digest is its dtype|row shape seed alone because the
    XOR fold cancels in pairs. advantages has exactly that shape.

  • A real 25-step GRPO run — Llama-3.2-1B on OpenMathInstruct-2, 1 node ×
    4 GB200, TransferQueue simple, 5 processes reporting:
    https://wandb.ai/nvidia/nemo-rl-dataplane-obs/runs/w316chaj

    get 65% of data-plane time · 24 MB moved per step · 38,400 rows
    hash-checked, 0 mismatches. Its frac_of_step predates the reduction
    change and was a per-process mean; the current figure, measured on the
    four-arm A/B above, is 1.42% of a ~15.3 s step guard-off and 1.73-1.83%
    with the guard on — the slowest process's serial data-plane time, since that
    is the one the gradient all-reduce makes everyone wait for.

Notes for review

  • Percentiles are gated on sample size: p50 needs 20 calls in the window, p90
    needs 40 (~4 observations above the rank). Below that the key is absent
    rather than reporting the maximum under a percentile's name. p90 rather than
    p99 because a step holds tens of calls — a p99 off 58 samples equalled the
    maximum 80% of the time.
  • max_ms is scoped to the step by being reset by its reader, since a
    maximum cannot be differenced out of a cumulative counter.
  • Supersedes the 345-line observability.py already on main.

@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia requested a review from a team as a code owner August 12, 2026 23:20
@copy-pr-bot

copy-pr-bot Bot commented Aug 12, 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.

@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia requested review from a team as code owners August 13, 2026 01:20
@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia requested review from a team as code owners August 22, 2026 06:12
@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia force-pushed the zhiyul/data_plane_observability_metrics branch from c67f613 to 48ed054 Compare August 24, 2026 00:50
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 48ed054

@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia force-pushed the zhiyul/data_plane_observability_metrics branch from 5e79a5d to 67be5a7 Compare August 27, 2026 07:06
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 67be5a7

@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia force-pushed the zhiyul/data_plane_observability_metrics branch 2 times, most recently from 9cbeea4 to 501a7fd Compare August 30, 2026 15:27
@ZhiyuLi-Nvidia ZhiyuLi-Nvidia added the CI:L1 Run doctests, unit tests, and functional tests label Aug 30, 2026
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 501a7fd

@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 4ea3bc7

@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia force-pushed the zhiyul/data_plane_observability_metrics branch from 4ea3bc7 to 2d5719b Compare September 9, 2026 09:58
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 2d5719b

@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 05348d2

Comment thread tests/test_suites/llm/grpo-deepscaler-1.5b-8K-tq_simple.sh
Comment thread nemo_rl/data_plane/README.md
Comment thread nemo_rl/data_plane/observability.py
Comment thread nemo_rl/data_plane/observability.py Outdated
@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia force-pushed the zhiyul/data_plane_observability_metrics branch from 16e3cc0 to d1cd96f Compare September 10, 2026 23:53
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test dc3d609

@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 485a454

@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia force-pushed the zhiyul/data_plane_observability_metrics branch from 485a454 to e1505b1 Compare September 13, 2026 20:12
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test e1505b1

@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 96c161c

@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test 2af2c3a

@terrykong terrykong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this — the measurement discipline behind it is better than most performance PRs get. Running the A/B interleaved on one node (off/on/off/on, 20 steps each) instead of a single pair, quoting the 687 ms gap between the two off-arms as the noise floor, and defaulting the expensive hash guard to false in all 24 recipes so it only ever turns on from the command line — that is the shape of evidence that makes a number believable. Every one of the five threads @zyzhou5 opened came back with a measurement.

The feature earns its keep. The data plane moves the whole training batch between processes and had no per-op timing at all; frac_of_step is the only number that says whether optimising it is worth anything. Land it once the blocking item is fixed.

One blocking item. The comment on ppo-qwen2.5-1.5b-gsm8k-2n8g-megatron-valuetp2sp-dynbatch-noncolocated-async-single-controller.sh:28 explains it: that suite's config chain is the only one of the 24 with no data_plane.observability block left in it, so the new override kills the nightly at config parse. The fix needs enabled: true as well as verify_tensor_hash — see the comment for why restoring just the deleted lines is not enough.

Why this reached head invisibly. No job on this PR runs any of the 24 modified suite scripts. The label is CI:L1, which runs tests/functional/, and test_all_tests_can_find_config_if_dryrun exits in common.env before any uv run, so the override is never parsed. gh pr checks 3616 is green and says nothing at all about these scripts.

The PR description has gone stale. It still advertises step/percent_of_dataplane/by_cause/{fixed_overhead,transfer}, a "Reading the numbers correctly" bullet about the affine fit, and an "overhead/transfer split" column. grep -rn by_cause over *.py, *.md, *.yaml and *.sh at head returns nothing — b80b24cb4 removed the fit, and _BREAKDOWN_COLUMNS (observability.py:975-984) has no such column. step/wall_ms in that table should be step/wall_s. The README itself is correct throughout; only the description needs updating.

What was run here. 518 unit tests passed (15 skipped, 0 failed) across tests/unit/data_plane and tests/unit/algorithms/test_grpo.py. The three pinned ruff hooks pass. No GPU on this machine, so no nightly, no real TransferQueue, and nothing exercising mooncake or RDMA.

One last small thing: the single-controller copy of the log-before-commit fix has no unit test, while the grpo_sync copy does.

Generated by Claude Code

Comment thread nemo_rl/data_plane/observability.py Outdated
Comment thread nemo_rl/utils/logger.py Outdated
Comment thread tests/unit/data_plane/test_observability.py
Comment thread nemo_rl/algorithms/grpo_sync.py Outdated
Comment thread nemo_rl/data_plane/README.md Outdated
Comment thread nemo_rl/data_plane/observability.py Outdated
Comment thread nemo_rl/algorithms/grpo_sync.py Outdated
Comment thread nemo_rl/data_plane/worker_mixin.py Outdated
terrykong
terrykong previously approved these changes Sep 16, 2026
Zhiyu Li and others added 23 commits September 16, 2026 11:07
The 1.7 s end-to-end difference is not explained by the 119 ms of guard time
or the 297 ms of data-plane time. comm_volume_mb was unchanged, so the mirror
columns are not measurable payload and the payload explanation was wrong. Step
time varied 14.3-20.3 s within each run, guard-off was slower at step 10, and
the runs were on different nodes.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
Replaces the two-node comparison. Interleaved off/on/off/on, 20 steps each:
the guard costs ~180 ms/step by the counters (wall and self are disjoint, so
it is their sum, not self alone), and its effect on step time is unmeasurable
against a 0.6-0.9 s run-to-run floor -- the guard-on runs were 0.15 s faster.

The earlier 1.7 s was two nodes and noise. That the accounted cost sits inside
the floor is also the evidence the counters are not under-reporting.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
The DP ranks fetch in parallel and meet at the gradient all-reduce, so no rank
passes the barrier until the straggler has its shard -- the phase costs what
the slowest rank paid. That is why the reduction is a max: summing gives
process-time that exceeded the step, and averaging reports a cost no rank ever
paid.

Records the two things it still understates -- the driver's ops are serial
with the workers' fetches, and the rollout actor is in no scope -- and that
the barrier is at the all-reduce, so what leaks through is the straggler's
excess rather than its whole fetch.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
Scratch log of trials and dead-ends from the metrics-accuracy investigation.
Working notes, not a deliverable -- it went onto the branch via a broad
git add -A.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
Three findings from reviewing today's diff:

- step/self/frac is a ratio of two sums, so it is the wrapper's share of
  data-plane process-time and does not equal overhead_ms / (step/wall_s *
  1e3). Named rather than changed: sum over max is a ratio of nothing.
- The comment claimed the max is a lower bound on serial cost. It is not a
  bound either way -- it drops the driver's phase, which is serial with the
  fetches, and counts time that overlapped compute on the async path.
- test_cluster_frac_of_step used ten identical ranks, where max, mean and
  per-process share all coincide, so it passed under every reduction. Now one
  straggler against nine fast ranks: max 10%, mean 1.9%, sum 19%, and it
  asserts the first while rejecting the other two.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
No behaviour change.

exposed_ms claimed a property the README spends a paragraph refuting -- the
max drops the driver's phase, which is serial with the fetches, and counts
time that overlapped compute on the async path. It is the slowest process's
time, so it is slowest_ms.

The single-process fallback was wall_ms / n_procs. Correct -- at one process
the mean, sum and max coincide, and the only caller that could pass more
always has the key -- but it reads as the per-process mean this reduction
replaced. wall_ms says the same thing without the second look.

n_procs then had no use left in _step_metrics beyond the comments.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
Both its terms are summed across processes while wall_s is now a max, so the
two do not divide into each other. That is deliberate -- a sum over a max is a
ratio of nothing -- but a reader seeing them in the same step/self/ group will
try it, and the discrepancy is the DP degree rather than a bug.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
grpo_math_1B.yaml already sets observability.verify_tensor_hash: false, and
every one of these recipes reaches it through defaults -- directly, or via
grpo_math_1B_megatron.yaml. Restating false in each of them is dead config
that has to be kept in sync with the base for no effect.

The suites that need the guard already ask for it on the command line, so
this removes 48 lines and changes nothing.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
The test asserted that every algorithm logs data-plane metrics before
log_metrics(..., step_finished=True), because wandb accepted a log against a
committed step and silently discarded it.

main's Logger now buffers: _buffer_step_metrics_locked accumulates into
_pending_metrics keyed by step and flushes one row per step, with an atexit
drain. A log arriving after step_finished is buffered, not dropped, so source
order is no longer observable behaviour and the guard would only ever fail on
a legitimate reordering.

Same reasoning as taking main's logger.py during this rebase, which dropped
the _committed_step warning this PR had added for the same bug.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
Two blank-line slips from this branch's edits: an extra line left where
_as_i64 was removed, and a missing one before the read closure in
get_samples.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
_hash_field and _with_mirrors were defined twice, byte-identical. Python
shadows the first silently, so the 73 unit tests and ruff both passed; only
the docs build noticed, as autodoc2.dup_item warnings promoted to errors.

Scanned every changed .py in the branch by AST for other duplicate functions,
classes, methods and constants, and the changed configs for duplicate YAML
keys: this was the only one.

Signed-off-by: Zhiyu Li <zhiyul@nvidia.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
The PPO config chain defines no data_plane.observability, so the suite's
data_plane.observability.verify_tensor_hash=True override had no key to
set and Hydra raised at config parse under struct mode. enabled: true is
half the fix: without it no metrics wrapper is built and the suite's own
rows_checked > 0 gate fails instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
frac_of_step differenced a max over cumulative per-process totals. The
difference of maxima is not the maximum of differences: it equals the
straggler's step only when the cumulative leader is also this step's
straggler, and drifts toward the per-process mean the reduction replaced
as ranks grow (31% low at 16 ranks). Accumulate step_wall_ms per process
and reduce that with the max instead.

Also gates the hash deltas on rows_checked, so a process that only reads
still reports them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
The driver-scope fallback called get_step_metrics after the collect had
already closed the driver's step window, so every step/by_op/*/max_ms
read 0.0, and its baseline was never advanced on cluster steps, so the
first fallback after N cluster steps reported N steps as one. Both now
run off the snapshot the collect took, with a baseline per scope that is
advanced on both paths.

The state moves to TQPolicy with the client whose counters it
differences, replacing the out-of-band _prev_cluster_snapshot the
trainer set through getattr/setattr, and the four 'is observability on'
tests become one is_metrics_client predicate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
log_table was the only WandbLogger call passing step= to run.log
directly, and the only one outside _log_lock. Custom-axis streams (GPU
monitoring, on by default) advance wandb's internal step on their own,
so within a couple of steps the table targeted a step wandb had already
passed and the panel was dropped with no error. Routed through
_buffer_step_metrics_locked, the way log_plot already is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
…gate

README and docstrings still described the per-process mean, the dropped
latency fit, three hash stores and a step/{op}/max_ms key that is
emitted as step/by_op/{op}/max_ms. Records the second accepted limit of
the hash guard beside the first: an XOR fold cancels in pairs, so a
constant-valued row of even length has a digest that depends only on its
dtype and shape.

The nightly gates merge the cluster and driver key families instead of
preferring cluster, so a step whose fan-out failed -- the step most
likely to have something wrong with it -- is still read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
The driver's baseline was copied onto the policy while the client kept
its own, unused. get_step_metrics now accepts the snapshot its caller
already took -- a fan-out has to read the client first, and reading is
what closes the step window -- so the client stays the only owner and the
policy holds just the cluster baseline. That drops the string-keyed
baseline dict and the public step_metrics re-export the duplication
needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
…apshot

A default argument is evaluated eagerly, so `snap.get(cluster_key,
snap[single_key])` looked up the single-process key unconditionally and
raised on every cluster step. The metrics guard caught it and training
continued, so the only symptom was that no data_plane series were logged
at all. Found by an e2e run, not by review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
…rule

Replaces the hand-rolled second key name and the branch that had to pick
between the two. `step_wall_ms` joins `_SNAPSHOT_MAX`, so merge_snapshots
reduces it the same way it already reduces every other max field, and
both scopes read one key that means one thing: this step's data-plane
wall time, of the process that paid the most.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
The guard swallows by design, so silence was its only symptom: a KeyError
on every step of a real run logged one indistinguishable line per step,
published no data_plane/* series, and an empty panel reads the same as a
data plane that cost nothing. The first failure now carries its traceback
at ERROR -- the exception names a key, not the line that asked for it --
and later ones carry a running count, which separates 'broken since step
1' from 'flaked once'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
Answers why the field was missing: a merged snapshot is assembled key by
key from the merge tuples, not copied from a process snapshot, so a field
in neither tuple is simply absent from the cluster view -- and the reader,
which cannot tell the two shapes apart, raised on it and took every
data_plane/* series down with it.

The field is carried now, so this is belt and braces: the read falls back
to the summed wall time, which over-reports one series instead of
publishing none. Two tests pin it -- the merge contract (whatever the step
metrics read, the merged snapshot carries) and the degraded path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
…ounter

Two type errors from today's commits, caught by the lint job:

- `snap.get(key, default)` widens to include None for pyrefly, and the
  result feeds a division. Reads the key explicitly and branches on None,
  which keeps the graceful fallback and types cleanly.
- the panel-failure counter rebound a module int through `global`, which
  pyrefly rejects and which has no precedent elsewhere in nemo_rl. An
  itertools.count needs no rebinding, and 'give me the next number' was
  the only operation it ever wanted. Imported qualified: a bare `count`
  collides with two loop variables in this file (F402).

pyrefly clean and 369 passed / 3 skipped in the nightly container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@oci-aga-slurm-1-dm-02.cm.cluster>
@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia force-pushed the zhiyul/data_plane_observability_metrics branch from 74ecb40 to d60550e Compare September 16, 2026 19:13
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor Author

/ok to test d60550e

@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia enabled auto-merge (squash) September 16, 2026 23:51
@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia merged commit fb121a0 into main Sep 17, 2026
105 checks passed
@ZhiyuLi-Nvidia
ZhiyuLi-Nvidia deleted the zhiyul/data_plane_observability_metrics branch September 17, 2026 02:15
kajalj22 added a commit that referenced this pull request Sep 18, 2026
resolves the pyrefly lint failure unrelated to this branch's changes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:L1 Run doctests, unit tests, and functional tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants