refactor!: replace shipped RL instruments with a consumer-driven metric registry - #46
Merged
Merged
Conversation
ahmadki
requested changes
Aug 27, 2026
…ic registry
Lens stops shipping instruments/rl.py. Metric families whose names belong to a
consuming framework are now declared by that framework through a generic
registry and recorded against it, so lens never has to carry — and the consumer
never has to import, extend, or override — a per-consumer instrument module.
New nemo.lens.instruments API (re-exported at the package level):
- MetricSpec(key, name, kind, unit, description): one series; kind is one of
gauge / histogram / counter / up_down_counter, validated at construction.
- register_metric_group / unregister_metric_group / registered_metric_groups:
a process-global, lock-guarded group registry mirroring strategies.py.
- record_metrics(meter, group, values=None, *, attributes=None, **kwargs):
lazily creates and per-meter caches instruments, skips None, and never
raises into the caller (unknown group/key or a failed create/emit is logged
and swallowed, as with every instrument path).
RL names move to the consumer: the rl.* constants are removed from
nemo.lens.semconv (NeMo-RL owns them now, like megatron.*). A reset_metric_registry
autouse fixture isolates the registry across tests.
BREAKING CHANGE: nemo.lens.instruments.rl and record_rl_metrics are removed.
Consumers register the "rl" group via register_metric_group and emit with
record_metrics(meter, "rl", ...). NeMo-RL needs a paired PR to declare its rl.*
group; the metric record functions are not part of the five-symbol fallbacks
surface, so fallbacks.py is unchanged.
Signed-off-by: Raj Singh <rajsin@nvidia.com>
- record_metrics: make meter/group/values positional-only so a consumer may declare metric keys named "meter"/"group"/"values" and pass them as kwargs without colliding with the parameters (attributes stays the one reserved keyword). Fixes a path that could raise into the caller. - Warnings log once per (group) / (group, key) via a small _warn_once helper and a dedicated _WARN_LOCK, instead of on every call; (re)registration clears a group's warnings. Prevents log floods in the training hot loop. - semconv: keep the rl.* span-attribute names (rl.algorithm, rl.reward, rl.generation.backend, rl.num_rollouts); only the rl.* metric-name constants move to the NeMo-RL consumer. This PR stays metrics-only. - Fork safety: register os.register_at_fork handlers (before/after_in_parent/ after_in_child) so the registry locks held at fork are reinitialised in the child rather than inherited locked and deadlocking, matching CPython logging. - Tests for each: kwarg/param-name collisions, warn-once, and fork safety (deterministic reinit + a real forked child guarded by an alarm). strategies.py has the same latent fork issue but is left untouched to keep this PR scoped to metrics. Signed-off-by: Raj Singh <rajsin@nvidia.com>
rrs45
force-pushed
the
raj/metrics-registry
branch
from
August 27, 2026 20:48
8b7bbc0 to
193f88d
Compare
Member
|
/ok to test 193f88d |
ahmadki
requested changes
Aug 28, 2026
record_metrics promises never to raise into the caller, but three argument paths sat outside every guard: - dict(attributes) raised TypeError when a consumer declared a metric key named "attributes" and passed it as a keyword, since it binds to the reserved parameter instead. - merged.update(values) raised TypeError/ValueError for a non-mapping values argument, an easy positional slip. - entry.instruments.get(meter) raised TypeError for any meter that cannot be weak-referenced, None being the likely mistake. The instrument cache is a WeakKeyDictionary, so the lookup threw before the existing guard around instrument creation could catch it. Normalize both arguments under a guard that warns once and degrades: a bad values is skipped, a bad attributes records the call without attributes, and an unusable meter is reported with its type. Also fixes an attributes-only call losing its data with no warning at all, since the empty-merged early return happened before attributes were ever inspected. Signed-off-by: Raj Singh <rajsin@nvidia.com>
Member
|
/ok to test 9bdcae8 |
@ahmadki, there was an error processing your request: See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/2/ |
Member
|
/ok to test 1c970e9 |
ahmadki
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Lens stops shipping
instruments/rl.py. Metric families whose names belong to a consuming framework (RL series in NeMo-RL) are now declared by that consumer through a generic registry and recorded against it, so lens never has to carry — and the consumer never has to import, extend, or override — a per-consumer instrument module. This applies to metrics the same delegation PR #42 applied to rank/world-size.Why?
The consuming frameworks already emit metrics and create new ones as they evolve. It is best to manage OTel metrics within the frameworks to maintain consistency — without having to override or extend a lens-side definition and add overhead every time a metric changes. Lens remains an abstraction layer for defining and emitting OTel events, allowing the frameworks to drive naming conventions and standards.
New
nemo.lens.instrumentsAPI (re-exported at the package level)MetricSpec(key, name, kind, unit, description)— one series;kind∈{gauge, histogram, counter, up_down_counter}, validated at construction.keyis the identifier the caller records under (so it must be a Python identifier);nameis the OTel name actually emitted (so it can be dotted, e.g.rl.reward.mean).register_metric_group/unregister_metric_group/registered_metric_groups— a process-global, lock-guarded group registry mirroringstrategies.py.record_metrics(meter, group, values=None, /, *, attributes=None, **kwargs)— lazily creates and per-meter caches instruments, skipsNone, and never raises into the caller.meter/group/valuesare positional-only so a consumer may declare metric keys with those names;attributesis the one reserved keyword.Usage
Never raises into the caller
record_metricssits in a per-step loop, so every failure mode is logged once and skipped rather than raised: an unregistered group, an unknown key, aNonevalue, a malformedvaluesorattributesargument, an unusablemeter, and a failed instrument create or emit. Warnings are deduplicated per(group)and per(group, key)so a misconfigured call cannot flood the log.Fork safety
os.register_at_fork(before / after_in_parent / after_in_child)holds both module locks across a fork so the child starts from a consistent registry, then replaces them with freshLock()s in the child — the same approach CPython'sloggingmodule takes. Athreading.Lockinherited in a locked state has no thread left to release it and would deadlock the child on its next acquire.Also
rl.*metric-name constants fromnemo.lens.semconv— NeMo-RL owns those now, declared via the registry. Therl.*span-attribute names (rl.algorithm,rl.reward,rl.generation.backend,rl.num_rollouts) stay insemconv; this PR is metrics-only.reset_metric_registryautouse fixture to isolate the registry across tests.docs/user-guide/metrics.mdx,docs/design/semconv.mdx, and theAGENTS.mdinstruments gotcha.BREAKING CHANGE
nemo.lens.instruments.rlandrecord_rl_metricsare removed. Consumers register the"rl"group viaregister_metric_groupand emit withrecord_metrics(meter, "rl", ...).Before your PR is "Ready for review"
Pre checks:
I read the Contributor guidelines.
All commits are signed off (
git commit -s) per the DCO.I added or updated tests under
tests/for any behavior change.I ran
pytestandpre-commit run --all-fileslocally; both pass (239 passed, registry coverage 98%).I updated
docs/where behavior, configuration, or public API changed.If this changes a lens public API symbol, I updated the cross-repo
_fallbacks.pyfiles in Megatron-LM / RL / Gym.Cross-repo note: the removed/added symbols are metric record functions, which are not part of the five-symbol
_fallbacks.pysurface (trace_fn,managed_span,span_cm,is_span_group_enabled,safe_set_span_attributes), so no consumer_fallbacks.pyedit is required. However, NeMo-RL needs a paired PR inRL/nemo_rl/telemetry/to declare itsrl.*group viaregister_metric_group(...)and replacerecord_rl_metrics(meter, ...)calls withrecord_metrics(meter, "rl", ...). Megatron-LM and NeMo-Gym do not callrecord_rl_metricsand are unaffected.