Semantic Conventions
nemo.lens.semconv centralizes the attribute names NeMo Lens emits. The
attributes module owns names and version metadata, while resources owns
Resource field sets and type mappings. Existing imports from nemo.lens.semconv
remain supported. Two categories of attributes coexist:
- Standard OTel semconv:
gen_ai.*andk8s.*. These track upstream OTel specs. (Resource attributes likeservice.*andhost.*are also standard OTel names but are set as literals inproviders.pyandresources/local.pyrather than mirrored as constants.) - NeMo custom namespaces:
nv.dl.*/dl.*(distributed learning),rl.*,gym.*,slurm.*,nemo.*,wandb.*. These are NeMo-specific extensions that do not exist upstream.
Why Constants Instead of Strings
Using constants instead of raw strings provides three key benefits:
- Grep-ability: renaming an attribute across the codebase means changing one constant, not every call site.
- Type safety (weak but real):
NV_DL_RANKis an exported name; typos becomeImportErrors."nv.dl.rank"typos become silent data loss. - Central definitions: one package lists the attribute names and Resource type mappings. This keeps them reviewable and documented.
Callers who want the string can use NV_DL_RANK directly; Python strings-as-constants have no boxing cost.
Version Tracking
This constant documents which upstream OTel semconv version the standard namespaces (gen_ai.*, k8s.*) are aligned with. Upstream bumps namespace conventions periodically; gen_ai.* graduated from experimental to stable around 1.30.
When upgrading NeMo Lens to a new semconv version, complete these tasks:
- Review the upstream changelog for renamed or removed attributes.
- Update
SEMCONV_VERSION. - Update any changed constants.
- Add the version bump to the NeMo Lens changelog so consumers know to update.
Resource Types and Attribute Carriers
semconv/resources.py defines explicit type mappings for Resource fields.
normalize_resource_attributes() applies those mappings after decoding an
environment carrier. Unknown values are unchanged, so numeric-looking identifiers
such as "007" stay strings. Every mapped Resource integer field, including
Slurm fields, uses one converter that accepts native integers or complete signed
ASCII base-10 strings. Invalid DL/GPU values remain visible and produce a
warning. Invalid Slurm values are omitted so locally detected values can be
used; the conversion rule is shared while callers retain their failure policy.
semconv/encoding.py contains generic dictionary composition and explicit
converter lookup. The resources/attributes.py module owns formatting and
parsing for the comma-separated, percent-encoded OTEL_RESOURCE_ATTRIBUTES
carrier. Parsing returns strings. Type reconstruction is a separate step and
always uses a caller-selected mapping; the Resource codec does not infer types.
Stability Markers
From semconv/attributes.py:
“Stable” for custom namespaces means these names will not change in minor releases. Breaking changes bump the major version.
“Experimental” for gen_ai.* matches upstream; OTel considers them stable-in-practice but reserves the right to tweak until the full semconv 1.30 stabilization.
Namespace Conventions
NeMo Lens organizes attributes into standard upstream namespaces and NeMo-specific custom namespaces.
Standard Upstream Namespaces
Follow upstream OTel spec exactly. Do not redefine, and do not rename. If upstream says gen_ai.request.model, NeMo Lens uses gen_ai.request.model.
nv.dl.* / dl.* Distributed Learning
Shared across Megatron-LM, NeMo RL, NeMo Gym. Anything a distributed training job needs:
<project>.* Project-Specific Attributes
Use project-specific namespaces for attributes that apply to one consumer rather than the full NeMo ecosystem:
-
megatron.*: Megatron-LM consumer-specific span attributes (e.g., model architecturemegatron.num_layers/megatron.hidden_size, iteration-levelmegatron.skipped/megatron.update_successful). These are set as inline string attributes in the Megatron-LM fork and are not defined as constants innemo.lens.semconv. The semconv package holds the shared and standard namespaces (dl.*,gen_ai.*,rl.*,gym.*,slurm.*,nemo.*,wandb.*,k8s.*); forrl.*it holds the span-attribute names only (see below). -
rl.*— RL-specific. The span-attribute names (rl.algorithm,rl.reward,rl.generation.backend,rl.num_rollouts) are defined here as constants. The metric series are owned by the NeMo-RL consumer.NeMo-RL declares its
rl.*metric series through the metric registry (register_metric_group) in its own tree, so those names are not defined here. Among them,rl.grad_norm,rl.learning_rateandrl.throughput.tokens_per_secare RL-scoped counterparts to quantities thedl.*namespace registers, describing the policy update specifically, so an RL job can report them without claiming the shareddl.*series that a co-resident trainer owns. The first two are deliberate spelling matches fordl.grad_norm/dl.learning_rate, so the two namespaces differ only by prefix. Throughput is the exception:rl.*uses the dottedrl.throughput.tokens_per_secto match therl.response_length.mean/rl.generation.duration_mssubgroup convention, whereasdl.*uses the flatdl.throughput_tokens_per_sec. Query the two by their own names rather than by swapping the prefix. -
gym.*— Gym server-specific (verify.success_rate,rollout.batch_size)
nemo.* NeMo-Wide Identification
nemo.run.id— unique run identifier, shared across all ranksnemo.user.id— optional team/user labelnemo.span.truncated— optional; set totrueonly on a span that was still open at telemetry shutdown and was force-closed. Its end timestamp is the shutdown time, not the time the work finished, so exclude these spans from duration statistics. Absent on every normally ended span. See Production.
Environment Namespaces
slurm.*— SLURM job attributesk8s.*— Kubernetes pod/node attributes (standard OTel)wandb.*— W&B Weave integration metadata
Adding New Attributes
Checklist before adding a constant:
- Is there an upstream OTel semconv name? Use it.
- Is this shared across consumers? Use
dl.*or another shared namespace. - Is this project-specific? Use
<project>.*. Such namespaces are set inline in the consumer fork (e.g. Megatron-LM), not as constants insemconv/attributes.py. - Is this a metric or a span attribute? Metric instruments go in
instruments/; attribute names go insemconv/attributes.py. - Is the attribute always available? If it is not, document it as “optional” so query authors know to handle missing values.
Do not add attributes speculatively. A constant with no call site is dead code that becomes stale.
Attribute vs. Metric Decision
Putting a continuously-varying value such as loss on a span attribute produces thousands of span-attribute time series in Jaeger that cannot be aggregated. Use a metric.
Attribute Cardinality
OTel metric attributes have a cardinality budget — each distinct combination of attribute values creates a new time series. gen_ai.operation.name="text_completion" is fine (one value). gen_ai.request.model="llama-3-8b" is fine (a handful of values). user.session.id="abc-123" is dangerous (unbounded cardinality — one time series per user session).
Rule of thumb: metric attributes should have tens to hundreds of distinct values, not millions. Use span attributes for high-cardinality values.