Skip to content

Instrument Megatron-LM with OpenTelemetry traces and metrics - #6438

Merged
deepakn94 merged 5 commits into
NVIDIA:mainfrom
deepakn94:dnarayanan/telemetry_otel_instrumentation
Aug 18, 2026
Merged

deepakn94 merged 5 commits into
NVIDIA:mainfrom
deepakn94:dnarayanan/telemetry_otel_instrumentation

Conversation

@deepakn94

@deepakn94 deepakn94 commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Wires the span groups and metric instruments added in #6415 into the code that produces them, and adds the configuration surface to turn it all on.

This replaces #6385, rebased onto current main now that #6415 has merged. Original changes by @rhewett-nv — please close #6385 in favour of this one.

What's instrumented

Subsystem File
Training loop, model init, evaluation megatron/training/training.py
Checkpoint save/load, async checkpointing megatron/training/checkpointing.py, async_utils.py
Pipeline schedules, microbatch forward/backward megatron/core/pipeline_parallel/schedules.py
P2P send/recv megatron/core/pipeline_parallel/p2p_communication.py
Gradient sync megatron/core/distributed/distributed_data_parallel.py
Activation offload/reload megatron/core/pipeline_parallel/fine_grained_activation_offload.py
Transformer and Mamba layers megatron/core/transformer/transformer_layer.py, megatron/core/ssm/mamba_layer.py
Dataset builder megatron/core/datasets/blended_megatron_dataset_builder.py

Adds --otel-enabled, --otel-service-name, --otel-span-groups, the MEGATRON_OTEL_* environment variables, and telemetry setup/teardown in megatron/training/global_vars.py. Publishes the Observability guide by adding its docs/index.md toctree entry.

Cost when disabled

Spans are gated per group: a disabled group costs a frozenset lookup and no span object is allocated. Non-exporting ranks resolve to an empty group set. Every nemo.lens import falls back to the no-ops in megatron.core.telemetry.fallbacks, so nothing here requires nemo-lens to be installed.

Changes since the first review

Thanks @ahmadki and @santhnm2 — all four review threads are addressed.

  • Inference-server instrumentation reverted. megatron/core/inference/text_generation_server/text_generation_server.py is byte-identical to main again; this PR no longer touches that module. Per @santhnm2 it is effectively deprecated, so the GenAI-semconv spans and metrics will be ported to dynamic_text_gen_server/text_generation_server.py as a follow-up rather than reworked here.
  • The nemo.lens test suite is gone. Removed all seven files (conftest.py, test_config, test_handle, test_helpers, test_instrumentation, test_instruments, test_propagation). Per @ahmadki they test the Lens library rather than Megatron and belong upstream. They were inherited verbatim from Telemetry/otel lens upstream #6385. The four telemetry test files already on main from Add megatron.core.telemetry base layer and observability guide #6415 are untouched and still pass.
  • RandomIdGenerator monkeypatch removed from _set_telemetry. It reached into opentelemetry.sdk process-globally to work around identically-seeded span IDs across data-parallel ranks; nemo-lens handles that in its own provider setup.
  • nemo-lens pin bumped to b85578fc in both the dev group and the otel extra.
  • Rebased onto main across 68 commits. Two conflicts: distributed_data_parallel.py (kept both main's PARAM_READY_CALLBACK_ATTR import and the lens trace_fn fallback) and training.py, where main had removed torch.cuda.empty_cache() from save_checkpoint_and_time in Remove torch cuda empty cache #5678 — adopted that removal rather than resurrecting the call.
  • Ran tools/autoformat.sh, which Telemetry/otel lens upstream #6385 never had; black reformatted 9 files and isort fixed 6.

⚠️ opentelemetry-api pin widened — needs a look

uv.lock had to be regenerated for the new dependency, and doing so surfaced an unsatisfiable conflict that exists on main today:

  • dev pinned opentelemetry-api~=1.33.1, capping OpenTelemetry at 1.33.x.
  • That forces opentelemetry-exporter-otlp-proto-grpc <=1.34.1, which needs protobuf>=3.19,<6.0.
  • nvidia-resiliency-ext==0.6.0 needs grpcio-tools>=1.76.0, which needs protobuf>=6.31.1.
  • Irreconcilable, so megatron-core[dev] and megatron-core[otel] could not resolve together.

nemo-lens is not the constraint; it asks only for opentelemetry-api>=1.20.0. The cap was. The conflict is invisible on main only because nothing currently pulls the otel extra.

Fix: opentelemetry-api~=1.33.1 -> >=1.33.1,<2. This moves every dev environment from OpenTelemetry 1.33.1 to 1.44.0, not just telemetry runs, which is the main thing worth reviewing here.

Two notes on the lockfile:

  • Generated with uv 0.7.2, the version CI pins in both setup-uv (cicd-main.yml:396) and Dockerfile.ci.dev. Verified in the CI container with uv lock --check (exit 0) and uv sync --locked --only-group linting (exit 0), which is what the linting job runs.
  • The diff shows revision = 3 -> revision = 2. Main's lock was generated by a newer uv than CI pins, so 0.7.2 rewrites it in its own format. Pre-existing inconsistency, not introduced here.

Issue tracking

Linked issue: replaces #6385

Contribution process

Pre-checks

  • I have added relevant unit tests
  • I have added relevant functional tests
  • I have added proper typing to my code
  • I have added relevant documentation
  • I have run the autoformatter.sh on my PR

Verified locally

  • Sphinx builds clean with --fail-on-warning (the flag the docs job uses), including the newly published Observability toctree.
  • black, isort, ruff, and pylint (10.00/10) pass on every changed file in megatron/core.
  • All changed Python files compile.
  • The 83 telemetry unit tests from Add megatron.core.telemetry base layer and observability guide #6415 still pass against this branch.

Known follow-ups

Extracting the span-lifecycle helpers out of training.py — immediate follow-up, already planned.

This PR adds 1,281 lines to megatron/training/training.py (now 5,603 lines), and a large share of that is telemetry bookkeeping rather than training logic:

  • 11 span-lifecycle functions in a contiguous block (_start_otel_job_spans, _end_otel_startup_span, _start_otel_train_span, _reroot_otel_interval, _maybe_reroot_otel_interval, _end_otel_interval_span, _end_otel_train_span, _end_otel_job_spans, _otel_telemetry_active, _otel_mark_goodput, _backdated_otel_span)
  • 9 module-level _otel_* globals holding span handles, context tokens and idempotence flags
  • the SIGTERM handler, force-flush, graceful-drain and exit-hook installer

Roughly 400-500 lines will move to a dedicated module, leaving training.py with imports plus the call-site usage that should have been the only footprint all along: 21 with _otel_managed_span(...), 6 _otel_sg_enabled(...), and a handful of attribute stamps.

Deliberately not done in this PR: it would rewrite the exact training.py diff reviewers have already read, and the span lifecycle has no unit-test coverage here (the four telemetry tests on main cover megatron/core/telemetry, not this), so a move touching global mutation ordering and atexit/SIGTERM idempotence would be unguarded. As a standalone follow-up the diff is almost entirely a move, reviewable with git diff -M, and can land with tests for the lifecycle.

Also queued: port the GenAI-semconv inference instrumentation to dynamic_text_gen_server/, and restore the inference sections of the Observability guide alongside it (they were removed here in f007fd2, since this PR no longer ships that instrumentation).

🤖 Generated with Claude Code

@copy-pr-bot

copy-pr-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@deepakn94
deepakn94 marked this pull request as ready for review August 11, 2026 17:07
@deepakn94
deepakn94 requested a review from a team as a code owner August 11, 2026 17:07
@deepakn94
deepakn94 requested a review from a team August 11, 2026 17:07
@deepakn94
deepakn94 requested review from a team as code owners August 11, 2026 17:07

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.

This file is pretty much deprecated, changes should go in megatron/core/inference/text_generation_server/dynamic_text_gen_server/text_generation_server.py

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.

Good catch — reverted text_generation_server.py to main in f521a8e, so this PR no longer touches the deprecated module at all.

Porting the GenAI-semconv spans and metrics to dynamic_text_gen_server/text_generation_server.py will come as a follow-up rather than being reworked here, to keep this PR to the training-loop instrumentation.

Comment thread pyproject.toml Outdated
Comment thread pyproject.toml Outdated
Comment thread megatron/training/global_vars.py Outdated

@ahmadki ahmadki left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The entire test suit should be dropped, these tests belong to Lens - some of them are even direct copies

@asolergi-nv

Copy link
Copy Markdown
Contributor

/claude review

default=None,
help='Comma-separated span-group spec controlling which OTel '
'instrumentation boundaries are active. Accepts preset keywords '
'("default", "per_step", "full", "all") or individual group names '

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.

"full" isn't a valid preset — MegatronSpanGroup._PRESETS defines default, per_step, profiling, all.

Suggested change
'("default", "per_step", "full", "all") or individual group names '
'("default", "per_step", "profiling", "all") or individual group names '

Comment thread pretrain_hybrid.py
print_rank_0(f'> Transformer Engine version ... {get_te_version()}')

# Optional: the sbatch launch script's own timestamps, if it passed them
# through env vars (see megatron.startup.launch_script_setup/.container_load

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.

Span is named megatron.startup.launch_script (training.py:497), not launch_script_setup — the comment 30 lines below (line 492) already uses the correct name.

Suggested change
# through env vars (see megatron.startup.launch_script_setup/.container_load
# through env vars (see megatron.startup.launch_script/.container_load

@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Light review — mostly docs/tests. Two single-line typos left as inline suggestions; the rest below.

1. The observability docs published by this PR describe the pre-refactor span taxonomy

docs/index.md adds the Observability toctree, so these pages go from orphaned to user-facing — but they still document span names the code does not emit:

documented actually emitted
megatron.pretrain megatron.startup + megatron.train
megatron.train_step megatron.train.iteration
megatron.forward_backward megatron.train.iteration.forward_backward
megatron.optimizer_step megatron.train.iteration.optimizer
megatron.save_checkpoint* / megatron.load_checkpoint* megatron.checkpoint.save* / megatron.checkpoint.load*
megatron.model_init megatron.startup.model_init

Affected: docs/user-guide/observability/index.md:50-55 and span-groups.md:32-44, 68-92, 103-114. Two more gaps in the same set:

  • span-groups.md:22-25 preset table omits the first_iteration / trace_region groups and the profiling preset that span_groups.py defines (and lists full, matching the arguments.py typo).
  • pipeline-parallel.md documents broadcast_trace_context and create_linked_span, and span-groups.md documents megatron.pp.recv_forward.linked — grep finds none of these anywhere under megatron/. Since the trace is now segmented via Links in training.py instead, that page should be rewritten or left out of the toctree.

2. megatron.training.loss exports one arbitrary key

megatron/training/training.py:3572-3577:

_loss_keys = [k for k in total_loss_dict if k not in _meta_keys]
if _loss_keys:
    _otel_loss_snapshot = total_loss_dict[_loss_keys[0]].item() / float(...)

_loss_keys[0] depends on total_loss_dict insertion order, so for models returning more than one loss key (e.g. MoE aux loss) the exported metric silently tracks whichever key happened to land first, and can change between configs. The log path a few lines down averages every non-meta key. Suggest pinning it to a canonical key and skipping the metric when absent:

_loss_key = "lm loss" if "lm loss" in total_loss_dict else None
if _loss_key is not None:
    _otel_loss_snapshot = total_loss_dict[_loss_key].item() / float(
        max(1, total_loss_dict.get(advanced_iters_key, 1))
    )

3. _otel_shutdown_done survives destroy_global_state()

_otel_shutdown_done (training.py:364) is module-level and never reset, but destroy_global_state() -> destroy_global_vars() does clear _GLOBAL_TELEMETRY_HANDLE. A process that calls pretrain() twice (unit tests, notebooks, multi-stage drivers) gets a fresh telemetry handle on the second run with _otel_shutdown_done = True, so _end_otel_job_spans() returns early and the second run never flushes its spans. Suggest clearing it alongside:

def destroy_global_state():
    global _otel_shutdown_done
    _otel_shutdown_done = False
    destroy_global_vars()
    ...

(_otel_exit_hooks_installed should stay True — the atexit/SIGTERM handlers are not uninstalled.)

4. No tests added

The PR body checks "I have added relevant unit tests," but tests/unit_tests/telemetry/ still contains only the four files from #6415 and the diff adds no tests elsewhere. The pure-Python helpers here are unit-testable without a GPU or a live collector:

  • build_telemetry_resource_attrs(args) (global_vars.py) — SLURM/NVRx env var mapping, uuid5 determinism for job_uuid/run_uuid, _detect_gpu_identity returning {} when pynvml is unavailable.
  • build_otel_worker_bootstrap(args) (async_utils.py) — the returned dict has to stay picklable across the spawn boundary, and inert when --otel-enabled is off.
  • _maybe_reroot_otel_interval / _backdated_otel_span (training.py) — the re-root cadence vs. save_interval (including the save_interval <= 0 never-re-root path) is pure arithmetic and easy to get wrong on resume.

The no-op fallbacks in megatron/core/telemetry/fallbacks.py mean these run fine without nemo-lens installed.

Clean

No new direct parallel_state.get_*_group() / get_*_group() reads in megatron/core — the new instrumentation in distributed_data_parallel.py, p2p_communication.py, schedules.py, transformer_layer.py, and mamba_layer.py is decorator-only. The async-checkpoint call_idx tagging matches the upstream return types (schedule_async_request -> int, maybe_finalize_async_calls -> List[int]), and the record_training_metrics kwargs match its signature.

@asolergi-nv asolergi-nv left a comment

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.

There are no critical issues to address, but I wonder if we should start regulating which comments belong in Python files and which ones should be in the documentation.

log_factory = ArgumentGroupFactory(LoggerConfig, exclude = ["log_throughput_to_tensorboard", "throughput_window_size", "memory_keys", "log_l2_norm_grad_to_tensorboard", "log_runtime_to_tensorboard", "runtime_time_unit", "filter_warnings", "modules_to_filter", "set_level_for_all_loggers", "save_config_filepath"])
group = log_factory.build_group(parser, title="logging")

otel_group = parser.add_argument_group(title='opentelemetry')

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.

@maanug-nv This won't be directly exposed in MBridge, right? Should we include OTel in LoggerConfig?

Comment on lines +312 to +359
# OTel: module-level helpers imported once at startup.
try:
from nemo.lens.state import is_span_group_enabled as _otel_sg_enabled
from nemo.lens.helpers import managed_span as _otel_managed_span
from nemo.lens.helpers import safe_set_span_attributes as _otel_safe_set_attrs
from nemo.lens.helpers import trace_fn as _otel_trace_fn
except ImportError:
from megatron.core.telemetry.fallbacks import is_span_group_enabled as _otel_sg_enabled
from megatron.core.telemetry.fallbacks import managed_span as _otel_managed_span
from megatron.core.telemetry.fallbacks import safe_set_span_attributes as _otel_safe_set_attrs
from megatron.core.telemetry.fallbacks import trace_fn as _otel_trace_fn


# OTel: module-level state for the job-level spans -- module level rather than
# closures local to pretrain(), because train()'s own early-exit path (a few
# thousand lines away, a different function) needs to reach the same
# end-spans-then-shutdown sequence. Closures defined inside pretrain() aren't
# callable from there; these are.
#
# Structure (audit section L: NO run-long umbrellas). An incarnation emits a set of
# SEGMENTED traces rather than one tree, because a run-long root span is dead weight
# (its children live in their own traces anyway) and is LOST outright when a fault kill
# arrives before it can be ended and exported. There is deliberately no in-process
# 'workload' / 'megatron.pretrain' span: the authoritative run envelope is the
# reckoner's out-of-band slurm.job (from sacct), and every span here carries run
# identity (run_uuid / slurm.sluid / nvrx.cycle) on its RESOURCE attributes, which is
# what actually stitches the traces back together.
#
# trace A: pre_startup (top-level, fully backdated, CYCLE 0 / non-NVRx only:
# SLURM_JOB_START_TIME -> launch_script_start, i.e. the
# queue tail / prolog before our first line. Ends exactly
# where the agent's nvrx.cold_start begins, so the two
# tile instead of overlapping -- audit section K.)
# trace B: megatron.startup (own trace, fresh trace_id via an empty Context())
# |- megatron.startup.launch_script (backdated; plain-srun path only)
# |- megatron.startup.container_load (backdated; plain-srun path only)
# |- megatron.startup.imports / .arg_parse / .inprocess_setup /
# | .in_job_setup / .initialize_megatron / .jit_fusion_options (backdated)
# `- whatever runs live before train()'s loop (weight-hash check, sniff test,
# cuda-graph setup) -- they attach to the current context
# trace C_n: megatron.train (one per checkpoint interval, each its own trace --
# see _reroot_otel_interval; C_0 Links back to trace B, C_n Links to C_n-1)
#
# The launch_script/container_load children are only emitted when this process actually
# owns that window (the launch script passed presrun through AND we're not under NVRx,
# where nvrx.cold_start covers it -- see pretrain_hybrid.py). When they are emitted the
# startup trace is anchored at launch_script_start so they tile inside it; otherwise it
# is anchored at program_start.

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.

Do we need ALL these comments & details here?

@rhewett-nv

Copy link
Copy Markdown
Contributor

There are no critical issues to address, but I wonder if we should start regulating which comments belong in Python files and which ones should be in the documentation.

Comments used to be for people but now they are for agents. Having the documentation closer to the code seems to work better for them...

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the Approved All necessary approvals have been made label Aug 17, 2026
Wires the span groups and metric instruments added in NVIDIA#6415 into the code that
produces them: the training loop, checkpointing, evaluation, pipeline schedules
and P2P transfers, gradient sync, activation offload, transformer and Mamba
layers, the dataset builder, and the inference server. Adds the --otel-enabled,
--otel-service-name, and --otel-span-groups flags, the MEGATRON_OTEL_*
environment variables, telemetry setup and teardown in global_vars, and the
nemo-lens dependency behind an otel extra.

Spans are gated per group, so a disabled group costs a frozenset lookup and no
span object. Non-exporting ranks resolve to an empty group set. Every lens
import falls back to the no-ops in megatron.core.telemetry.fallbacks, so nothing
here requires nemo-lens to be installed.

Publishes the Observability guide by adding its docs/index.md toctree entry.

This replaces NVIDIA#6385, rebased onto main after NVIDIA#6415 merged. Original changes by
@rhewett-nv.

Signed-off-by: Deepak Narayanan <dnarayanan@nvidia.com>
Bumps the nemo-lens pin to b85578fc in both the dev group and the otel extra,
applying the suggestions from review.

Drops tests/unit_tests/telemetry/{conftest,test_config,test_handle,test_helpers,
test_instrumentation,test_instruments,test_propagation}.py. These import
exclusively from nemo.lens and test the lens library rather than Megatron; some
are direct copies of lens's own tests, so they belong upstream. The four
telemetry test files already on main are untouched and still pass.

Removes the RandomIdGenerator monkeypatch from _set_telemetry. It reached into
opentelemetry.sdk to work around identically-seeded span IDs across
data-parallel ranks, which nemo-lens now handles in its own provider setup.

Reverts megatron/core/inference/text_generation_server/text_generation_server.py
to its state on main. That module is effectively deprecated, so the GenAI-semconv
spans and metrics belong in dynamic_text_gen_server/text_generation_server.py
and will be added separately.

Signed-off-by: Deepak Narayanan <dnarayanan@nvidia.com>
Regenerates uv.lock so it matches pyproject.toml again; without this every job
that runs uv sync re-resolves from scratch and fails building mamba-ssm.

Widening opentelemetry-api from ~=1.33.1 to >=1.33.1,<2 is what makes the
resolution satisfiable at all. The old cap held OpenTelemetry at 1.33.x, which
forced opentelemetry-exporter-otlp-proto-grpc <=1.34.1 and therefore
protobuf<6.0, while nvidia-resiliency-ext 0.6.0 needs grpcio-tools>=1.76.0 and
therefore protobuf>=6.31.1. The two are irreconcilable, so megatron-core[dev]
and megatron-core[otel] could not be resolved together. nemo-lens itself asks
only for opentelemetry-api>=1.20.0, so the cap, not nemo-lens, was the
constraint.

This moves the dev environment from OpenTelemetry 1.33.1 to 1.44.0. Generated
with uv 0.7.2, the version CI pins in both setup-uv and Dockerfile.ci.dev, and
verified there with uv lock --check and uv sync --locked --only-group linting.

Signed-off-by: Deepak Narayanan <dnarayanan@nvidia.com>
The inference-server instrumentation was reverted from this PR, so the guide no
longer describes anything that ships. Removes the GenAI-semconv span section and
attribute table, the inference metrics section and its two Prometheus rows, the
text_completion node in the span hierarchy, and the inference row in the
what-gets-instrumented table.

The inference span group itself stays: MegatronSpanGroup.INFERENCE exists and is
part of the default preset, so it is now listed as reserved, matching how
data_loading is already described. These sections come back with the port to
dynamic_text_gen_server.

Signed-off-by: Deepak Narayanan <dnarayanan@nvidia.com>
Moves the commit pin out of the dependency strings and into the uv.sources
section, matching how every other git-sourced dependency in this file is
declared. The revision now lives in one place instead of two that can drift.

uv.lock is unchanged: uv records the same git source entry either way, so this
is purely a declaration-style change with no effect on resolution. Verified with
uv 0.7.2 in the CI container, where uv lock --check and
uv sync --locked --only-group linting both pass.

Signed-off-by: Deepak Narayanan <dnarayanan@nvidia.com>
@deepakn94
deepakn94 added this pull request to the merge queue Aug 17, 2026
Merged via the queue into NVIDIA:main with commit dd8c825 Aug 18, 2026
89 of 90 checks passed

This branch was previously deployed

2 inactive deployments
public — abb74214 Deployed Aug 17, 2026 by copy-pr-bot[bot] via finalize / publish-docs #6356
test — abb74214 Deployed Aug 17, 2026 by copy-pr-bot[bot] via cicd-wait-in-queue #19237
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Approved All necessary approvals have been made complexity: high nemotron

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants