Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
2229bf2
feat(data-plane): track data-plane time, latency percentiles and byte…
ZhiyuLi-Nvidia Aug 12, 2026
96a2cd4
feat(data-plane): emit per-step data-plane metrics from the sync trainer
ZhiyuLi-Nvidia Aug 13, 2026
79f09a9
refactor(data-plane): address /simplify review of the metrics diff
ZhiyuLi-Nvidia Aug 13, 2026
e57a882
perf(data-plane): cut metrics overhead, add optional wire-hash check
ZhiyuLi-Nvidia Aug 21, 2026
70b8a30
fix(data-plane): fingerprint jagged leaves; simplify the hash to one …
ZhiyuLi-Nvidia Aug 22, 2026
57fe723
perf(data-plane): scope jagged fingerprints to the buffer, not a rect…
ZhiyuLi-Nvidia Aug 22, 2026
55e00aa
fix(data-plane): count fields dropped as incomparable
ZhiyuLi-Nvidia Aug 22, 2026
0b83e2b
refactor(data-plane): trim the observability diff after review
ZhiyuLi-Nvidia Aug 22, 2026
040d459
perf(data-plane): size nested leaves from their packed values buffer
ZhiyuLi-Nvidia Aug 22, 2026
7fc6aea
refactor(data-plane): name the real hazard in _tensor_bytes
ZhiyuLi-Nvidia Aug 22, 2026
839afb6
feat(data-plane): enable data-plane metrics by default
ZhiyuLi-Nvidia Aug 22, 2026
f137600
perf(data-plane): hold one byte total per partition, not one per key
ZhiyuLi-Nvidia Aug 22, 2026
caec8c5
fix(data-plane): one unit per dimension, and a tail metric that moves
ZhiyuLi-Nvidia Aug 22, 2026
79aaebf
fix(data-plane): report the latency split in ms, not as a ratio
ZhiyuLi-Nvidia Aug 22, 2026
c1f4275
fix(data-plane): scope the metric prefix to the driver that produced it
ZhiyuLi-Nvidia Aug 22, 2026
7894024
feat(data-plane): aggregate metrics across processes, and report thei…
ZhiyuLi-Nvidia Aug 22, 2026
3b7d3f2
fix(data-plane): bill the fan-out to observability, not just the wrapper
ZhiyuLi-Nvidia Aug 22, 2026
a12c233
fix(data-plane): a max below its own median, and a fraction above 1
ZhiyuLi-Nvidia Aug 22, 2026
7776cee
fix(data-plane): make every series say whether it is a delta or a level
ZhiyuLi-Nvidia Aug 22, 2026
c473775
fix(data-plane): free only the keys a clear actually held
ZhiyuLi-Nvidia Aug 22, 2026
f0e2a4f
fix(data-plane): clamp percentiles where they are derived, not at one…
ZhiyuLi-Nvidia Aug 22, 2026
c87ebd4
refactor(data-plane): cleanup pass, and two bugs it turned up
ZhiyuLi-Nvidia Aug 22, 2026
0a539ff
feat(data-plane): log a per-op breakdown table, not just series
ZhiyuLi-Nvidia Aug 22, 2026
85589f9
fix(data-plane): put the latency split in the cluster view too
ZhiyuLi-Nvidia Aug 22, 2026
f73fe2e
fix(data-plane): per-op cluster time also reads as elapsed when it is…
ZhiyuLi-Nvidia Aug 22, 2026
acd6fa6
fix(data-plane): report per-op time per call, which is the invariant one
ZhiyuLi-Nvidia Aug 22, 2026
96b80f9
fix(data-plane): split the latency per call, like the mean it explains
ZhiyuLi-Nvidia Aug 22, 2026
fcbffe4
fix(data-plane): gate percentiles per quantile; per-row hashes when r…
ZhiyuLi-Nvidia Aug 22, 2026
38950f9
fix(data-plane): report p90, not p99, as the per-op tail
ZhiyuLi-Nvidia Aug 22, 2026
3e51c58
feat(data-plane): chart time shares, not 32 per-op series
ZhiyuLi-Nvidia Aug 22, 2026
f1652a0
refactor(data-plane): name the metric for its denominator, share -> t…
ZhiyuLi-Nvidia Aug 22, 2026
e295826
refactor(data-plane): time_pct -> pct_of_dataplane
ZhiyuLi-Nvidia Aug 22, 2026
6329f21
refactor(data-plane): spell out percent in the metric names
ZhiyuLi-Nvidia Aug 22, 2026
f4fa1f5
docs(data-plane): the transfer term IS the bandwidth term
ZhiyuLi-Nvidia Aug 22, 2026
f03ca4b
fix(data-plane): report hash counters and measuring cost in both scopes
ZhiyuLi-Nvidia Aug 22, 2026
a19317d
fix(data-plane): log before the step commits; track per-op transfer v…
ZhiyuLi-Nvidia Aug 23, 2026
70f888a
fix(data-plane): stop the hash guard failing every per-row field
ZhiyuLi-Nvidia Aug 23, 2026
52ab0c2
fix(data-plane): make a hash mismatch adjudicable, and measure what i…
ZhiyuLi-Nvidia Aug 23, 2026
e938ac1
refactor(data-plane): apply the /simplify review
ZhiyuLi-Nvidia Aug 23, 2026
bf8ffb9
refactor(data-plane): narrow this PR back to the data plane
ZhiyuLi-Nvidia Aug 23, 2026
6ebf71c
fix(data-plane): satisfy pyrefly on the three types the diff loosened
ZhiyuLi-Nvidia Aug 24, 2026
33468e2
style(data-plane): sort the test imports
ZhiyuLi-Nvidia Aug 24, 2026
f51c500
fix(data-plane): salt hash fingerprints on the host
ZhiyuLi-Nvidia Aug 24, 2026
f35e6cb
feat(data-plane): log data-plane metrics from the single-controller loop
ZhiyuLi-Nvidia Aug 24, 2026
58a49b5
test(data-plane): simplify observability tests; guard the metrics path
ZhiyuLi-Nvidia Aug 27, 2026
9867ee9
test(data-plane): gate every data-plane nightly on the wire guard
ZhiyuLi-Nvidia Aug 27, 2026
79efb1d
refactor(data-plane): drop the latency/bandwidth fit
ZhiyuLi-Nvidia Aug 27, 2026
da99feb
feat(data-plane): report jagged pack/unpack time
ZhiyuLi-Nvidia Aug 29, 2026
888d8fd
refactor(data-plane): report charted durations in seconds
ZhiyuLi-Nvidia Aug 30, 2026
8af5563
fix(data-plane): make the wire guard see a row's shape
ZhiyuLi-Nvidia Aug 30, 2026
dd83c8c
fix(data-plane): log single-controller metrics before the step is com…
ZhiyuLi-Nvidia Sep 7, 2026
dd3095b
fix(data-plane): drop the PROMOTE_1D_FIELDS branch from _from_wire
ZhiyuLi-Nvidia Sep 7, 2026
8fe9a4a
refactor(data-plane): share the duplicated observability wiring
ZhiyuLi-Nvidia Sep 9, 2026
e0fdc93
chore(data-plane): satisfy ruff, ruff-format and pyrefly on the obser…
ZhiyuLi-Nvidia Sep 9, 2026
546827f
fix(data-plane): satisfy pyrefly in the observability module
ZhiyuLi-Nvidia Sep 9, 2026
a013ada
fix(data-plane): release fingerprints when another process clears the…
Sep 10, 2026
cc054da
fix(data-plane): reconcile the accounting for samples another process…
Sep 10, 2026
6f51dbe
refactor(data-plane): drop the reconcile warning, keep the release
Sep 10, 2026
8839675
feat(data-plane): make the wire guard check across processes
Sep 10, 2026
3de6cba
Revert "feat(data-plane): make the wire guard check across processes"
Sep 10, 2026
677f2d2
feat(data-plane): carry the wire-in digest beside the field it describes
Sep 10, 2026
c78cc8d
test(data-plane): rewrite the hash tests against the mirror columns
Sep 10, 2026
6d2495f
fix(data-plane): one definition of the mirror column list
Sep 10, 2026
9619fc2
test(data-plane): one sanity check, not five
Sep 10, 2026
8cbe1a9
test(data-plane): the later writer stamps too
Sep 10, 2026
ea28e41
perf(data-plane): fold the field digests on tensors, and say so in th…
Sep 10, 2026
0f6e014
test(data-plane): assert the wire guard looked, not just that it foun…
Sep 10, 2026
84986df
test(data-plane): gate on what the guard did, not on what it did not
Sep 10, 2026
d1cd96f
docs(data-plane): frac_of_step is carried work, not exclusive time
Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/configs/grpo_math_1B.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,9 @@ data_plane:
# serialized groups and cost throughput;
# higher buys little at linear HBM cost.
# GDR needs that headroom to pay off.
# observability: # NotRequired
# enabled: false
observability: # per-op data-plane timing/volume
enabled: true # per-op timing/volume; cost is below measurement noise
verify_tensor_hash: false # debug: per-row hash of each row's values+dtype+shape, wire-in vs wire-out

# Multi-Teacher On-Policy Distillation (MOPD): distills from one or more teacher
# models into the policy via token-level teacher-minus-student logprob advantages,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-deepscaler-1.5b-8K.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-gemma3-1b-it-1n8g-fsdp2tp1.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-gspo-deepscaler-1.5b-8K.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-llama3.1-8b-instruct-1n8g-megatron-fp8-rollouts.v3.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ checkpointing:
# TransferQueue data plane is mandatory for the SingleController path.
data_plane:
enabled: true
observability:
verify_tensor_hash: true

# SC async-RL runtime knobs.
async_rl:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-llama3.1-8b-instruct-2n8g-fsdp2tp1-noncolocated.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ defaults: grpo-llama3.1-8b-instruct-2n8g-megatron-fp8-e2e.yaml
data_plane:
enabled: true
backend: mooncake_cpu
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-llama3.2-1b-instruct-1n8g-fsdp2tp1.v3.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ defaults: grpo-llama3.2-1b-instruct-1n8g-fsdp2tp2-temp0.8-topp0.9-topk50.yaml
data_plane:
enabled: true
backend: mooncake_cpu
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ defaults: grpo-llama3.2-1b-instruct-1n8g-megatron-temp0.8-topp0.9-topk50.yaml
data_plane:
enabled: true
backend: mooncake_cpu
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-llama3.2-1b-instruct-1n8g-megatron.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ defaults: grpo-llama3.2-1b-instruct-1n8g-megatron_generation.yaml
data_plane:
enabled: true
backend: mooncake_cpu
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-moonlight-16ba3b-4n8g-megatron.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ defaults: grpo-nanov3-30BA3B-1n8g-fsdp2.v2.yaml
data_plane:
enabled: true
backend: mooncake_cpu
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-nanov3-30BA3B-2n8g-megatron-pack-cp.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
defaults: grpo-qwen2.5-math-1.5b-instruct-1n8g-fsdp2tp1.v3.yaml
data_plane:
enabled: true
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ checkpointing:
# TransferQueue data plane is mandatory for the SingleController path.
data_plane:
enabled: true
observability:
verify_tensor_hash: true

# SC async-RL runtime knobs.
async_rl:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ defaults: grpo-qwen3-1.7b-1n8g-megatron-eagle3.yaml
data_plane:
enabled: true
backend: mooncake_cpu
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ checkpointing:
# TransferQueue is mandatory for the SingleController path.
data_plane:
enabled: true
observability:
verify_tensor_hash: true

async_rl:
sampler:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ checkpointing:
checkpoint_dir: results/grpo-qwen3-30ba3b-8n8g-megatron-cp2-r3-tq_simple
data_plane:
enabled: true
observability:
verify_tensor_hash: true
logger:
log_dir: logs/grpo-qwen3-30ba3b-8n8g-megatron-cp2-r3-tq_simple
wandb:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ defaults: grpo-qwen3-8B-base-1n8g-fsdp2-lora.yaml
data_plane:
enabled: true
backend: mooncake_cpu
observability:
verify_tensor_hash: true
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ async_rl:

data_plane:
enabled: true
observability:
verify_tensor_hash: true

checkpointing:
checkpoint_dir: results/mopd-qwen3-1.7b-3n8g-megatron-pack-single-controller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ data_plane:
local_buffer_size: 4294967296
reuse_registered_buffers: true
staging_buffer_size: 268435456
observability:
verify_tensor_hash: true

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.

Should we turn this off for normal recipes and only turn it on in the automated test launch script?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right for this file, though it splits by kind. 19 of the 24 YAMLs that set this are -tq_simple / -tq_mooncake wrappers — CI-only configs that inherit the base recipe and flip data_plane.enabled. For those the flag already is in the test config, and common-tq.env:8 says data_plane.* overrides belong in the wrapper YAML rather than the launch script.

The 5 that match your concern are the single-controller recipes with no wrapper: this one, plus grpo-llama3.1-8b-...-streaming2, grpo-qwen2.5-math-1.5b-...-sync, grpo-qwen3-30ba3b-10n8g-...-async-single-controller and mopd-qwen3-1.7b-...-single-controller. Those are user-facing configs and I agree the guard shouldn't be on by default in them.

Two ways to fix, happy to take either:

  • (a) drop the line from all 5 and add ++data_plane.observability.verify_tensor_hash=True to their test-suite .sh — loses the wrapper convention, keeps CI coverage.
  • (b) give them -tq_*-style wrapper YAMLs like the other 19 — keeps the convention, costs 5 new files.

I lean (a): they already have their own .sh, and 5 wrapper YAMLs to preserve a convention that exists to avoid CLI overrides is more machinery than the problem justifies. It does mean common-tq.env:8 needs an "except the single-controller recipes" caveat. Which would you prefer?


# SC async-RL runtime knobs, replacing the nulled ppo.async_ppo block.
async_rl:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ defaults: prorlv2-qwen2.5-math-1.5b-instruct-1n8g-fsdp2tp1.v2.yaml
data_plane:
enabled: true
backend: mooncake_cpu
observability:
verify_tensor_hash: true
69 changes: 69 additions & 0 deletions nemo_rl/algorithms/grpo_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import gc
import os
import time
import warnings
from typing import TYPE_CHECKING, Any, Optional

Expand Down Expand Up @@ -78,6 +79,13 @@
from nemo_rl.data.llm_message_utils import batched_message_log_to_flat_message
from nemo_rl.data.multimodal_utils import present_multimodal_fields
from nemo_rl.data_plane.interfaces import KVBatchMeta
from nemo_rl.data_plane.observability import (
MetricsDataPlaneClient,
cluster_step_metrics,
log_step_metrics,
merge_snapshots,
metrics_never_fail_the_step,
)
from nemo_rl.data_plane.schema import DP_CALIB_INPUT_FIELDS, DP_TRAIN_FIELDS
from nemo_rl.distributed.batched_data_dict import BatchedDataDict
from nemo_rl.environments.interfaces import EnvironmentInterface
Expand Down Expand Up @@ -381,6 +389,63 @@ def _compute_seq_logprob_error_metrics(
return masking_data["sample_mask"], seq_logprob_error_metrics


def _log_data_plane_metrics(
policy: Any, logger: Logger, step: int, total_step_time: float
) -> None:
"""Log this step's data-plane cost. Never raises.

On by default, so this runs every step of every recipe.
"""
with metrics_never_fail_the_step(step):
_log_data_plane_metrics_impl(policy, logger, step, total_step_time)


def _log_data_plane_metrics_impl(
policy: Any, logger: Logger, step: int, total_step_time: float
) -> None:
"""Log this step's data-plane cost. No-op unless observability is enabled.

Prefers the cluster view -- the driver's counters plus every policy
worker's, summed -- and falls back to the driver's alone when the
fan-out reaches only one process. Reported one way or the other, never
both, so there is a single answer to "what did the data plane cost"
rather than two that disagree by roughly the DP degree.

The prefix names the scope because the two differ by a lot: the driver
issues about one op of each kind per step while the bulk traffic is the
workers' per-DP-rank ``get_samples``. Note that even the cluster view
omits the rollout actor, which builds its own client and is not on the
worker group -- so ``kv_first_write`` is not in these totals.

The previous reading lives on the policy, alongside the client whose
counters it differences, rather than in module state: two trainers in
one process would otherwise interleave one ``prev`` and produce
negative deltas.
"""
client = getattr(policy, "dp_client", None)
if not isinstance(client, MetricsDataPlaneClient):
return # observability disabled -> plain adapter

collect = getattr(policy, "collect_data_plane_snapshots", None)
collect_started = time.perf_counter()
snapshots = collect() if callable(collect) else []
if len(snapshots) > 1:
merged = merge_snapshots(snapshots)
# The fan-out is part of what observability costs, and the larger
# part: omitting it reported a twentieth of the real bill.
collect_ms = (time.perf_counter() - collect_started) * 1e3
prev = getattr(policy, "_prev_cluster_snapshot", {})
metrics = cluster_step_metrics(
merged, prev, total_step_time, collect_ms=collect_ms
)
policy._prev_cluster_snapshot = merged
log_step_metrics(logger, metrics, step, "cluster")
else:
# Single process, or the fan-out could not reach the workers.
metrics = client.get_step_metrics(total_step_time)
log_step_metrics(logger, metrics, step, "driver")


def grpo_train_sync(
policy: ColocatablePolicyInterface,
policy_generation: GenerationInterface,
Expand Down Expand Up @@ -1375,6 +1440,10 @@ def grpo_train_sync(
logger.log_metrics(
performance_metrics, total_steps + 1, prefix="performance"
)
# Before the step_finished=True log below, which commits the step:
# anything logged against a committed step is dropped by wandb, so
# these series were computed, printed, and silently discarded.
_log_data_plane_metrics(policy, logger, total_steps + 1, total_time)
logger.log_metrics(
timing_metrics,
total_steps + 1,
Expand Down
42 changes: 42 additions & 0 deletions nemo_rl/algorithms/single_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@
from nemo_rl.data.multimodal_utils import present_multimodal_fields
from nemo_rl.data_plane import DATA_PLANE_CHECKPOINT_SCHEMA_VERSION, KVBatchMeta
from nemo_rl.data_plane.async_utils import call_data_plane
from nemo_rl.data_plane.observability import (
MetricsDataPlaneClient,
log_step_metrics,
metrics_never_fail_the_step,
)
from nemo_rl.data_plane.schema import (
DP_CALIB_INPUT_FIELDS,
DP_TRAIN_FIELDS,
Expand Down Expand Up @@ -1519,6 +1524,38 @@ async def _cleanup_consumed_metas_unlocked(
if errors:
raise BaseExceptionGroup("post-train DataPlane cleanup failed", errors)

def _log_data_plane_metrics(self, total_step_time: float) -> None:
"""Log this step's data-plane cost. Never raises.

On by default, so this runs every step of every recipe. Mirrors
``grpo_sync._log_data_plane_metrics``.
"""
with metrics_never_fail_the_step(self._train_steps):
self._log_data_plane_metrics_impl(total_step_time)

def _log_data_plane_metrics_impl(self, total_step_time: float) -> None:
"""Log this step's data-plane cost. No-op unless observability is enabled.

The synchronous loop logs these series from ``_log_data_plane_metrics``
in ``grpo_sync``. Without the same call here the single-controller path
builds the metrics client, pays for its counters on every op, and emits
nothing -- the failure is silent, because an empty dashboard looks the
same as a data plane that cost nothing.

Driver scope only, and the prefix says so. This client issues the
advantage stage's get plus the post-train clear; the bulk traffic is
the trainer and generation workers' own clients, in their own
processes with their own counters, so ``comm_volume_mb`` here is well
under what the job actually moved. ``grpo_sync`` gets a cluster view by
fanning out over its policy worker group; this loop has no such group to
fan out over, so driver scope is all there is here.
"""
if not isinstance(self._dp_client, MetricsDataPlaneClient):
return # observability disabled -> plain adapter

metrics = self._dp_client.get_step_metrics(total_step_time)
log_step_metrics(self._logger, metrics, self._train_steps, "driver")

@staticmethod
def _group_ids_from_meta(meta: KVBatchMeta) -> list[str]:
"""Return stable prompt-group IDs in canonical sample order."""
Expand Down Expand Up @@ -2849,6 +2886,11 @@ async def _train_pump(self) -> None:
self._logger.log_metrics(
step_metrics, step=self._train_steps, prefix="train"
)
# Must precede the step_finished=True log below. That log commits
# the wandb step, and wandb silently discards anything logged
# against a step it has already committed -- no exception, no
# failed return, just an empty chart. grpo_sync had the same bug.
self._log_data_plane_metrics(total_time)
# step_finished=True here since this is the final log of our current step.
self._logger.log_metrics(
timing_metrics,
Expand Down
Loading
Loading