Skip to content

[2/2] Track every Megatron-Bridge script with MLflow - #2514

Open
kevalmorabia97 wants to merge 2 commits into
kmorabia/mlflow-tool-corefrom
kmorabia/mbridge-qad-export-mlflow
Open

kevalmorabia97 wants to merge 2 commits into
kmorabia/mlflow-tool-corefrom
kmorabia/mbridge-qad-export-mlflow

Conversation

@kevalmorabia97

@kevalmorabia97 kevalmorabia97 commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: new feature

[2/2] of a split. Based on #2544 — merge that first; this PR's diff is only the Megatron-Bridge half.

#2477 added MLflow tracking to examples/megatron_bridge/quantize.py. It was one of five scripts in that directory that write a checkpoint; the other four recorded nothing, so the provenance chain stopped at the PTQ checkpoint and a deployed model could not be traced back to the run that produced it.

All five now take the same --mlflow / --mlflow_experiment / --mlflow_run_name flags, and each declares what it records as a Tool beside its own flags — the shared mlflow_utils.py knows none of them:

Script Records
prune_minitron.py command, arguments, log, prune_score metric, pointer
quantize.py (#2477, moved onto the shared Tool in #2544) + resolved recipe, quantizer summary
distill.py + Megatron-Bridge's per-iteration metrics and resolved config
export_quantized_megatron_to_hf.py command, arguments, log, pointer
export_distilled_megatron_to_hf.py same, one pointer per exported checkpoint

Each writes .experiment.json into the checkpoint it produced, and each tags what it consumed, so prune → quantize → distill → export is walkable both from disk and by tag query on the server.

distill.py opens the run and Megatron-Bridge joins it. Its LoggerConfig records per-iteration metrics and the full resolved config — which a wrapper around main() cannot see — but nothing of distill.py's own arguments and no invocation. Megatron-Bridge takes mlflow.active_run() when one exists, applies the tags and logs into it, so distill_run() opens the run on the rank Megatron-Bridge looks at (the last one) and the two share it. Its early exit is handled explicitly: train() leaves through sys.exit(0) on --exit_interval, which a blanket handler would record as FAILED.

The library pieces that exist for that shared run land here with their first caller, rather than in [1/2] where they would have none: split_tracking_credentials, so a URI handed to something which records it carries no credential; log_active_run_experiment_json, for pointing a checkpoint at a run this process did not open; and MlflowRunLogger._reattach, because a co-owner can end the run first — Megatron-Bridge does, as KILLED, when SIGTERM arrives mid-training.

Two of Megatron-Bridge's defaults are deliberately not inherited: checkpoint artifact upload stays off unless --mlflow_log_checkpoints (it pushes the whole checkpoint over HTTP after every save), and an untracked run passes no mlflow_* fields at all, since they landed in Megatron-Bridge 0.6 and sending them unconditionally would break an untracked run on an older one.

Usage

# Any of the five, same flags:
torchrun --nproc_per_node 8 prune_minitron.py  ... --mlflow https://<server>/
torchrun --nproc_per_node 8 quantize.py        ... --mlflow https://<server>/
torchrun --nproc_per_node 8 distill.py         ... --mlflow https://<server>/
torchrun --nproc_per_node 8 export_quantized_megatron_to_hf.py ... --mlflow https://<server>/

# Each checkpoint names the run that wrote it:
cat /output/qad/checkpoints/.experiment.json

Experiments default to $USER/megatron_bridge_{prune,quantize,distill,export,distill_export}/<model basename>-<variant>.

Testing

  • Real runs on a toy Qwen3 in one MLflow experiment covering all five Megatron-Bridge scripts and hf_ptq — prune, quantize, QAD distillation, quantized export, BF16 distillation, distilled export, HF PTQ — each closing FINISHED with the invocation, its arguments as params, its log, and a matching .experiment.json on disk. The chain tags line up: each stage's source_checkpoint_path is the previous stage's checkpoint_path.
  • tests/examples/megatron_bridge in nvcr.io/nvidia/nemo:26.08, the only lane that runs it: 76 passed. Plus the three suites from [1/2] One MLflow tracking core behind a Tool record #2544: 195 pass.
  • pre-commit run --files <changed>: all hooks pass.
  • Each fix from the review rounds has a test that fails with the fix reverted: the resumed run, the foreign active run, the percent-decoded credential, the credential that cannot be moved, the rank-dependent LoggerConfig, the exit-callback guard, and the iter_* join.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅
  • Did you get Claude approval on this PR?: several rounds; re-requested on this head.

Additional Information

Split from a single ~1150-line PR at review's request; #2544 carries the library consolidation this builds on, and this branch is based on it. Earlier review threads here show as outdated after the rebases — they are all resolved and their fixes are in this branch.

One known gap, stated in the README rather than implied: distill.py --hf_export_path writes a second HuggingFace checkpoint from rank 0, which is not the rank that owns the run, so it carries no pointer yet. For the same reason the uploaded logs/distill.log holds the last rank's output — print_rank_0 keeps the script's own lines on rank 0 — which the README now says outright; carrying rank 0's log into a run owned by another rank needs cross-rank upload and is a follow-up.

Two defects found on shared-run paths during review, both verified against the installed Megatron-Bridge 0.6 rather than its docs. Megatron-Bridge ends the run it shares with distill.py as KILLED from its SIGTERM handler (train.py:1413) and then leaves through sys.exit() (train.py:805), i.e. before distill_run's finally — and MLflow's fluent calls resolve their target by opening a run when none is active, so a preempted distillation's log and metrics went to a second, empty run and its KILLED status was overwritten. Separately, an unreachable server disabled our logger but logger_kwargs still handed Megatron-Bridge the same URI, and state.py calls set_experiment unguarded from inside the training loop — so a best-effort $MLFLOW_TRACKING_URI aborted the training instead of degrading to untracked.

🤖 Generated with Claude Code

@kevalmorabia97
kevalmorabia97 requested review from a team as code owners September 22, 2026 22:26
@kevalmorabia97
kevalmorabia97 requested review from ChenhanYu and removed request for a team September 22, 2026 22:26
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds shared MLflow configuration and checkpoint-provenance utilities. Megatron-Bridge pruning, quantization, distillation, and export workflows use these utilities. Hugging Face PTQ and vLLM example utilities also adopt shared tracking helpers. Tests and documentation cover the updated workflows.

Changes

MLflow tracking workflows

Layer / File(s) Summary
Shared MLflow configuration and run lifecycle
modelopt/torch/utils/mlflow.py, tests/unit/torch/utils/test_mlflow.py, tests/_test_utils/mlflow.py, tests/conftest.py
Adds tool-based tracking configuration, CLI and credential handling, run lifecycle management, tags, and checkpoint provenance. Shared fixtures and unit tests cover run behavior, foreign-run pointers, and credential handling.
Megatron-Bridge run and checkpoint tracking
examples/megatron_bridge/mlflow_utils.py, examples/megatron_bridge/{prune_minitron.py,quantize.py,distill.py,export_*_megatron_to_hf.py}, tests/examples/megatron_bridge/test_mlflow_utils.py, examples/megatron_bridge/README.md, CHANGELOG.rst
Adds tracking configurations and run handling for pruning, quantization, distillation, and exports. Distillation passes run settings to Megatron-Bridge logging and records provenance when the checkpoint marker changes. Tests and documentation cover these workflows and optional checkpoint uploads.
Shared tracking in example utilities
examples/hf_ptq/{example_utils.py,hf_ptq.py}, examples/vllm_serve/vllm_mlflow_utils.py, tests/examples/hf_ptq/test_hf_ptq_args.py, tests/examples/vllm_serve/test_vllm_mlflow_utils.py
Hugging Face PTQ delegates tracking configuration, run tracking, and metadata to shared helpers. vLLM passes a Tool configuration to shared argument handling. Tests use the shared APIs and fixtures.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Distill as distill.py
  participant RunContext as distill_run
  participant BridgeLogger as Megatron-Bridge logger
  participant MLflow
  participant Checkpoint
  Distill->>RunContext: Enter the distillation run context
  RunContext->>MLflow: Open the run on the last process
  Distill->>BridgeLogger: Supply logger_kwargs
  BridgeLogger->>MLflow: Log training metrics and resolved config
  Distill->>RunContext: Record provenance after the checkpoint marker changes
  RunContext->>Checkpoint: Write the .experiment.json pointer
Loading

Suggested reviewers: cjluo-nv

Merge Risk: 🟡 Moderate · up to 8a02c

If another component leaves a different MLflow run active, a tracked script can write its final metrics and artifacts to that run and then close it, which corrupts experiment records. Separately, a tracking URI that contains only a token (or only a username) can end up stored in Megatron-Bridge run parameters and checkpoint configuration. Fix the run-ID check before merging; the credential exposure is narrow but simple to fix.

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 204 functions across 16 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns ✅ Passed PASS. The authoritative PR diff adds no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, external-input eval()/exec(), or # nosec …
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding MLflow tracking to the Megatron-Bridge scripts.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.46%. Comparing base (5f9e8d8) to head (442547f).

Files with missing lines Patch % Lines
modelopt/torch/utils/mlflow.py 95.65% 3 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                      @@
##           kmorabia/mlflow-tool-core    #2514      +/-   ##
=============================================================
- Coverage                      78.44%   76.46%   -1.99%     
=============================================================
  Files                            607      605       -2     
  Lines                          68843    67176    -1667     
=============================================================
- Hits                           54006    51368    -2638     
- Misses                         14837    15808     +971     
Flag Coverage Δ
examples-diffusers 21.33% <0.00%> (-0.03%) ⬇️
examples-gpt-oss 13.44% <0.00%> (-0.02%) ⬇️
examples-hf_ptq 23.03% <26.08%> (-0.01%) ⬇️
examples-llm_distill 13.50% <0.00%> (-0.02%) ⬇️
examples-llm_eval 17.51% <7.24%> (-0.02%) ⬇️
examples-llm_qat 17.68% <0.00%> (-0.02%) ⬇️
examples-llm_sparsity 15.93% <0.00%> (-0.02%) ⬇️
examples-megatron_bridge 26.65% <55.07%> (+0.03%) ⬆️
examples-specdec_bench 13.20% <0.00%> (-0.02%) ⬇️
examples-speculative_decoding 17.88% <7.24%> (-0.02%) ⬇️
examples-torch_onnx 21.86% <0.00%> (-0.03%) ⬇️
examples-torch_trt 15.27% <0.00%> (-0.02%) ⬇️
examples-vllm_serve 13.69% <26.08%> (+<0.01%) ⬆️
gpu 50.24% <7.24%> (-8.55%) ⬇️
regression 15.15% <0.00%> (-0.02%) ⬇️
unit 58.50% <95.65%> (-0.32%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kevalmorabia97

Copy link
Copy Markdown
Collaborator Author

/claude review

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (claude-opus-5) — DM the bot to share feedback.

Nicely scoped follow-up (~300 core-logic lines, well tested), but log_active_run_experiment_json uses the fluent mlflow.log_text, which starts a brand-new run on the already-closed path this PR explicitly supports.

Needs action:

  • Log the artifact through MlflowClient().log_text(run.info.run_id, ...) in modelopt/torch/utils/mlflow.py — mlflow.log_text calls _get_or_start_run(), so the last_active_run() branch silently opens a second run. See inline comment.
  • Extend the ForeignMlflow fake in tests/unit/torch/utils/test_mlflow.py to fail if a run is started, so the closed-run path is actually covered.
  • Tighten record_checkpoint_provenance in examples/megatron_bridge/mlflow_utils.py: checkpoint_dir.is_dir() is true on any resumed run, so a run that dies before its first save stamps a previous run's checkpoint. See inline comment.
  • Replace the if tool is DISTILL identity check in add_mlflow_args with a Tool field (e.g. log_checkpoints_flag: bool = False), so the per-script differences all live in one place.

No action needed:

  • Existing test updates are all signature/refactor follow-through (_run_tags/_run_outputs folded into Tool), coverage intact.

Comment thread modelopt/torch/utils/mlflow.py Outdated
Comment thread examples/megatron_bridge/mlflow_utils.py Outdated
Comment thread examples/megatron_bridge/distill.py Outdated
Comment thread modelopt/torch/utils/mlflow.py Outdated
Comment thread examples/megatron_bridge/mlflow_utils.py Outdated
Comment thread examples/megatron_bridge/mlflow_utils.py
Comment thread examples/megatron_bridge/mlflow_utils.py Outdated
Comment thread examples/megatron_bridge/mlflow_utils.py

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review — 1 CRITICAL, 2 IMPORTANT, 2 SUGGESTION

Full review (scope: /claude review with no extra instructions). 9 files changed (+517/−86); reviewed all of modelopt/, examples/ and both test files, plus the README.md and CHANGELOG.rst diffs.

The refactor itself is clean: collapsing three copies of the wiring into one Tool record per script is the right call, and the decision not to open a competing run in distill.py — letting Megatron-Bridge’s LoggerConfig own it — is well reasoned and well documented, as is keeping mlflow_log_artifacts off by default. The findings are all in the new provenance-pointer path.

Most impactful

1. log_active_run_experiment_json uploads its artifact to a brand-new run when the real run has already closed (modelopt/torch/utils/mlflow.py:779) — CRITICAL. mlflow.log_text is the fluent API, so it resolves its target through _get_or_start_run(); with mlflow.active_run() returning None, that starts a run rather than reusing the one last_active_run() just returned. That is precisely the state the last_active_run() fallback was added for (Megatron-Bridge sys.exit()s from inside train()), so on the documented main path every QAD job leaves a spurious empty run and attaches experiment.json to it, while the on-disk pointer names the real run. Nothing raises, and the stubbed ForeignMlflow.log_text in the new test cannot see it. Fix: go through MlflowClient().log_text(run_id=..., ...).

2. record_checkpoint_provenance never clears a stale pointer (examples/megatron_bridge/mlflow_utils.py:237) — IMPORTANT. quantize.py and the export get this for free from track_run’s untracked branch; this path returns early instead. Since distill.py passes load=checkpoint_dir and a reused --output_dir is the normal Slurm-requeue flow, an untracked re-run — or a tracked one where mlflow is absent or the server unreachable — leaves the earlier run’s .experiment.json claiming the new weights, contradicting the invariant log_experiment_json documents.

3. QAD runs get MLflow’s auto-generated run name, not the documented UTC timestamp (examples/megatron_bridge/mlflow_utils.py:223) — IMPORTANT. The timestamp default lives in MlflowRunLogger.start, which distill.py never reaches, so mlflow_run_name=None is what Megatron-Bridge receives. Both the --mlflow_run_name help text registered on all three parsers and the README line this PR edits promise the UTC start time.

Two SUGGESTIONs are inline: the consumed Megatron checkpoint appears in no tag, so the PTQ→QAD→export chain is joinable only via the on-disk pointer rather than a server-side tag query; and record_checkpoint_provenance’s “no checkpoint to point at” claim does not hold for a resumed --output_dir.

Verified as correct

  • DISTILL.checkpoint matches distill.py’s checkpoint_dir = os.path.join(args.output_dir, "checkpoints").
  • dist.is_last_process() exists and is global-rank based, matching where Megatron-Bridge opens the run; the process group is still alive where the finally runs.
  • The finally placement is right — sys.exit() raises SystemExit, which propagates through it.
  • log_active_run_experiment_json’s JSON keys match MlflowRunLogger.run_info exactly, so consumers see one format.
  • resolved_recipe_texts(getattr(args, "recipe", None)) correctly tolerates the export’s missing --recipe; _NON_PARAM_ARGS and the print_args(masked_args(args)) / checkpoint_exported ordering are consistent across both single-pass scripts.
  • logger_kwargs returning {} when untracked does keep an older Megatron-Bridge working, as the PR body claims.

Minor, not raised inline

distill.py --hf_export_path writes a second, deployable HF checkpoint (export_llm_to_hf / save_vlm_to_hf) that gets neither a pointer nor stale-pointer cleanup. Arguably out of scope given export_quantized_megatron_to_hf.py covers the primary export path — noting it in case it was an oversight.

Risk

Moderate. The behavioural blast radius is examples-only, and the single library change is additive (log_active_run_experiment_json is new; nothing else calls it) with no modelopt_state, mode-registration or config-schema surface touched, so there is no checkpoint or public-API compatibility risk. Finding 1 is worth fixing before merge because it writes to the tracking server on the default path and fails silently.

🤖 Generated with Claude Code

@kevalmorabia97

Copy link
Copy Markdown
Collaborator Author

On the minor point raised but not filed inline — distill.py --hf_export_path writing a second HF checkpoint with no pointer and no stale-pointer cleanup: confirmed, and deliberately left out of this PR.

It is not a two-line addition. That branch does:

is_rank_0 = dist.rank() == 0
dist.cleanup()          # process group destroyed; export_ckpt makes its own
if is_rank_0:
    export_llm_to_hf(...)

So the rank that writes the checkpoint is rank 0, while Megatron-Bridge owns the MLflow run on the last rank — and after dist.cleanup() every rank reports itself as rank 0 of a world of 1, so is_last_process() no longer distinguishes them. Getting a pointer there means deciding which rank writes it, and ordering it against an export that has already torn down the group. That deserves its own change rather than being bolted onto this one, and export_quantized_megatron_to_hf.py — the path the README and test_qad.py both use to produce a deployable checkpoint — is fully covered here.

Happy to do it as a follow-up if you'd rather it not wait.

For the record, everything else from both review rounds is now in 72c9550, with the five inline threads answered individually. Suites after the fixes: 101 library, 40 megatron_bridge, 47 hf_ptq, 32 vllm_serve; pre-commit clean.

🤖 Generated with Claude Code

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (claude-opus-5) — DM the bot to share feedback.

Re-review: the spurious-run bug is gone and the other five threads are properly closed, but the chosen fix uses a log_text(..., run_id=) kwarg that does not exist at the declared mlflow-skinny>=2.9 floor.

Needs action:

  • 💬 Author replied "verified against the installed client" — the floor in pyproject.toml is mlflow-skinny>=2.9, where fluent log_text(text, artifact_file) takes no run_id. Use MlflowClient().log_text(run_id, text, artifact_file) (works on every version) or raise the floor. See inline on modelopt/torch/utils/mlflow.py:808.
  • Add a regression that a TypeError from log_text is visible, or make the except Exception in log_active_run_experiment_json narrower — today an unsupported signature degrades to a warning with no pointer on disk and a stale pointer left in place.
  • Confirm the new cross-stage join is robust to relative paths: checkpoint_run_tags resolves checkpoint_path but leaves source_checkpoint_path as given, so a relative --student_megatron_path breaks the PTQ→QAD→export tag query test_the_three_stages_chain_on_the_server pins.

No action needed:

  • ✔️ Resolved since the last review: the closed-run double-open, the is_dir() provenance guard (now checkpoint_marker), stale-pointer clearing, the tool is DISTILL check, the run-name default, and the source-checkpoint tag.

Comment thread modelopt/torch/utils/mlflow.py Outdated
@kevalmorabia97

Copy link
Copy Markdown
Collaborator Author

/claude review

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (claude-opus-5) — DM the bot to share feedback.

Approving: all three open items from the last round are fixed in the diff, and nothing new surfaced.

No action needed:

  • ✔️ Resolved since the last review: log_active_run_experiment_json now uses MlflowClient().log_text(info["run_id"], text, ...) (positional, valid at the mlflow-skinny>=2.9 floor); the pointer is written before the upload with test_a_failed_upload_still_leaves_the_pointer_on_disk pinning that a TypeError no longer costs the on-disk record; and checkpoint_run_tags now resolves source_checkpoint_path on the same terms as checkpoint_path, with relative/absolute and Hub-id tests.
  • Existing test edits are signature follow-through from folding _run_tags/_run_outputs into Tool; coverage is intact.
  • Minor, not blocking: if mlflow.get_experiment raises, log_active_run_experiment_json returns without dropping a stale pointer, unlike the run is None path — narrow edge, worth a follow-up at most.

Complex PR: 1 existing test file modified or removed. Looping in a human for approval.

Comment thread examples/megatron_bridge/mlflow_utils.py Outdated
Comment thread modelopt/torch/utils/mlflow.py
Comment thread examples/megatron_bridge/mlflow_utils.py Outdated
Comment thread examples/megatron_bridge/mlflow_utils.py
Comment thread tests/examples/megatron_bridge/test_mlflow_utils.py Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review — 0 CRITICAL, 2 IMPORTANT, 3 SUGGESTION

Full review (scope: /claude review with no extra instructions). 9 files changed (+837/−102); reviewed all of modelopt/ and examples/, both test files, and the README.md/CHANGELOG.rst diffs — nothing skipped.

Both items the previous round flagged are genuinely fixed: MlflowClient().log_text(run_id, text, artifact_file) is positional and works at the mlflow-skinny>=2.9 floor, and source_checkpoint_path is now resolved on the same terms as the upstream checkpoint_path while leaving a Hub org/name alone — test_a_source_path_joins_whatever_the_caller_typed pins exactly the case that was broken. ForeignMlflow.start_run raising is a good way to make the closed-run path assert itself.

The two IMPORTANT findings are both new.

Most impactful

1. logger_kwargs hands Megatron-Bridge the unmasked tracking URI (examples/megatron_bridge/mlflow_utils.py:236) — IMPORTANT. args.mlflow keeps any user:token@ the user passed, and that is the one thing every other path in this module redacts: _redact_argv for command.txt, the _SECRET_NAME/_redact params filter at modelopt/torch/utils/mlflow.py:549, run_info["tracking_uri"], print_args(masked_args(args)) in both wired scripts. By this PR's own README wording LoggerConfig records "the full resolved config as params", and the resolved config is also serialised to checkpoints/iter_*/run_config.yaml (asserted at tests/examples/megatron_bridge/test_distill.py:274) — so on the QAD path a URI-embedded token becomes a searchable MLflow param and ships inside the distilled checkpoint. The other two scripts never hand the URI to anything that serialises it, so this is specific to the new path. Either strip the credentials into MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD before passing the URI, or document in the DISTILL help text and the README that distill.py needs them from the environment.

2. log_active_run_experiment_json leaves a stale pointer when it cannot read the run (modelopt/torch/utils/mlflow.py:806) — IMPORTANT. The run is None branch four lines up calls drop_experiment_json; this one returns after a warning. Both this docstring and log_experiment_json's promise the pointer beside a saved checkpoint is "this run's or absent -- never a previous run's". The reachable case is the ordinary requeue flow: distill.py points load at the same <output_dir>/checkpoints, so a resumed run starts with the previous run's .experiment.json there, and record_checkpoint_provenance has already proven the marker moved. A server that goes away between the last save and this call therefore leaves a pointer misnaming the author of the new weights — a wrong answer rather than no answer. test_recording_the_active_run_never_fails_the_job covers this path but starts from an empty directory, so it cannot see it.

Three SUGGESTIONs are inline: default_run_name() evaluated per-rank so run_config.yaml can name a run name that does not exist; --mlflow_log_checkpoints registered in the underscored spelling only, against the dual-spelling convention the sibling flags document; and two _run_inputs monkeypatch stubs left at the old one-argument signature.

Verified as correct

  • checkpoint_marker reads latest_checkpointed_iteration.txt, which Megatron-Bridge really does write after distillation — tests/examples/megatron_bridge/test_qad.py:110 asserts it on the distilled checkpoint, so the marker-moved gate is not silently always-false.
  • The marker gate itself is sound: read before distill(), compared after, so a resumed run that died before its own first save leaves the inherited checkpoint and its pointer alone. test_no_pointer_when_this_run_saved_nothing walks all three states.
  • dist.is_last_process() is global-rank based (rank() == size() - 1) and the default process group is still alive where the finally runs — confirmed by distill.py's own "Save rank before destroying process group" comment in the --hf_export_path branch, which is the first place the group goes away. So exactly one rank writes the pointer; there is no all-ranks-look-like-rank-0 fanout where a non-owning rank would drop_experiment_json over the owner's write.
  • finally placement is right: Megatron-Bridge's sys.exit() from inside train() raises SystemExit, which propagates through it, and the not args.validate_only guard matches skip_train=args.validate_only.
  • Tool.checkpoint for DISTILL matches distill.py:384's os.path.join(args.output_dir, "checkpoints"); every attribute the three Tool records read (student_hf_path, student_megatron_path, output_dir, hf_model_name_or_path, megatron_path, export_unified_hf_path) is a flag its script registers before resolve_mlflow_args runs.
  • resolve_mlflow_args assigns args.mlflow from $MLFLOW_TRACKING_URI, so logger_kwargs's if not args.mlflow early return does not quietly drop env-configured tracking for distill.py — and it guarantees mlflow_experiment is non-None whenever the dict is non-empty.
  • _experiment_json unifies the two writers on one schema, and run_url no longer double-slashes; MlflowRunLogger.run_url reading through run_info is not circular now that run_info builds the URL itself, and still returns "" before the run opens.
  • os is imported at module top, so checkpoint_run_tags' new os.path.exists guard is fine; from mlflow.tracking import MlflowClient and positional log_text both predate the 2.9 floor.
  • args.checkpoint_exported = False is set after print_args, and _NON_PARAM_ARGS excludes it plus mlflow_log_checkpoints, so neither reaches the params.

Risk

Moderate, and confined. The single library change is additive — log_active_run_experiment_json and default_run_name are new, and the checkpoint_run_tags(..., model=) parameter is keyword-with-default, so the existing hf_ptq caller is unaffected. No modelopt_state, mode registration, config schema or export-format surface is touched, so there is no checkpoint or public-API compatibility exposure. Finding 1 is the one I would fix before merge, because it writes a secret to durable storage on a documented invocation form and nothing surfaces it.

🤖 Generated with Claude Code

@kevalmorabia97

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment thread examples/megatron_bridge/mlflow_utils.py Outdated
Comment thread modelopt/torch/utils/mlflow.py Outdated
Comment thread examples/megatron_bridge/README.md Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review — 1 IMPORTANT, 2 SUGGESTION

Scope: the trigger was a bare /claude review, so this is a full review. All 9 changed files opened (small PR, +946/-104): modelopt/torch/utils/mlflow.py, the four examples/megatron_bridge/ files, both test files, README, CHANGELOG. This is the first Claude pass on the PR — no prior findings to reconcile. Reviewed against the stacked base kmorabia/mbridge-quantize-mlflow (two-dot diff, since the checkout is shallow).

Findings

No. Severity Where What
1 IMPORTANT Compatibility examples/megatron_bridge/mlflow_utils.py:280-287 A tracked QAD run that cannot find Megatron-Bridge’s run writes no pointer, prints nothing, and deletes the pointer it inherited
2 SUGGESTION modelopt/torch/utils/mlflow.py:826-836 A transient get_experiment() failure drops a pointer whose run_id is already in memory
3 SUGGESTION examples/megatron_bridge/README.md:129 "All three scripts that write a checkpoint" — four do; distill.py --hf_export_path and export_distilled_megatron_to_hf.py are uncovered

Most impactful

Finding 1 is the one worth acting on. log_active_run_experiment_json() collapses "this job is untracked" and "tracking was requested but no run is visible on this rank" into the same quiet drop_experiment_json() + return. The second case is reachable — a Megatron-Bridge version that opens the run somewhere other than rank == world_size - 1, an mlflow client absent from the image, MLflow logging disabled inside LoggerConfig — and in it the user passed --mlflow, got no error, got no pointer, and lost the one a previous run left. For a feature whose entire purpose is "the checkpoint names the run that produced it", that failure is invisible in the job log. record_checkpoint_provenance() already has args.mlflow in hand, so telling the two cases apart is a returned bool plus a one-line warning.

The design leans on "Megatron-Bridge opens the run on the last rank". That is consistent with Megatron’s own is_last_rank() and with dist.is_last_process(), and I could not verify it from this repo (megatron.bridge is not installed in this environment), so it is not a separate finding — but it is the assumption that finding 1 would make safe to be wrong about.

What I traced and found correct

  • The three-stage join keys line up. quantize.checkpoint_path (resolved --export_megatron_path) equals distill.source_checkpoint_path (resolved --student_megatron_path); distill.checkpoint_path (resolved <output_dir>/checkpoints) equals export.source_checkpoint_path (resolved --megatron_path, which README:328 points at <output>/checkpoints). The new os.path.exists() guard in checkpoint_run_tags correctly leaves a Hub id like org/name unresolved, and model= keeps the model tag naming the model rather than the checkpoint directory for the two stages whose source is a checkpoint.
  • dist.broadcast(default_run_name()) is safe where it is called. distill.py:680 runs dist.setup() before main(), so the default process group is initialized and torch.cuda.set_device(local_rank()) has run — the .cuda() inside broadcast will not collide ranks on device 0. All ranks parse identical argv, so args.mlflow / args.mlflow_run_name are identical and every rank takes the same branch of the or; no rank-divergent collective.
  • The finally around distill(config) is right, and SystemExit survives it. dist.abort() re-raises SystemExit rather than swallowing it, so the --exit_interval path still exits cleanly after the pointer is written. record_checkpoint_provenance does no collective, so the last rank doing MLflow HTTP while peers unwind cannot deadlock.
  • The saved_before marker guard holds. Fresh save, resume-then-save, and resume-then-crash all behave as documented; checkpoint_marker reads <output_dir>/checkpoints/latest_checkpointed_iteration.txt, which matches checkpoint_dir at distill.py:384 and save= at :613.
  • Tool.outputs = field(default=lambda args: {}) does not bind as a method. The generated __init__ always assigns the instance attribute (via object.__setattr__ under frozen=True), so the instance dict shadows the class-level function; test_the_export_tags_point_at_the_deployable_checkpoint exercises this through _describe.
  • The refactor is behaviour-preserving. _experiment_json adds a .rstrip("/") that run_url previously lacked (strictly better — no doubled slash before #/), run_name resolution is unchanged, and no existing test needed editing (0 deletions in test_mlflow.py).
  • split_tracking_credentials is the right call for the distill path. Megatron-Bridge logs its resolved config as params and serialises it into run_config.yaml inside the checkpoint, so masking would not work and leaving the credential would make it durable in two places; os.environ.setdefault correctly lets a deliberately-exported variable win.
  • Plugin laziness respected: mlflow and mlflow.tracking are both imported inside the function, and mlflow_utils.py still imports no Megatron.

No mode registration, config schema, modelopt_state, or public modelopt/torch/*/__init__.py surface is touched, so nothing here affects checkpoint restore. The only public-API change is additive — three new __all__ entries — plus a new model= parameter on checkpoint_run_tags with a backward-compatible default.

Risk: low. Confined to example scripts and one opt-in tracking utility; the optimization and export paths are untouched, and every failure mode in the new library helper is caught and warned rather than raised. Finding 1 is an observability hole in a provenance feature, not a correctness bug in anything that produces weights.

🤖 Generated with Claude Code

Comment thread modelopt/torch/utils/mlflow.py Outdated
Comment thread modelopt/torch/utils/mlflow.py
Comment thread examples/megatron_bridge/mlflow_utils.py Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review — CRITICAL: 1 · IMPORTANT: 0 · SUGGESTION: 3

Scope: the trigger was a bare /claude review, so this is a full review. 12 files changed (+1400/−53); I opened all 8 source files (modelopt/torch/utils/mlflow.py, the six examples/megatron_bridge/ scripts, the README) plus surrounding context in distributed.py, validate_tracking_uri/resolve_tracking_uri/_closing_run/tracked_run, and the logger_kwargs and credential tests in tests/examples/megatron_bridge/test_mlflow_utils.py. Two-dot diff against origin/kmorabia/mlflow-tool-core, since the checkout is shallow.

Prior rounds reconciled. The findings from the last two passes are genuinely fixed:

  • The stray-run bug is gone: log_text now gates on _reattach(), so log_experiment_json(None) in tracked_run.close() can no longer open a second run ahead of the re-attach.
  • _reattach compares active.info.run_id against self._run.info.run_id rather than assuming any active run is this one — the guard the last round asked for, and it is what makes the KILLED carry-over in finish() land on the right run.
  • split_tracking_credentials percent-decodes both halves, so https://alice:tok%2Fen@server/ authenticates the same from distill.py as from quantize.py.
  • _checkpoint_root normalizes an iter_* directory up to the checkpoints root, so the distill → distill_export hop joins on the same key the distillation tagged.
  • args.checkpoint_exported is no longer dead state in export_distilled_megatron_to_hf.py; the settles_pointer=False tools get exported=lambda: False and never read the attribute.

The blocking one

[CRITICAL] logger_kwargs takes its "hand Megatron-Bridge nothing" decision only on rank 0 (examples/megatron_bridge/mlflow_utils.py:292). if recordable is None and dist.is_master(): print(...); return {} — the rank guard belongs to the print, but it also gates the return, so every non-master rank falls through and receives mlflow_tracking_uri=None with mlflow_experiment set. That is the rank that matters: distill_run opens the run on dist.is_last_process() precisely because that is where Megatron-Bridge looks, and for any world_size > 1 that rank is not master. It therefore enters Megatron-Bridge's MLflow path (which this module documents as keyed on mlflow_experiment) with no URI, calls set_experiment unguarded from inside the training loop against ./mlruns, and repoints the process-global tracking URI out from under MlflowRunLogger — the same class of failure the sibling logger.enabled broadcast in distill_run was added to prevent. It also makes LoggerConfig, and so run_config.yaml, differ by rank. test_an_unmovable_credential_is_not_recorded_at_all passes only because a single-process test has dist.is_master() == True. Reachable with any half-credential URI, e.g. a bearer token pasted as userinfo (--mlflow https://token@server/).

Non-blocking

One smaller note not worth its own thread: split_tracking_credentials uses os.environ.setdefault, so a user who has already exported MLFLOW_TRACKING_USERNAME/PASSWORD and passes a differently-credentialed --mlflow gets our logger authenticating off the URI userinfo (where requests wins) and Megatron-Bridge authenticating off the pre-existing variables — a 401 raised from inside the training loop. The docstring states the precedence but not that the mismatch is a hazard.

What I traced and found correct

  • The _reattach state machine. On the preempted path close() → log_experiment_json(None) → log_text → _reattach captures _closed_as = "KILLED" and re-opens; finish()'s second _reattach then sees its own run, returns True, and the ("FAILED", "KILLED") filter restores KILLED — a non-terminal RUNNING never reaches end_run, as the comment claims.
  • No stray run from the provenance writer. log_active_run_experiment_json goes through MlflowClient().log_text(run_id, ...) and otherwise touches only get_experiment/get_tracking_uri, neither of which resolves through _get_or_start_run().
  • The pointer invariant holds on every path I walked, including the requeue flow: checkpoint_marker reads latest_checkpointed_iteration.txt rather than trusting the directory, so a run that dies before its own first save leaves the resumed checkpoint's pointer alone, and record_checkpoint_provenance is called unconditionally so an untracked re-run into a reused --output_dir clears what it inherited. DISTILL_EXPORT/DISTILL correctly opt out of tracked_run's own pointer with settles_pointer=False, and the untracked-cleanup branch at mlflow.py:1072 is unreachable for them precisely because record_exported_checkpoint/record_checkpoint_provenance own it instead.
  • Collectives are entered by all ranks under identical predicates. Both dist.broadcast calls in distill_run sit under if args.mlflow, which every rank parsed identically; src=dist.size() - 1 matches the rank tracked_run was told is main; size() == 1 short-circuits to the local object, which is None when the logger disabled itself, so a single-process run degrades correctly.
  • Rank capture around dist.cleanup(). export_distilled_megatron_to_hf.py:300 saves is_rank_0 before the teardown and record_exported_checkpoint refuses on anything else, so the post-cleanup "every rank is rank 0" state cannot produce N writers; it is also the same rank mlflow_run enabled the logger on, so the fluent active_run() it reads is really there.
  • split_tracking_credentials' fail-closed scheme check is consistent with validate_tracking_uri (mlflow.py:166), which rejects the same non-http(s) URIs — so the None return cannot be reached by a legitimate file:/sqlite: backend, and rpartition/partition handle an @ or : inside the password correctly.
  • Exit paths. _closing_run maps SystemExit(0) to FINISHED and non-zero to FAILED, so train()'s --exit_interval exit and prune_minitron.py's accuracy gate are both recorded as the PR describes, and record_checkpoint_provenance in the finally still runs on the exit path.
  • Every Tool callable resolves against an argument its script defines, and prune_score / checkpoint_exported / mlflow_log_checkpoints are all in _BOOKKEEPING, so none of them leaks into the params.

Risk

Moderate, and confined to examples/megatron_bridge/ plus additive surface in modelopt/torch/utils/mlflow.py. The library changes are behind enabled/self._run is None guards and everything after the run opens is best-effort, so nothing here can fail a job that would otherwise have worked — with the one exception of the CRITICAL, which hands a training loop a tracking configuration it will trip over. Fix that and the rest is documentation-grade.

🤖 Generated with Claude Code

@kevalmorabia97
kevalmorabia97 force-pushed the kmorabia/mbridge-qad-export-mlflow branch from 4a484f8 to 82c1636 Compare September 24, 2026 14:19
@kevalmorabia97

Copy link
Copy Markdown
Collaborator Author

/claude review

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (claude-opus-5-5) — DM the bot to share feedback.

Nudge: the two open code items from the last round are fixed, but the PR is still 566 lines of core logic, over the 500-line budget, and none of the size exceptions apply.

Needs action:

  • ✂️ Split this PR into stacked [x/N] PRs, merged in order after #2544, each passing CI alone and linking the others. [2/3] Add shared-run MLflow helpers: split_tracking_credentials, log_active_run_experiment_json, and _reattach/_closed_as/set_terminated in modelopt/torch/utils/mlflow.py, plus their test_mlflow.py cases and the FakeMlflow resume support (about 151 lines). [3/3] Track every Megatron-Bridge script with MLflow: examples/megatron_bridge/* plus test_mlflow_utils.py (about 415 lines). If the owner accepts the overage instead, post that waiver on the PR.

No action needed:

  • ✔️ Resolved since the last review: the README now says the uploaded distill.py log comes from the last rank and does not include the print_rank_0 output. mlflow_run now clears args.mlflow when the logger disables itself, and a test pins that record_exported_checkpoint then stays quiet. Also fixed: every rank now declines the half-credential config, the owning rank is captured before dist.cleanup(), a foreign-run finish() closes this run by id, and prune_score is stashed right after the search.
  • The edits to existing tests are justified. The naming test is now parametrised over all five tools, pointer gating covers the prune and export scripts, and run_tags/add_mlflow_args are now reached through mlflow_utils. No assertion was loosened.
  • The design is settled. This PR extends the existing Tool/tracked_run core from #2544 rather than adding a second system.

_name(args.student_megatron_path) if args.student_megatron_path else "bf16"
),
model=lambda args: args.student_hf_path,
checkpoint=lambda args: str(Path(args.output_dir) / "checkpoints"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] DISTILL.checkpoint is unconditional, so a --validate_only run still tags checkpoint_path=<output_dir>/checkpoints even though it writes nothing.

run_tags states the invariant explicitly — "Omitted rather than empty when the run writes no checkpoint: a search for runs that produced one should not match it." With --validate_only against a reused --output_dir, a tag query for "which run produced this checkpoint" now returns the validation run alongside the training run that actually wrote it. The on-disk pointer is correct (distill.py guards record_checkpoint_provenance with if not args.validate_only), so this is server-side only.

Folding the condition into the Tool also removes the need for that guard, and makes checkpoint_marker return None for the same reason:

    # None for --validate_only: the run writes no checkpoint, so nothing to tag or point at.
    checkpoint=lambda args: (
        None if args.validate_only else str(Path(args.output_dir) / "checkpoints")
    ),

Comment on lines +175 to +180
def resolve_mlflow_args(
args: argparse.Namespace, parser: argparse.ArgumentParser, tool: Tool
) -> None:
"""Settle where tracking is configured from, and name the experiment."""
_resolve_mlflow_args(args, parser, tool)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] This wrapper adds nothing — it forwards all three arguments to _resolve_mlflow_args unchanged. add_mlflow_args below genuinely needs to wrap (it appends --mlflow_log_checkpoints), but this one only exists so the scripts can import both names from mlflow_utils. A plain alias says that, and keeps _resolve_mlflow_args's docstring as the single source of truth instead of a second, shorter paraphrase that can drift:

Suggested change
def resolve_mlflow_args(
args: argparse.Namespace, parser: argparse.ArgumentParser, tool: Tool
) -> None:
"""Settle where tracking is configured from, and name the experiment."""
_resolve_mlflow_args(args, parser, tool)
# Re-exported so a script imports the whole tracking CLI from one module; add_mlflow_args
# below wraps because it has something to add.
resolve_mlflow_args = _resolve_mlflow_args

Comment on lines +745 to +747
# The Tool names the Megatron checkpoint, which is what a chain joins on, so the HF
# one is pointed at the run here -- both are written when both flags are given.
record_exported_checkpoint(args, args.output_hf_path, dist.is_master())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] The comment holds only when --output_megatron_path is also given. PRUNE.checkpoint is output_megatron_path or output_hf_path, so with --output_hf_path alone the Tool names the HF checkpoint, tracked_run writes .experiment.json there at close, and this call writes the same JSON to the same directory — a harmless duplicate, but the comment's premise ("the Tool names the Megatron checkpoint") is false in that configuration.

Worth narrowing both the comment and the call, e.g.:

        # The Tool points at the Megatron checkpoint when there is one, since that is what a
        # chain joins on, so the HF one needs its own pointer only then.
        if args.output_megatron_path:
            record_exported_checkpoint(args, args.output_hf_path, dist.is_master())

@claude

claude Bot commented Sep 24, 2026

Copy link
Copy Markdown

Claude review — 0 CRITICAL, 0 IMPORTANT, 3 SUGGESTION

Full review (scope: /claude review, no extra instructions). 12 files changed (+1391/−62); reviewed all of modelopt/ and examples/, the tests/_test_utils shim and the test diff, plus README.md and CHANGELOG.rst.

Prior round's findings are resolved. The CRITICAL I raised last time — log_active_run_experiment_json uploading through the fluent mlflow.log_text, which _get_or_start_run()s a spurious empty run once Megatron-Bridge has closed the real one — is fixed: it now goes through MlflowClient().log_text(run_id, ...) (modelopt/torch/utils/mlflow.py:934), with the reason recorded at the call site. The stale-pointer gap in record_checkpoint_provenance is closed too (the helper drop_experiment_jsons when it finds no run, and the saved_before marker keeps a run that died before its own first save from claiming the checkpoint it resumed from).

What I traced this round

  • The shared run. _reattach() is the load-bearing new piece and it holds up: it distinguishes "no run active" (re-attach by id, remembering the status a co-owner terminated with) from "a different run active" (decline, and set_terminated by id rather than leave the run RUNNING, since MLflow's atexit only touches whatever is active). The _closed_as in ("FAILED", "KILLED") filter is the right shape — a co-owner's clean close never masks a failure finish() can see, and a non-terminal RUNNING read back mid-flight never reaches end_run.
  • Rank agreement. distill_run opens on dist.is_last_process() (where Megatron-Bridge looks), broadcasts the run name from rank 0 before tracked_run so the two do not straddle a second boundary, and broadcasts the post-start() URI from src=size-1 so a server that disabled our logger cannot still be handed to LoggerConfig. Both broadcasts are reached on every rank under the same condition (args.mlflow, resolved identically from flag/env on all ranks), so there is no divergent-collective deadlock; a fatal logger.start() on the last rank is covered by the except BaseException: dist.abort() the comment points at.
  • is_main capture across dist.cleanup(). Checked each of the four call sites. export_distilled_megatron_to_hf.py correctly uses the pre-cleanup is_rank_0 in the non-VLM loop and dist.is_master() in the VLM branch (no cleanup there); prune_minitron.py has no in-main() cleanup; distill.py passes owns_the_run captured before tracked_run.
  • split_tracking_credentials. Fails closed on a non-http(s) scheme (where urlparse hides userinfo in .path), rpartition("@") is correct for a percent-encoded userinfo, unquote matches what requests would have done with the credential left in the URI, and returning None for half a credential makes logger_kwargs decline on every rank rather than only the one that noticed.
  • Tool lambdas. Verified every attribute each of the five records reads (hf_model_name_or_path, output_megatron_path, output_hf_path, prune_target_*, megatron_path, export_unified_hf_path, student_hf_path, student_megatron_path, output_dir, hf_export_path) exists on the owning parser, and that the required ones cannot be None when resolve_mlflow_args builds the experiment name. _checkpoint_root normalises an iter_* path to the root DISTILL tags as checkpoint_path, so distill → export joins.
  • Pointer/tag invariants. settles_pointer=False on DISTILL and DISTILL_EXPORT correctly hands path=None to tracked_run so only the per-checkpoint writer touches .experiment.json; _closing_run maps sys.exit(0) to FINISHED, which is what prune's score gate (sys.exit(1) → FAILED) and Megatron-Bridge's --exit_interval need.

Findings — all three are non-blocking polish in the provenance-metadata layer, none affects a checkpoint's own pointer:

  1. DISTILL.checkpoint is unconditional, so a --validate_only run tags checkpoint_path for a checkpoint it does not write — against the invariant run_tags states. Folding validate_only into the Tool also retires the explicit guard in distill.py.
  2. mlflow_utils.resolve_mlflow_args is a pure pass-through; an alias says the same thing without a second docstring to keep in sync.
  3. The comment above record_exported_checkpoint in prune_minitron.py assumes --output_megatron_path is set; with --output_hf_path alone the Tool already names that directory and the pointer is written twice.

Risk: low. Everything lands in examples/megatron_bridge plus three additive helpers in modelopt/torch/utils/mlflow.py; nothing touches modes, config schema, modelopt_state, or an export path's weights/metadata. mlflow stays a lazy optional import behind _open_run/log_active_run_experiment_json, and every failure after the run opens degrades to a warning. Backward compatible: new flags default off, and MlflowRunLogger's existing callers see _reattach() return True on the first check.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review passed — no blocking issues found. LGTM

kevalmorabia97 added a commit that referenced this pull request Sep 24, 2026
[1/2] Merges with #2514 after this.

Three example scripts had each reimplemented the same tracking wiring: the
flags, the $USER/<tool>/<model>-<variant> experiment convention, the params and
tags and artifacts a run uploads, and the open/close dance with its status. The
copies had already drifted -- only hf_ptq wrote a provenance pointer, only
vllm_serve republished the resolved URI -- and every new script meant another
copy.

What a script records is now one declarative Tool record, declared in the script
itself beside the flags it reads: which arguments name its model, its checkpoint
and what it consumed, what it uploads, what it measures. tracked_run takes that
record and runs the whole thing, so a script adds tracking in three lines.
examples/hf_ptq, examples/vllm_serve and examples/megatron_bridge/quantize.py
move onto it with no change in behaviour, beyond hf_ptq's source_checkpoint_path
tag now resolving to an absolute path so it can join the run that produced its
input.

The four test suites shared four copies of a stand-in for the mlflow module,
which had drifted far enough that one made log_artifact a no-op -- a test
asserting on an upload asserted nothing. They now share one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@kevalmorabia97
kevalmorabia97 force-pushed the kmorabia/mbridge-qad-export-mlflow branch from 82c1636 to 783ee5b Compare September 24, 2026 15:39
kevalmorabia97 added a commit that referenced this pull request Sep 24, 2026
[1/2] Merges with #2514 after this.

Three example scripts had each reimplemented the same tracking wiring: the
flags, the $USER/<tool>/<model>-<variant> experiment convention, the params and
tags and artifacts a run uploads, and the open/close dance with its status. The
copies had already drifted -- only hf_ptq wrote a provenance pointer, only
vllm_serve republished the resolved URI -- and every new script meant another
copy.

What a script records is now one declarative Tool record, declared in the script
itself beside the flags it reads: which arguments name its model, its checkpoint
and what it consumed, what it uploads, what it measures. tracked_run takes that
record and runs the whole thing, so a script adds tracking in three lines.
examples/hf_ptq, examples/vllm_serve and examples/megatron_bridge/quantize.py
move onto it with no change in behaviour, beyond hf_ptq's source_checkpoint_path
tag now resolving to an absolute path so it can join the run that produced its
input.

The four test suites shared four copies of a stand-in for the mlflow module,
which had drifted far enough that one made log_artifact a no-op -- a test
asserting on an upload asserted nothing. They now share one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@kevalmorabia97
kevalmorabia97 force-pushed the kmorabia/mbridge-qad-export-mlflow branch from 783ee5b to 87ac734 Compare September 24, 2026 16:04

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (claude-opus-5-5) — DM the bot to share feedback.

Nudge: the code is ready, but this PR is still 531 lines of core logic, over the 500-line budget, and none of the size exceptions apply.

Needs action:

  • ✂️ Split this PR into stacked [x/N] PRs, merged in order after #2544, each passing CI on its own and linking the others. You said you won't split because it's only slightly over; that's the owner's call, but "slightly over" isn't an exception, so split or have the owner post a waiver on the PR. [2/3] Add shared-run MLflow helpers: split_tracking_credentials, log_active_run_experiment_json, _reattach/_closed_as/set_terminated in modelopt/torch/utils/mlflow.py, with their test_mlflow.py cases and FakeMlflow resume support (about 151 lines). [3/3] Track every Megatron-Bridge script with MLflow: examples/megatron_bridge/* plus test_mlflow_utils.py (about 380 lines).

No action needed:

  • ✔️ Resolved since the last review: DISTILL.checkpoint returns None for --validate_only, resolve_mlflow_args is a plain alias, and prune_minitron.py writes the extra HF pointer only when a Megatron checkpoint also exists.
  • The edits to existing tests are justified: the naming test is parametrised over all five tools, and pointer gating now covers prune and export. No assertion was loosened.
  • The design is settled: this extends the Tool/tracked_run core from #2544 rather than adding a second system.

kevalmorabia97 added a commit that referenced this pull request Sep 24, 2026
[1/2] Merges with #2514 after this.

Three example scripts had each reimplemented the same tracking wiring: the
flags, the $USER/<tool>/<model>-<variant> experiment convention, the params and
tags and artifacts a run uploads, and the open/close dance with its status. The
copies had already drifted -- only hf_ptq wrote a provenance pointer, only
vllm_serve republished the resolved URI -- and every new script meant another
copy.

What a script records is now one declarative Tool record, declared in the script
itself beside the flags it reads: which arguments name its model, its checkpoint
and what it consumed, what it uploads, what it measures. tracked_run takes that
record and runs the whole thing, so a script adds tracking in three lines.
examples/hf_ptq, examples/vllm_serve and examples/megatron_bridge/quantize.py
move onto it with no change in behaviour, beyond hf_ptq's source_checkpoint_path
tag now resolving to an absolute path so it can join the run that produced its
input.

The four test suites shared four copies of a stand-in for the mlflow module,
which had drifted far enough that one made log_artifact a no-op -- a test
asserting on an upload asserted nothing. They now share one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@kevalmorabia97
kevalmorabia97 force-pushed the kmorabia/mbridge-qad-export-mlflow branch from 87ac734 to 6f76b6a Compare September 24, 2026 16:59
@kevalmorabia97

Copy link
Copy Markdown
Collaborator Author

Size waiver (repo owner). This work is intentionally split as 2 PRs, not 3: #2544 is the shared tracking core, this PR is the examples/megatron_bridge wiring on top. Each stands alone, builds, and carries its own tests.

This PR is ~563 lines of source against the ~500-line budget. The bulk is five example scripts each declaring their own Tool; splitting them apart would mean several near-identical reviews of the same few-line pattern. The overage is accepted; please review as-is.

kevalmorabia97 added a commit that referenced this pull request Sep 24, 2026
[1/2] Merges with #2514 after this.

Three example scripts had each reimplemented the same tracking wiring: the
flags, the $USER/<tool>/<model>-<variant> experiment convention, the params and
tags and artifacts a run uploads, and the open/close dance with its status. The
copies had already drifted -- only hf_ptq wrote a provenance pointer, only
vllm_serve republished the resolved URI -- and every new script meant another
copy.

What a script records is now one declarative Tool record, declared in the script
itself beside the flags it reads: which arguments name its model, its checkpoint
and what it consumed, what it uploads, what it measures. tracked_run takes that
record and runs the whole thing, so a script adds tracking in three lines.
examples/hf_ptq, examples/vllm_serve and examples/megatron_bridge/quantize.py
move onto it with no change in behaviour, beyond hf_ptq's source_checkpoint_path
tag now resolving to an absolute path so it can join the run that produced its
input.

The four test suites shared four copies of a stand-in for the mlflow module,
which had drifted far enough that one made log_artifact a no-op -- a test
asserting on an upload asserted nothing. They now share one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
kevalmorabia97 and others added 2 commits September 24, 2026 11:32
[2/2] Merges after #2544.

of five scripts in that directory that write a checkpoint; the other four
recorded nothing, so the provenance chain stopped at the PTQ checkpoint and a
deployed model could not be traced back to the run that produced it.

prune_minitron.py, distill.py, export_quantized_megatron_to_hf.py and
export_distilled_megatron_to_hf.py now take the same flags, each declaring what
it records as a Tool beside its own flags. Each writes .experiment.json into the
checkpoint it produced and tags what it consumed, so prune -> quantize ->
distill -> export is walkable both from disk and by tag query.

distill.py opens the run rather than being wrapped by one: Megatron-Bridge's
LoggerConfig records per-iteration metrics and the full resolved config, which a
wrapper cannot see, and it joins mlflow.active_run() when there is one. So the
run is opened on the rank Megatron-Bridge looks at -- the last one -- and the
two share it. Its early exit is handled explicitly: train() leaves through
sys.exit(0) on --exit_interval, which a blanket handler would record as FAILED.

The shared wiring these four need lands here with them: distill_run,
logger_kwargs, checkpoint_marker and the two record_* helpers in mlflow_utils,
the add_mlflow_args wrapper that adds --mlflow_log_checkpoints, the exported
callback's settles_pointer branch and the args.mlflow reset -- each with the
caller that earns it -- plus split_tracking_credentials,
log_active_run_experiment_json and MlflowRunLogger._reattach in the library, the
last because a co-owner can end the run first, as Megatron-Bridge does on
SIGTERM.

Two of Megatron-Bridge's defaults are deliberately not inherited: checkpoint
artifact upload stays off unless --mlflow_log_checkpoints, and an untracked run
passes no mlflow_* fields at all, since they landed in Megatron-Bridge 0.6 and
sending them unconditionally would break an untracked run on an older one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
prune_minitron.py puts --output_megatron_path and --output_hf_path in a required
mutually exclusive group, so exactly one is ever set. The extra
record_exported_checkpoint call added last round sat inside the HF save branch
behind `if args.output_megatron_path:` -- a condition that can never hold there
-- and PRUNE.checkpoint's `or None` tail is dead for the same reason.

The review that asked for it said passing both was a normal invocation, and I
took that without checking the parser. The test could not catch it because
_parse_prune builds its own parser from _SCRIPT_ARGS, so it asserted on two
combinations argparse rejects outright: both flags, and neither. It now covers
the two legal invocations and checks the pointer lands in whichever output was
saved.

Found by running the scripts for real against a live MLflow server, where the
prune step exited 2 before the run was ever opened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
@kevalmorabia97
kevalmorabia97 force-pushed the kmorabia/mbridge-qad-export-mlflow branch from 7142303 to 442547f Compare September 24, 2026 18:32

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (claude-opus-5-5) — DM the bot to share feedback.

Nudge: the code is ready, but this PR is 552 lines of core logic, over the 500-line budget. The owner's waiver is not one of the size exceptions, so a human makes the merge call.

Needs action:

  • ✂️ Split this PR into stacked [x/N] PRs, merged in order after #2544. Each PR should pass CI on its own and link the others. [2/3] Add shared-run MLflow helpers (about 151 lines): split_tracking_credentials, log_active_run_experiment_json, _reattach/_closed_as/set_terminated in modelopt/torch/utils/mlflow.py, with their test_mlflow.py cases and the FakeMlflow resume support. [3/3] Track every Megatron-Bridge script with MLflow (about 401 lines): examples/megatron_bridge/* plus test_mlflow_utils.py. If #2544 also splits, renumber the whole stack to [x/4]. If a reviewer accepts the waiver instead, they should say so on the PR.

No action needed:

  • ✔️ Resolved since the last review: DISTILL.checkpoint returns None for --validate_only, resolve_mlflow_args is a plain alias, mlflow_run clears args.mlflow when the logger turns itself off, every rank declines a URI with only half a user:token, and EXPORT.source normalises iter_* paths.
  • The edits to existing tests are justified: the naming test now runs over all five tools, and the pointer checks now also cover the prune and export scripts. No assertion was loosened.
  • The design is settled: this builds on the Tool/tracked_run core from #2544 rather than adding a second system.

@kevalmorabia97
kevalmorabia97 added this pull request to stack #2546 September 24, 2026 20:31

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants