Skip to content

Commit bb9be60

Browse files
cjluo-nvclaude
andcommitted
Move the MLflow wiring into example_utils and simplify the flag handling
hf_ptq.py is down from ~90 lines of tracking glue to three calls: add_mlflow_args(parser), resolve_mlflow_args(args, parser) and `with mlflow_run(args):`. Everything that knows about the flags, the params, the tags and the summary paths now sits beside the other hf_ptq helpers in example_utils. Simplify how the URI is settled. Provenance is now just "did the user type the flag", read before the fallback, so the whole thing is one assignment and one branch: args.mlflow_required = args.mlflow is not None args.mlflow = args.mlflow or os.environ.get("MLFLOW_TRACKING_URI") or None Dropping nargs="?" is what makes that work. The bare --mlflow form meant "use $MLFLOW_TRACKING_URI", which is now what happens with no flag at all, so it was redundant -- and it was the only source of the empty-string case that forced the three-way logic. A bare --mlflow now gets argparse's own "expected one argument". Move the MLflow section of the README to the end, per review: it is a lot of detail to meet before the feature examples like AutoQuantize. Added to the contents table so it stays findable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
1 parent d40f751 commit bb9be60

4 files changed

Lines changed: 225 additions & 207 deletions

File tree

examples/hf_ptq/README.md

Lines changed: 56 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This section focuses on Post-training quantization, a technique that reduces mod
1919
| Evaluate Accuracy | Evaluate your model's accuracy! | \[[Link](#evaluate-accuracy)\] | |
2020
| Exporting Checkpoints | Export to Hugging Face Unified Checkpoint and deploy on TRT-LLM/vLLM/SGLang | \[[Link](#exporting-checkpoints)\] | \[[docs](https://nvidia.github.io/Model-Optimizer/deployment/3_unified_hf.html)\] |
2121
| Pre-Quantized Checkpoints | Ready to deploy Hugging Face pre-quantized checkpoints | \[[Link](#pre-quantized-checkpoints)\] | |
22+
| Tracking runs with MLflow | Record a PTQ run on an MLflow server so it can be reproduced from its entry alone | \[[Link](#tracking-runs-with-mlflow)\] | |
2223
| Resources | Extra links to relevant resources | \[[Link](#resources)\] | |
2324

2425
</div>
@@ -293,61 +294,6 @@ scripts/huggingface_example.sh --model <model> --quant nvfp4 --vlm --calib_with_
293294
> Note: when `--calib_with_images` is set, `--calib_size` must be a single value, and the calibration dataset is nvidia/nemotron_vlm_dataset_v2.
294295
This functionality is currently in beta and has been tested on `nvidia/NVIDIA-Nemotron-Nano-12B-v2-VL-BF16`.
295296

296-
### Tracking runs with MLflow
297-
298-
Set MLflow's own `MLFLOW_TRACKING_URI`, or pass `--mlflow <tracking-uri>`, to record a PTQ
299-
run on an MLflow server so it can be reproduced later from its MLflow entry alone:
300-
301-
```bash
302-
python hf_ptq.py \
303-
--pyt_ckpt_path <huggingface_model_card> \
304-
--recipe general/ptq/nvfp4_default-kv_fp8_cast \
305-
--export_path <quantized_ckpt_path> \
306-
--mlflow https://<your-mlflow-server>/
307-
```
308-
309-
The run is opened *before* the model loads, so a bad URI or a missing token fails within
310-
seconds rather than after a full calibration.
311-
312-
<details>
313-
<summary>Uploaded artifacts</summary>
314-
315-
| Artifact | Contents |
316-
| --- | --- |
317-
| `command.txt` | The full invocation, copy-pasteable, with credentials masked |
318-
| `version.txt` | The ModelOpt version that ran |
319-
| `recipe/resolved_recipe.yaml` | The `--recipe` with its `$import`s expanded, so it stands alone |
320-
| `logs/hf_ptq.log` | The run's Python stdout/stderr, including the traceback if it crashed |
321-
| `summary/quant_summary.txt` | The per-quantizer summary (unless `--no-verbose`) |
322-
| `summary/moe.html` | Per-expert calibration token counts, when the run produces them |
323-
324-
</details>
325-
326-
Every command-line argument is also logged as a searchable param, alongside
327-
`user` / `hostname` / `modelopt_version` / `git_sha` tags. A run that fails is
328-
still recorded, with status `FAILED` and its log attached.
329-
330-
Other flags:
331-
332-
- `--mlflow_experiment` — defaults to `$USER/hf_ptq/<checkpoint basename>-<recipe name>`,
333-
falling back to `--qformat` when no `--recipe` is used.
334-
- `--mlflow_run_name` — defaults to the UTC start time, `YYYYmmdd-HHMMSS`.
335-
- `$MLFLOW_TRACKING_URI` enables tracking on its own; `--mlflow` overrides it. A URI taken
336-
from the environment is best-effort — if the client is missing or the server is
337-
unreachable the run warns and continues untracked, since the variable is often exported
338-
for other tooling. An explicit `--mlflow` fails loudly instead.
339-
340-
Authentication uses MLflow's own environment variables (`MLFLOW_TRACKING_TOKEN`, or
341-
`MLFLOW_TRACKING_USERNAME` / `MLFLOW_TRACKING_PASSWORD`).
342-
343-
The tracking itself lives in `modelopt.torch.utils.mlflow`
344-
([`MlflowRunLogger`](../../modelopt/torch/utils/mlflow.py)), so other example scripts can
345-
record runs the same way; `hf_ptq.py` only supplies the params and artifacts specific to PTQ.
346-
347-
> Note: only the main rank uploads, so `--use_fsdp2` runs produce a single run. The log
348-
> captures Python output; output written directly by native libraries (NCCL, CUDA) goes to
349-
> the terminal only. On SLURM, keep the job's own `.out` file for those.
350-
351297
### Megatron-Bridge Example Script
352298

353299
Please refer to [examples/megatron_bridge/README.md](../megatron_bridge/README.md) for example scripts for PTQ / QAD with Megatron-Bridge which is generally more performant than the Hugging Face scripts.
@@ -693,6 +639,61 @@ After the TensorRT-LLM checkpoint export, you can use the `trtllm-build` build c
693639
- Deployable on [TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM), [vLLM](https://github.com/vllm-project/vllm) and [SGLang](https://github.com/sgl-project/sglang)
694640
- More models coming soon!
695641

642+
## Tracking runs with MLflow
643+
644+
Set MLflow's own `MLFLOW_TRACKING_URI`, or pass `--mlflow <tracking-uri>`, to record a PTQ
645+
run on an MLflow server so it can be reproduced later from its MLflow entry alone:
646+
647+
```bash
648+
python hf_ptq.py \
649+
--pyt_ckpt_path <huggingface_model_card> \
650+
--recipe general/ptq/nvfp4_default-kv_fp8_cast \
651+
--export_path <quantized_ckpt_path> \
652+
--mlflow https://<your-mlflow-server>/
653+
```
654+
655+
The run is opened *before* the model loads, so a bad URI or a missing token fails within
656+
seconds rather than after a full calibration.
657+
658+
<details>
659+
<summary>Uploaded artifacts</summary>
660+
661+
| Artifact | Contents |
662+
| --- | --- |
663+
| `command.txt` | The full invocation, copy-pasteable, with credentials masked |
664+
| `version.txt` | The ModelOpt version that ran |
665+
| `recipe/resolved_recipe.yaml` | The `--recipe` with its `$import`s expanded, so it stands alone |
666+
| `logs/hf_ptq.log` | The run's Python stdout/stderr, including the traceback if it crashed |
667+
| `summary/quant_summary.txt` | The per-quantizer summary (unless `--no-verbose`) |
668+
| `summary/moe.html` | Per-expert calibration token counts, when the run produces them |
669+
670+
</details>
671+
672+
Every command-line argument is also logged as a searchable param, alongside
673+
`user` / `hostname` / `modelopt_version` / `git_sha` tags. A run that fails is
674+
still recorded, with status `FAILED` and its log attached.
675+
676+
Other flags:
677+
678+
- `--mlflow_experiment` — defaults to `$USER/hf_ptq/<checkpoint basename>-<recipe name>`,
679+
falling back to `--qformat` when no `--recipe` is used.
680+
- `--mlflow_run_name` — defaults to the UTC start time, `YYYYmmdd-HHMMSS`.
681+
- `$MLFLOW_TRACKING_URI` enables tracking on its own; `--mlflow` overrides it. A URI taken
682+
from the environment is best-effort — if the client is missing or the server is
683+
unreachable the run warns and continues untracked, since the variable is often exported
684+
for other tooling. An explicit `--mlflow` fails loudly instead.
685+
686+
Authentication uses MLflow's own environment variables (`MLFLOW_TRACKING_TOKEN`, or
687+
`MLFLOW_TRACKING_USERNAME` / `MLFLOW_TRACKING_PASSWORD`).
688+
689+
The tracking itself lives in `modelopt.torch.utils.mlflow`
690+
([`MlflowRunLogger`](../../modelopt/torch/utils/mlflow.py)), so other example scripts can
691+
record runs the same way; `hf_ptq.py` only supplies the params and artifacts specific to PTQ.
692+
693+
> Note: only the main rank uploads, so `--use_fsdp2` runs produce a single run. The log
694+
> captures Python output; output written directly by native libraries (NCCL, CUDA) goes to
695+
> the terminal only. On SLURM, keep the job's own `.out` file for those.
696+
696697
## Resources
697698

698699
- 📅 [Roadmap](https://github.com/NVIDIA/Model-Optimizer/issues/1699)

examples/hf_ptq/example_utils.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import argparse
1617
import copy
1718
import glob
1819
import hashlib
@@ -23,13 +24,15 @@
2324
import shutil
2425
import warnings
2526
from collections.abc import Callable, Iterable
27+
from contextlib import AbstractContextManager, nullcontext
2628
from dataclasses import dataclass
2729
from datetime import timedelta
2830
from pathlib import Path
2931
from typing import Any
3032

3133
import torch
3234
import transformers
35+
import yaml
3336
from accelerate import infer_auto_device_map, init_empty_weights
3437
from accelerate.utils import get_max_memory
3538
from safetensors import safe_open
@@ -43,6 +46,7 @@
4346
ProcessorMixin,
4447
)
4548

49+
from modelopt.recipe import load_recipe
4650
from modelopt.torch.export.model_utils import is_multimodal_model
4751

4852
try:
@@ -51,6 +55,11 @@
5155
snapshot_download = None
5256

5357
from modelopt.torch.utils import distributed as dist_utils
58+
from modelopt.torch.utils.mlflow import (
59+
MlflowRunLogger,
60+
default_experiment_name,
61+
validate_tracking_uri,
62+
)
5463

5564
logger = logging.getLogger(__name__)
5665

@@ -1070,3 +1079,119 @@ def resolve_checkpoint_dir(quant_cfg: dict, model_path: str) -> tuple[dict, str]
10701079
if isinstance(algo.get("layerwise"), dict) and "checkpoint_dir" in algo["layerwise"]:
10711080
algo["layerwise"]["checkpoint_dir"] = resolved
10721081
return quant_cfg, resolved
1082+
1083+
1084+
def add_mlflow_args(parser: argparse.ArgumentParser) -> None:
1085+
"""Add the MLflow tracking flags."""
1086+
parser.add_argument(
1087+
"--mlflow",
1088+
default=None,
1089+
help=(
1090+
"Track this run on an MLflow server (e.g. https://<your-mlflow-server>/), "
1091+
"uploading the command, the resolved recipe, the run log and the quantization "
1092+
"summaries. MLflow's own $MLFLOW_TRACKING_URI enables tracking without this "
1093+
"flag, which overrides it. A URI taken from the environment is best-effort: if "
1094+
"it is unusable the run warns and continues untracked."
1095+
),
1096+
)
1097+
parser.add_argument(
1098+
"--mlflow_experiment",
1099+
default=None,
1100+
help=(
1101+
"MLflow experiment name. Default: "
1102+
"$USER/hf_ptq/<checkpoint basename>-<recipe name, or --qformat if no --recipe>."
1103+
),
1104+
)
1105+
parser.add_argument(
1106+
"--mlflow_run_name",
1107+
default=None,
1108+
help="MLflow run name. Default: the UTC start time as YYYYmmdd-HHMMSS.",
1109+
)
1110+
1111+
1112+
def resolve_mlflow_args(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
1113+
"""Settle where tracking is configured from, and name the experiment."""
1114+
# MLflow's own variable enables tracking on its own; --mlflow overrides it. Only the
1115+
# flag is a deliberate request, so only the flag is fatal when the URI is unusable: the
1116+
# variable is commonly exported for unrelated tooling and must not fail a quantization.
1117+
args.mlflow_required = args.mlflow is not None
1118+
args.mlflow = args.mlflow or os.environ.get("MLFLOW_TRACKING_URI") or None
1119+
if args.mlflow:
1120+
try:
1121+
args.mlflow = validate_tracking_uri(args.mlflow)
1122+
except ValueError as e:
1123+
if args.mlflow_required:
1124+
parser.error(f"--mlflow: {e}")
1125+
warnings.warn(f"Ignoring MLFLOW_TRACKING_URI, continuing untracked: {e}")
1126+
args.mlflow = None
1127+
else:
1128+
args.mlflow_experiment = args.mlflow_experiment or default_experiment_name(
1129+
"hf_ptq",
1130+
args.pyt_ckpt_path,
1131+
Path(args.recipe).stem if args.recipe else args.qformat,
1132+
)
1133+
1134+
1135+
_MLFLOW_NON_PARAM_ARGS = frozenset(
1136+
{"dist_state", "mlflow", "mlflow_experiment", "mlflow_required", "mlflow_run_name"}
1137+
)
1138+
1139+
1140+
def _mlflow_run_inputs(args: argparse.Namespace) -> tuple[dict, dict]:
1141+
"""Params and start-time artifacts describing this PTQ run."""
1142+
params = {k: v for k, v in vars(args).items() if k not in _MLFLOW_NON_PARAM_ARGS}
1143+
# dist_state is an object, so record the one field worth searching on.
1144+
params["world_size"] = args.dist_state.world_size
1145+
texts = {}
1146+
if args.recipe:
1147+
# The resolved recipe, not the source file: a recipe may be a directory or use
1148+
# $imports, and only the resolved form is self-contained.
1149+
resolved = load_recipe(args.recipe).model_dump(mode="json")
1150+
texts["recipe/resolved_recipe.yaml"] = yaml.safe_dump(resolved, sort_keys=False)
1151+
return params, texts
1152+
1153+
1154+
def _mlflow_logger(args: argparse.Namespace) -> MlflowRunLogger:
1155+
"""Build this run's logger; inert unless --mlflow was given and this is the main rank."""
1156+
return MlflowRunLogger(
1157+
args.mlflow,
1158+
args.mlflow_experiment,
1159+
run_name=args.mlflow_run_name,
1160+
enabled=bool(args.mlflow) and args.dist_state.is_main,
1161+
required=args.mlflow_required,
1162+
)
1163+
1164+
1165+
def mlflow_run(args: argparse.Namespace) -> AbstractContextManager:
1166+
"""Track this invocation for the duration of the block, or do nothing if untracked."""
1167+
logger = _mlflow_logger(args)
1168+
if not logger.enabled:
1169+
# Gathering the inputs re-reads the recipe, so keep it off the untracked path.
1170+
return nullcontext()
1171+
params, texts = _mlflow_run_inputs(args)
1172+
return logger.track(
1173+
params=params,
1174+
tags=_mlflow_run_tags(args),
1175+
texts=texts,
1176+
files=_mlflow_run_outputs(args),
1177+
)
1178+
1179+
1180+
def _mlflow_run_tags(args: argparse.Namespace) -> dict[str, str]:
1181+
"""Tags shared with the evaluation side, so a PTQ run and the evaluations of the
1182+
checkpoint it produced can be found together on one tracking server."""
1183+
return {"model": Path(args.pyt_ckpt_path).name, "checkpoint_path": args.pyt_ckpt_path}
1184+
1185+
1186+
def _mlflow_run_outputs(args: argparse.Namespace) -> dict[str, Path]:
1187+
"""Summaries written by post_quantize, keyed by artifact path.
1188+
1189+
Uploaded without the leading dot, which is awkward to browse in the MLflow UI. Missing
1190+
entries are skipped: the MoE table only exists for MoE models, and neither file is
1191+
written under ``--no-verbose``.
1192+
"""
1193+
export_path = Path(args.export_path)
1194+
return {
1195+
"summary/quant_summary.txt": export_path / ".quant_summary.txt",
1196+
"summary/moe.html": export_path / ".moe.html",
1197+
}

0 commit comments

Comments
 (0)