Configuration#
Telemetry is configured by the telemetry: block of your run config. Keep it there: a run’s telemetry settings should be recoverable from the file that describes the run, not from whatever happened to be in a shell.
Two things do belong in the environment, because they describe where you are running rather than what you are measuring: the standard OTEL_EXPORTER_OTLP_* endpoint/protocol/headers, and OTEL_SERVICE_NAME.
The telemetry: config block#
telemetry: is an optional top-level field of every algorithm’s MasterConfig. It is documented here, not baked into the exemplar configs — add it to your own run config.
telemetry:
enabled: false # master switch; when false, every site is a ~0-cost no-op
service_name: nemo-rl # service.name reported to the backend
span_groups: default # preset (default | per_step | all) or a comma-separated group list
traces_enabled: true # emit trace spans
metrics_enabled: true # emit the rl.* metric instruments
logs_enabled: false # bridge Python logging to OTel logs (trace-correlated)
exporter: otlp # otlp | console
vllm_native_tracing: false # opt in to vLLM's own OTLP tracing (gRPC by default, one span per request — see vllm-tracing.md)
The defaults above are the field defaults of TelemetryConfig (nemo_rl/telemetry/config.py). The endpoint, headers, and protocol are not in this block — they come from the standard OTEL_EXPORTER_OTLP_* env vars (see below).
Every process that enables telemetry exports — driver, workers, and singleton actors alike. There is no rank-filtering setting; see Which ranks export.
service_name maps onto the standard OTEL_SERVICE_NAME (lens reads it unprefixed), so setting either works.
For the full config model, field semantics, and validation rules, see lens: configuration.
How the settings reach the workers#
Ray actors do not inherit the driver’s Python objects, so on the driver init_telemetry_driver projects the block into NEMO_RL_OTEL_* environment variables before init_ray(); the resulting environment is snapshotted into the Ray runtime_env and every worker rebuilds the same config from it.
These variables are a transport, not a second configuration interface. They are listed here so that a NEMO_RL_OTEL_* name in a log or a ps output is recognisable, and because two of them have no telemetry: equivalent:
Variable |
Meaning |
|---|---|
|
Correlates the driver and every worker to one run. Generated from |
|
Optional user/team label, read by lens. |
The projection uses os.environ.setdefault, so a variable already present in the environment wins over the YAML value. That is deliberate for the two above, which a job scheduler supplies. For every other setting, prefer the config: a NeMo-RL toggle set in a shell leaves no trace of who set it or why, and splits a run’s configuration between a file and an environment with nothing recording which half came from where. The resolved settings are logged once at init for exactly this reason, and a hydra-style ++telemetry.<field>=<value> override covers the one-off case without leaving the config record.
Standard OTel SDK variables#
Endpoint, protocol, and headers are honoured by the OTel SDK directly:
Variable |
Example |
|---|---|
|
|
|
|
|
|
|
|
Pick the protocol to match your backend: a local collector or Jaeger typically speaks gRPC on :4317; a direct-to-SaaS OTLP endpoint typically speaks http/protobuf on :443. See Observability Stack.
Which ranks export#
Every process that enables telemetry exports. nemo-lens has no notion of rank: it does not select among ranks and does not sample by rank, so NeMo-RL has no export_strategy, export_rank, export_sample_rate, or sampler_enabled setting. Each process instead labels itself, and narrowing the fleet down is a decision you make with those labels:
Filter in the collector. Every span and metric carries
nv.dl.rankandnv.dl.world_sizeas resource attributes. Drop or keep whichever ranks you want in an OpenTelemetry Collector processor — the collector is also the only place that sees the whole fleet at once, which is where a policy about it belongs.There is no per-rank switch.
telemetry.enabledis one job-wide value that every worker inherits through the Rayruntime_env, so it cannot keep some ranks quiet while others export. Filter onnv.dl.rankin the collector instead. Setting it false does disable the whole job cheaply — a process with telemetry off gets an empty span-group set, sois_span_group_enabled()isFalseeverywhere and no span objects are created at all.
RANK is group-local: the policy group and the generation group each number their workers from zero, so nv.dl.rank is only unambiguous together with the rl.worker_group attribute that names which group it belongs to. Filter on the pair.
If you previously set export_strategy or one of its companions, the key still parses — TelemetryConfig allows extra keys — but it no longer does anything. It is not projected into the worker environment either, so nothing downstream reads it.
Run identification#
Every run gets an id that flows to all backends as the nemo.run.id resource attribute, shared by the driver and every worker. Note the spelling: run_id is the config field you set, nemo.run.id is the attribute key lens emits and the one your backend indexes.
Priority order:
NEMO_RL_OTEL_RUN_ID(explicit, highest priority).SLURM_JOB_ID(auto-detected on SLURM clusters).Auto-generated 12-character hex id (fallback).
The run id is written to the environment on the driver before init_ray(), so every worker inherits the same value and correlates to the same run. This is also how vLLM’s native spans are correlated back to the RL run — see vLLM Tracing.
Filter by nemo.run.id in your backend to isolate a specific run.
Resource attributes#
Each process sets stable-for-the-run values on its OTel Resource — init_telemetry_driver on the driver, init_telemetry_worker in each Ray actor — so they appear on every span/metric as backend “Process” tags:
Attribute |
Source |
|---|---|
|
the run id resolved above ( |
|
the lifecycle stage this run occupies: |
|
the |
|
|
|
|
|
always the literal |
|
lens’s |
|
TP size, read from whichever backend |
|
PP size, Megatron runs only — Automodel has no pipeline dimension |
|
|
|
|
|
|
|
on Megatron |
|
Megatron runs only: |
|
|
|
versions as loaded in the process that opened the span, not as pinned in the lockfile — they are per process, so filter on |
|
the installed distribution’s version, read from package metadata rather than by importing it — importing transformer-engine loads its PyTorch extension in every process that sets up telemetry. Omitted when it is not installed, which is every process except Megatron and Automodel workers: its absence on a driver, controller or vLLM span does not mean the run lacked TE |
|
this process’s rank and group size ( |
|
worker processes only: |
|
worker processes only: the worker group’s |
The config-derived rows are driver-only: a worker is handed the propagated environment, not the MasterConfig. Attribute construction is best-effort: a missing config key simply omits that attribute; it never raises. Plus auto-detected host / GPU / SLURM / Kubernetes attributes from lens’s resource detection.
Shared nv.dl.* keys are taken from nemo.lens.semconv rather than written as strings, so a typo is an ImportError instead of silent data loss. Note the two spellings of the parallelism sizes: nv.dl.topology.size.* is the resource-scoped one and the dl.tensor_parallel.* family is the span and metric spelling of the same quantity. That split is a convention this repo keeps, not something lens checks — normalize_resource_attributes runs only over the attributes lens derives from OTEL_RESOURCE_ATTRIBUTES, and the resource_attributes RL passes in are merged as they are. Either spelling would arrive intact, so the wrong one is a silently misnamed series rather than a failure.
Lens also declares nv.dl.topology.size.dp, which NeMo-RL does not report. RL configures no data-parallel size, so it could only be derived by dividing the cluster’s GPU count by the model-parallel sizes — and that assumes the policy owns every GPU in the cluster. A reward model with its own resources block, distillation teachers, expert parallelism and non-colocated generation each leave the policy fewer, and the division comes out too high rather than failing. Read the worker’s device mesh if you need the real number.
Typical configurations#
Each example puts the NeMo-RL settings in the config and only the destination in the environment. The ++ form is a hydra-style CLI override: it is applied to the config and echoed into the run’s log, so a one-off stays as traceable as an edit to the YAML.
Console exporter (no backend)#
uv run examples/run_grpo.py --config examples/configs/grpo_math_1B.yaml \
++telemetry.enabled=true ++telemetry.exporter=console
Spans and metrics print to stdout — a quick dry run with no backend to stand up.
Direct to an OTLP backend (http/protobuf)#
export OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-otlp-endpoint>:443
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS="<header>=<value>" # any auth headers your backend requires
uv run examples/run_grpo.py --config examples/configs/grpo_math_1B.yaml \
++telemetry.enabled=true
See Observability Stack for the full backend-export setup.
Per-step granularity#
telemetry:
enabled: true
span_groups: per_step
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
per_step adds the per-step phases (rollout, generation, reward, advantage, policy update) on top of the run-level job span, so the whole run is one trace. See Span Groups.