Extending Instrumentation#
To add new spans or metrics to Megatron code, use the instrumentation primitives from nemo.lens. The primitives themselves are documented in
lens: instrumentation.
This page covers Megatron conventions.
Adding a custom span#
Simple block — span_cm#
from megatron.training.global_vars import get_telemetry
from nemo.lens.helpers import span_cm
telemetry = get_telemetry()
if telemetry is not None:
with span_cm("megatron.my_custom_op", tracer=telemetry.tracer, param_count=1e9):
... # your code
span_cm always creates a span when telemetry is active — good for cold paths.
Group-gated block — managed_span#
For hot paths where you want minimal cost when the group is disabled:
from megatron.core.telemetry.span_groups import MegatronSpanGroup
from nemo.lens.helpers import managed_span
with managed_span(MegatronSpanGroup.STEP, "megatron.my_custom_step",
iteration=iteration) as span:
result = do_work()
if span is not None:
span.set_attribute("megatron.my_custom.result", result)
managed_span yields None when the group is disabled; the body still runs. Check if span is not None before setting attributes.
Fallback pattern#
Every import of lens in Megatron code must use the try/except fallback idiom so the code runs when lens isn’t installed:
try:
from nemo.lens.helpers import managed_span as _otel_managed_span
from nemo.lens.state import is_span_group_enabled as _otel_sg_enabled
except ImportError:
from megatron.core.telemetry.fallbacks import managed_span as _otel_managed_span
from megatron.core.telemetry.fallbacks import is_span_group_enabled as _otel_sg_enabled
megatron/core/telemetry/fallbacks.py re-exports from nemo.lens.fallbacks when lens is installed, otherwise provides inline no-ops.
Naming conventions#
Kind |
Convention |
Example |
|---|---|---|
Span name |
|
|
Span attribute |
|
|
Resource attribute |
|
|
Metric name |
|
|
Use the constants in nemo.lens.semconv when a name is shared across consumers (DL_RANK, NEMO_RUN_ID, etc.). For Megatron-specific names, hard-coded strings are fine — they’re short and grep-able.
Choosing a span group#
When adding a new span, decide which group it belongs to:
Always want it in production? →
job(very rare outside setup spans)Once per iteration? →
stepInside the forward/backward? →
forward_backwardormicrobatchInside the optimizer? →
optimizerRelated to checkpointing? →
checkpointRelated to evaluation? →
evaluateCross-rank communication? →
communicationInference request path? →
inference
Don’t invent a new group unless no existing group fits — new groups add to MegatronSpanGroup and require preset updates.
Adding a new span group#
If you do need a new group:
Edit
megatron/core/telemetry/span_groups.py:class MegatronSpanGroup(SpanGroup): # ... existing groups ... MY_NEW_GROUP = "my_new_group" ALL_GROUPS = frozenset([*SpanGroup.ALL_GROUPS, ..., MY_NEW_GROUP]) _PRESETS = { "default": frozenset([SpanGroup.JOB, SpanGroup.CHECKPOINT, SpanGroup.EVALUATE, INFERENCE]), # typically don't add new groups to default "per_step": frozenset([...]), # add to per_step if it's per-iteration "all": ALL_GROUPS, # always in all }
Document it in Span Groups with the spans it controls and typical frequency.
Update dashboard queries if the group introduces new metric labels.
Adding a metric#
For domain-specific metrics, add a module under megatron/core/telemetry/ following the pattern in megatron/core/telemetry/training_metrics.py:
# megatron/core/telemetry/my_domain_metrics.py
import weakref
from opentelemetry import metrics
_INSTRUMENTS: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary()
def _get_instruments(meter: metrics.Meter) -> dict:
instruments = _INSTRUMENTS.get(meter)
if instruments is None:
instruments = {
"my_new_metric": meter.create_histogram(
name="megatron.training.my_new_metric_ms",
unit="ms",
description="...",
),
}
_INSTRUMENTS[meter] = instruments
return instruments
def record_training_metrics(meter, *, my_new_value_ms=None, ...):
i = _get_instruments(meter)
if my_new_value_ms is not None:
i["my_new_metric"].record(my_new_value_ms)
Call record_training_metrics(meter=handle.meter, my_new_value_ms=42.0) only on the export rank (check handle.is_exporting).
See lens: metrics for the pattern rationale.
Testing new instrumentation#
Megatron’s telemetry tests live at tests/unit_tests/telemetry/ and use the fixture pattern from lens’s conftest.py (global OTel state reset before/after each test).
When adding a span:
Add a test in
tests/unit_tests/telemetry/that asserts the span is emitted when its group is enabled and absent when disabled.Use
InMemorySpanExporter(from lens’sconftest.py, shared viasys.pathor a test utility) to capture spans.Assert on span name, attributes, and parent relationships.
See lens: testing for fixture patterns.
When not to add instrumentation#
Inside a tight inner loop (per-token, per-parameter). Even
managed_span’s frozenset lookup adds up over trillions of invocations.On code that runs on all ranks with unbounded cardinality. If the span attribute includes something like a tensor shape with high variance, you get cardinality explosion at the backend.
As a replacement for logging. Structured logs belong in logs (and can be correlated via the log bridge). Spans describe bounded operations, not every interesting event.
When in doubt, start with a coarse span at the boundary of the subsystem, not a fine-grained one at every internal call.