-
Notifications
You must be signed in to change notification settings - Fork 603
Add MLflow tracking flags to megatron_bridge quantize.py #2477
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,154 @@ | ||||||||||||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||||||||||||
| # SPDX-License-Identifier: Apache-2.0 | ||||||||||||
| # | ||||||||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||
| # you may not use this file except in compliance with the License. | ||||||||||||
| # You may obtain a copy of the License at | ||||||||||||
| # | ||||||||||||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||
| # | ||||||||||||
| # Unless required by applicable law or agreed to in writing, software | ||||||||||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||
| # See the License for the specific language governing permissions and | ||||||||||||
| # limitations under the License. | ||||||||||||
|
|
||||||||||||
| """MLflow tracking for ``quantize.py``, mirroring ``examples/hf_ptq``. | ||||||||||||
|
|
||||||||||||
| Every rank parses and validates the same flags, so a typo in the URI fails identically | ||||||||||||
| everywhere instead of on one rank while the others wait in a collective. Only the master rank | ||||||||||||
| opens a run, so the log capture and the uploads happen once. | ||||||||||||
|
|
||||||||||||
| Nothing here imports Megatron, so the tracking can be exercised without it. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| import argparse | ||||||||||||
| from collections.abc import Iterator | ||||||||||||
| from contextlib import contextmanager | ||||||||||||
| from pathlib import Path | ||||||||||||
|
|
||||||||||||
| import yaml | ||||||||||||
|
|
||||||||||||
| import modelopt.torch.utils.distributed as dist | ||||||||||||
| from modelopt.recipe import load_recipe | ||||||||||||
| from modelopt.torch.utils.mlflow import MlflowRunLogger, drop_experiment_json | ||||||||||||
| from modelopt.torch.utils.mlflow import add_mlflow_args as _add_mlflow_args | ||||||||||||
| from modelopt.torch.utils.mlflow import resolve_mlflow_args as _resolve_mlflow_args | ||||||||||||
|
|
||||||||||||
| TOOL_NAME = "megatron_bridge_quantize" | ||||||||||||
|
|
||||||||||||
| # The tracking settings describe the destination rather than the quantization, and | ||||||||||||
| # checkpoint_exported is this script's own bookkeeping. | ||||||||||||
| _NON_PARAM_ARGS = frozenset( | ||||||||||||
| { | ||||||||||||
| "checkpoint_exported", | ||||||||||||
| "mlflow", | ||||||||||||
| "mlflow_experiment", | ||||||||||||
| "mlflow_required", | ||||||||||||
| "mlflow_run_name", | ||||||||||||
| } | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def add_mlflow_args(parser: argparse.ArgumentParser) -> None: | ||||||||||||
| """Add the MLflow tracking flags.""" | ||||||||||||
| _add_mlflow_args( | ||||||||||||
| parser, | ||||||||||||
| TOOL_NAME, | ||||||||||||
| tracks=( | ||||||||||||
| "Track this run on an MLflow server (e.g. https://<your-mlflow-server>/), " | ||||||||||||
| "uploading the command, the resolved recipe, the run log and the quantizer " | ||||||||||||
| "summary, and writing .experiment.json into --export_megatron_path so the " | ||||||||||||
| "checkpoint names the run that produced it." | ||||||||||||
| ), | ||||||||||||
| variant_help="recipe name, or --quant_cfg if no --recipe", | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def resolve_mlflow_args(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None: | ||||||||||||
| """Settle where tracking is configured from, and name the experiment.""" | ||||||||||||
| _resolve_mlflow_args( | ||||||||||||
| args, | ||||||||||||
| parser, | ||||||||||||
| tool=TOOL_NAME, | ||||||||||||
| model=args.hf_model_name_or_path, | ||||||||||||
| # ``or "none"``: neither flag is required by the parser, and the run that reaches | ||||||||||||
| # get_quant_config without one fails there rather than while being named. | ||||||||||||
| variant=Path(args.recipe).stem if args.recipe else (args.quant_cfg or "none"), | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _run_inputs(args: argparse.Namespace) -> tuple[dict, dict]: | ||||||||||||
| """Params and start-time artifacts describing this PTQ run.""" | ||||||||||||
| params = {k: v for k, v in vars(args).items() if k not in _NON_PARAM_ARGS} | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: sed -n '45,95p' SECURITY.md
sed -n '25,125p' examples/megatron_bridge/mlflow_utils.py
sed -n '220,400p' modelopt/torch/utils/mlflow.py
rg -n 'prompt|print_args|print_rank_0|log_input|log_param|capture|output' examples/megatron_bridge/quantize.py examples/megatron_bridge/mlflow_utils.py modelopt/torch/utils/mlflow.pyRepository: NVIDIA/Model-Optimizer Length of output: 18588 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- megatron flow ---'
sed -n '220,490p' examples/megatron_bridge/quantize.py
printf '%s\n' '--- helper and logger upload paths ---'
sed -n '1,175p' examples/megatron_bridge/mlflow_utils.py
sed -n '400,555p' modelopt/torch/utils/mlflow.py
printf '%s\n' '--- security mlflow wording ---'
rg -n -C 4 'MLflow|tracking|sensitive inputs|proprietary model|tokens|credentials|artifacts|logs' SECURITY.mdRepository: NVIDIA/Model-Optimizer Length of output: 27967 Sensitive Data Exposure Reachability: External Restrict MLflow inputs and captured output to non-sensitive data.
Use an allowlist for parameters. Exclude or redact prompts, paths, model details, and sensitive recipe fields from parameters, tags, command artifacts, resolved recipe artifacts, and captured logs. SECURITY.md provides applicable logging guidance for this production example, but the quoted “avoid logging” sentence is advisory rather than one of the explicitly mandatory CRITICAL-pattern checks. 🤖 Prompt for AI Agents |
||||||||||||
| # The parallelism flags say how the run was laid out but not how many GPUs it took: | ||||||||||||
| # data parallelism is implicit in the launcher's world size. | ||||||||||||
| params["world_size"] = dist.size() | ||||||||||||
| texts = {} | ||||||||||||
| if args.recipe: | ||||||||||||
| # The resolved recipe, not the source file: a recipe may be a directory or use | ||||||||||||
| # $imports, and only the resolved form is self-contained. | ||||||||||||
| resolved = load_recipe(args.recipe).model_dump(mode="json") | ||||||||||||
| texts["recipe/resolved_recipe.yaml"] = yaml.safe_dump(resolved, sort_keys=False) | ||||||||||||
| return params, texts | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _run_tags(args: argparse.Namespace) -> dict[str, str]: | ||||||||||||
| """Tags shared with ``hf_ptq`` and the evaluation side, so a PTQ run and whatever is | ||||||||||||
| later done with the checkpoint it produced can be found together on one server. | ||||||||||||
|
|
||||||||||||
| ``checkpoint_path`` is the checkpoint this run *writes*, because that is what | ||||||||||||
| ``export_quantized_megatron_to_hf.py`` (and any QAD run) is later pointed at; the input is | ||||||||||||
| kept separately. It is resolved because a relative path is useless as a join key. | ||||||||||||
| """ | ||||||||||||
| return { | ||||||||||||
| "model": Path(args.hf_model_name_or_path).name, | ||||||||||||
| "checkpoint_path": str(Path(args.export_megatron_path).resolve()), | ||||||||||||
| "source_checkpoint_path": args.hf_model_name_or_path, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _run_outputs(args: argparse.Namespace) -> dict[str, Path]: | ||||||||||||
| """Summaries written beside the checkpoint, keyed by artifact path. | ||||||||||||
|
|
||||||||||||
| Uploaded without the leading dot, which is awkward to browse in the MLflow UI. A missing | ||||||||||||
| entry is skipped: the summary is written by the master rank only once quantization | ||||||||||||
| has finished. | ||||||||||||
| """ | ||||||||||||
| return {"summary/quant_summary.txt": Path(args.export_megatron_path) / ".quant_summary.txt"} | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @contextmanager | ||||||||||||
| def mlflow_run(args: argparse.Namespace) -> Iterator[None]: | ||||||||||||
| """Track this invocation for the duration of the block, and keep the checkpoint's | ||||||||||||
| provenance pointer honest whether or not the run is tracked.""" | ||||||||||||
| logger = MlflowRunLogger( | ||||||||||||
| args.mlflow or "", | ||||||||||||
| args.mlflow_experiment, | ||||||||||||
| run_name=args.mlflow_run_name, | ||||||||||||
| enabled=bool(args.mlflow) and dist.is_master(), | ||||||||||||
| required=args.mlflow_required, | ||||||||||||
| ) | ||||||||||||
| export_path = Path(args.export_megatron_path) | ||||||||||||
| if not logger.enabled: | ||||||||||||
| # Gathering the inputs re-reads the recipe, so keep it off the untracked path. | ||||||||||||
| try: | ||||||||||||
| yield | ||||||||||||
| finally: | ||||||||||||
| if args.checkpoint_exported and dist.is_master(): | ||||||||||||
| drop_experiment_json(export_path) | ||||||||||||
| return | ||||||||||||
| params, texts = _run_inputs(args) | ||||||||||||
| with logger.track( | ||||||||||||
| params=params, | ||||||||||||
| tags=_run_tags(args), | ||||||||||||
| texts=texts, | ||||||||||||
| files=_run_outputs(args), | ||||||||||||
| ): | ||||||||||||
| try: | ||||||||||||
| yield | ||||||||||||
| finally: | ||||||||||||
| # Only a completed save may claim the checkpoint the pointer sits next to: | ||||||||||||
| # --export_megatron_path exists from print_quant_summary onwards, and may hold a | ||||||||||||
| # checkpoint from an earlier attempt whose weights this run never wrote. | ||||||||||||
| logger.log_experiment_json(export_path if args.checkpoint_exported else None) | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Remove stale provenance when optional MLflow startup fails. When tracking comes from A reused export directory therefore keeps an old Proposed fix- logger.log_experiment_json(export_path if args.checkpoint_exported else None)
+ checkpoint_path = export_path if args.checkpoint_exported else None
+ logger.log_experiment_json(checkpoint_path)
+ if args.checkpoint_exported and not logger.run_info:
+ drop_experiment_json(export_path)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Remove stale provenance after optional tracking fails.
When
MLFLOW_TRACKING_URIenables tracking without--mlflow,requiredis false. IfMlflowRunLogger.start()fails, it disables itself and later logger operations are no-ops. A completed export can then retain an old.experiment.jsonbecause this block does not call_drop_inherited_experiment_json.Call
_drop_inherited_experiment_json(args, export_path)when the logger is disabled after the tracked block. Add a regression test for an environment-configured logger that fails to start.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents