Skip to content

Add optional MLflow tracking to hf_ptq.py - #2023

Open
cjluo-nv wants to merge 8 commits into
mainfrom
feat/hf-ptq-mlflow-tracking
Open

Add optional MLflow tracking to hf_ptq.py#2023
cjluo-nv wants to merge 8 commits into
mainfrom
feat/hf-ptq-mlflow-tracking

Conversation

@cjluo-nv

@cjluo-nv cjluo-nv commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: new feature

Adds modelopt.torch.utils.mlflow.MlflowRunLogger, a reusable helper for recording a script run on an MLflow tracking server, and wires examples/hf_ptq/hf_ptq.py up to it via --mlflow <tracking-uri> so a PTQ run can be reproduced from its MLflow entry alone. Without the flag, behavior is unchanged — every hook is gated on it.

The logger lives in the library rather than the example so other scripts can record runs the same way: it takes a tracking URI, an experiment name and an explicit enabled flag, with params, tags and artifacts passed in. hf_ptq.py supplies only the PTQ-specific pieces (its params, the resolved recipe, the quantization summaries). mlflow is an optional dependency, imported only once tracking is enabled, so it is not a new requirement for the library.

The run is opened before the model loads, so a bad URI or an unreachable server fails in seconds rather than after hours of calibration. The invocation and the recipe are uploaded at that point too, which keeps a crashed run useful: it is still recorded, with status FAILED and its log attached.

Uploaded artifacts:

Artifact Contents
command.txt The full invocation, copy-pasteable
version.txt The ModelOpt version that ran (also a searchable tag)
recipe/resolved_recipe.yaml The --recipe with $imports expanded
logs/hf_ptq.log Everything the run printed, including a crash traceback
summary/quant_summary.txt Per-quantizer summary (unless --no-verbose)
summary/moe.html Per-expert calibration token counts, when the run produces them

Plus model / format / calibration settings as searchable params, and user / hostname / modelopt_version / git_sha tags.

Three design points worth review:

  1. The recipe is uploaded resolved, not verbatim. A recipe may be a directory or use $imports, so the source file is not self-contained. For huggingface/qwen3_6_moe/auto_quantize/w4a16_nvfp4_fp8_at_6p0bits-active_moe the source is 2,230 B / 58 lines against 7,563 B / 308 lines resolved — the raw file records under 30% of what actually ran.
  2. hf_ptq.py has no logging framework (bare print()), so the log is produced by teeing stdout/stderr. Handlers that libraries bound to sys.stderr at import time are re-pointed at the tee for the run's duration and handed back afterwards; without that, transformers / huggingface_hub warnings reach the console but never the log. Native (C-level) output is still not captured — documented in the README.
  3. The recipe upload lives in the caller, not the library. That keeps modelopt.recipe out of modelopt.torch.utils, which would otherwise risk a modelopt.torch.utilsmodelopt.recipemodelopt.torch.quantizationmodelopt.torch.utils import cycle.
  4. MLflow failures never fail the quantization. Startup validation is fatal by design (it is before any GPU work); the end-of-run upload is best-effort.

Only the main rank uploads, so --use_fsdp2 runs produce a single run.

Usage

python hf_ptq.py \
  --pyt_ckpt_path <huggingface_model_card> \
  --recipe general/ptq/nvfp4_default-kv_fp8_cast \
  --export_path <quantized_ckpt_path> \
  --mlflow https://<your-mlflow-server>/
[mlflow] experiment: $USER/hf_ptq/<checkpoint basename>-<recipe name>
[mlflow] run: https://<your-mlflow-server>/#/experiments/13/runs/c243352e...

--mlflow_experiment and --mlflow_run_name override the defaults ($USER/hf_ptq/<basename>-<recipe name or --qformat>, and the UTC start time). Passing --mlflow with no value uses $MLFLOW_TRACKING_URI. Authentication uses MLflow's own env vars.

Testing

Unit — 31 tests in tests/unit/torch/utils/test_mlflow.py for the library, plus 12 in tests/examples/hf_ptq/test_hf_ptq_args.py for the hf_ptq wiring. CPU-only, no network and no mlflow dependency (driven against a stub module). Covers experiment-name derivation and sanitization, URI accept/reject, tee pass-through, the pre-bound-handler redirect, artifact renaming, skipping absent optional outputs, the disabled path, and version.txt. 63 tests pass together with the existing test_hf_ptq_args.py / test_example_utils.py.

Hardware — real PTQ runs against a live MLflow server:

Run Result
Qwen3-0.6B, NVFP4 PTQ, 1×B200 FINISHED, all artifacts, sane post-quant generations
Qwen3.6-35B-A3B MoE, AutoQuantize w4a16_nvfp4_fp8_at_6p0bits-active_moe, 2×B200 FINISHED in 63 min, search hit effective bits: 6.00; 106 KB log capturing every per-layer decision, 4.4 MB quant summary
Qwen3.6-35B-A3B, plain NVFP4 PTQ, 2×B200 FINISHED
Qwen3-0.6B re-run after the library move, 1×H200 FINISHED, all five artifacts including version.txt
Run without --mlflow after the review fixes exactly 1 [load_recipe] line and 0 [mlflow] lines, confirming the untracked path is untouched
Crash mid-run (gated HF dataset) FAILED recorded with log + traceback attached, summaries correctly absent
Malformed URI Rejected by argparse with a Did you mean https://…? hint
Unreachable host Fails in 9.9 s total, before any model load
No --mlflow Exit 0, no MLflow output, unchanged export

Coverage gap, stated plainly: summary/moe.html is verified only against a synthetic file (unit test + a real upload). It could not be produced naturally — expert_token_count buffers live on _QuantSparseSequentialMoe, while Qwen3.5/3.6 experts take the fused _QuantFusedExperts path, so no such file is written for these models regardless of --moe_calib_experts_ratio. The uploader's conditional is correct; the branch simply had no natural input available here.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ — new optional flags only; no --mlflow means no behavior change.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: ✅ — adds mlflow as an optional extra in pyproject.toml (nvidia-modelopt[mlflow], folded into all) and to examples/hf_ptq/requirements.txt. Apache-2.0 (permissive). Imported lazily, so it is not required to install or import ModelOpt. No code copied from other sources.
  • Did you write any new necessary tests?: ✅ — 29 new unit tests.
  • Did you update Changelog?: ✅ — 0.47 New Features.
  • Did you get Claude approval on this PR?: ❌ — /claude review not yet run. A self-review was done first and its six findings are fixed in the third commit (the notable one: gathering the MLflow inputs re-read the recipe on every run, including without --mlflow).

Additional Information

The one deliberate coverage gap is summary/moe.html, described under Testing: no model available here takes the sparse-sequential MoE path that writes it, so it is covered by unit test and a synthetic upload rather than a natural one. The uploader treats it as an optional output and skips it when absent, which is exercised by test.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional MLflow tracking for Hugging Face PTQ runs.
    • Record searchable run parameters, model and checkpoint details, resolved recipes, quantization summaries, logs, and optional artifacts.
    • Failed runs are captured with FAILED status and diagnostic logs.
    • Added options to configure the tracking URI, experiment, and run name.
  • Documentation

    • Added setup and usage guidance, authentication details, artifact descriptions, and distributed-run considerations for MLflow tracking.

Records a PTQ run on an MLflow server so it can be reproduced from its
MLflow entry alone. Enabled by --mlflow <tracking-uri>; without the flag
nothing changes.

The run is opened before the model loads, so an unreachable server or a
bad URI fails in seconds rather than after hours of calibration. The
invocation and the recipe are uploaded at that point too, which keeps a
crashed run useful: it is still recorded, with status FAILED and its log
attached.

Uploads command.txt, recipe/resolved_recipe.yaml, logs/hf_ptq.log and the
quantization summaries, plus the model / format / calibration settings as
searchable params. The recipe is uploaded resolved rather than verbatim
because a recipe may be a directory or use $imports -- for the Qwen3.6 MoE
AutoQuantize recipe the source file is 2.2 KB against 7.6 KB resolved, so
only the resolved form describes what actually ran.

hf_ptq.py has no logging framework, so the log is produced by teeing
stdout/stderr. Handlers that libraries bound to sys.stderr at import time
are re-pointed at the tee for the duration, otherwise transformers and
huggingface_hub warnings reach the console but never the log.

Experiment defaults to $USER/hf_ptq/<checkpoint basename>-<recipe name, or
--qformat when no recipe>; run name defaults to the UTC start time. Only
the main rank uploads. MLflow failures never fail the quantization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Adds optional MLflow run tracking to the HF PTQ example, including CLI configuration, artifact and log capture, failure recording, client-only dependencies, shared stream handling, tests, documentation, and changelog updates.

Changes

MLflow PTQ tracking

Layer / File(s) Summary
Logger lifecycle and stream capture
modelopt/torch/utils/logging.py, modelopt/torch/utils/mlflow.py
Exports the shared TeeStream and records redacted command data, captured logs, and active exception tracebacks during MLflow run finalization.
HF PTQ MLflow integration
examples/hf_ptq/hf_ptq.py, examples/hf_ptq/requirements.txt, pyproject.toml, tests/examples/hf_ptq/test_hf_ptq_args.py
Adds MLflow CLI options, resolved recipe and summary artifacts, main-rank run startup, status-aware cleanup, and mlflow-skinny dependency configuration with argument and tag tests.
MLflow logger validation
tests/unit/torch/utils/test_mlflow.py
Tests URI validation, experiment naming, stream capture, artifact uploads, credential redaction, unreachable servers, failure handling, and traceback contents.
Tracking documentation and release notes
examples/hf_ptq/README.md, CHANGELOG.rst
Documents MLflow PTQ commands, artifacts, parameters, authentication, rank behavior, and the 0.47 changes.

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

Sequence Diagram(s)

sequenceDiagram
  participant PTQScript
  participant MlflowRunLogger
  participant MLflowServer
  PTQScript->>MlflowRunLogger: Start tracking before model loading
  MlflowRunLogger->>MLflowServer: Validate tracking URI and create run
  MlflowRunLogger->>MLflowServer: Log parameters, tags, recipe, and captured logs
  PTQScript->>MlflowRunLogger: Finish with status and summary artifacts
  MlflowRunLogger->>MLflowServer: Upload outputs and close run
Loading

Suggested reviewers: sugunav14, chenhanyu, chadvoegele

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: optional MLflow tracking added to hf_ptq.py.
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 No banned load/exec/nosec patterns in changed Python files; new mlflow-skinny dependency is Apache-2.0/permissive.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/hf-ptq-mlflow-tracking
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hf-ptq-mlflow-tracking

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

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.07968% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.84%. Comparing base (33d05b0) to head (05d0a6b).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/torch/utils/mlflow.py 83.40% 39 Missing ⚠️
modelopt/torch/utils/logging.py 31.25% 11 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2023       +/-   ##
===========================================
+ Coverage   66.83%   77.84%   +11.01%     
===========================================
  Files         519      520        +1     
  Lines       58916    59830      +914     
===========================================
+ Hits        39376    46575     +7199     
+ Misses      19540    13255     -6285     
Flag Coverage Δ
examples 43.14% <33.06%> (-0.24%) ⬇️
gpu 58.97% <27.09%> (+37.87%) ⬆️
regression 15.01% <1.99%> (+0.01%) ⬆️

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.

Promotes the tracking helper from examples/hf_ptq to
modelopt.torch.utils.mlflow so other example scripts can record runs the
same way.

The move required decoupling it from hf_ptq: MlflowRunLogger took an
argparse Namespace and read ten PTQ-specific attributes off it, which no
other caller could supply. It now takes a tracking URI, an experiment name
and an explicit enabled flag, with params, tags and artifacts passed in;
default_experiment_name takes (tool, model, variant) rather than
inspecting args. hf_ptq keeps the PTQ-specific parts in two small helpers.

Uploading the recipe moved to the caller as part of that, which also drops
the modelopt.recipe import from the library module -- keeping it would have
risked a modelopt.torch.utils -> modelopt.recipe -> modelopt.torch.quantization
-> modelopt.torch.utils cycle. The captured log now takes its name from
sys.argv[0] instead of hardcoding hf_ptq.

Also uploads the ModelOpt version as version.txt. It stays a tag as well:
the tag is what makes runs filterable, the artifact is what travels with a
downloaded run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2023/

Built to branch gh-pages at 2026-07-29 18:23 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Fixes found reviewing #2023.

The main one: gathering the MLflow inputs re-reads the recipe, and the
previous commit hoisted that call out of the enabled check, so every
hf_ptq run parsed the recipe a second time and printed an extra
"[load_recipe] loading:" line -- even without --mlflow, which the feature
is supposed to leave untouched. The wiring moves into _start_mlflow_run so
the guard is in one place and can be tested; a run without --mlflow is back
to a single load_recipe line.

Also:
- Only suggest "Did you mean https://..." when the URI has no scheme.
  Suggesting https://sqlite:///mlflow.db was nonsense.
- Cap the assembled experiment name, not just each component: four capped
  components could still produce 403 characters against the VARCHAR(256)
  the comment cites.
- Declare the optional mlflow dependency as an extra now that the module
  ships in modelopt/, and point the ImportError at it.
- TeeStream.__getattr__ raises for _stream/_sink instead of recursing when
  they are unset; start() is idempotent; finish() is nested in try/finally
  so a cleanup_distributed failure cannot strand a run in RUNNING.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@cjluo-nv
cjluo-nv marked this pull request as ready for review July 29, 2026 17:20
@cjluo-nv
cjluo-nv requested review from a team as code owners July 29, 2026 17:20
Comment thread examples/hf_ptq/hf_ptq.py Outdated

def _mlflow_run_inputs(args: argparse.Namespace) -> tuple[dict, dict]:
"""Params and start-time artifacts describing this PTQ run."""
params = {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

can this table be auto generated?

Comment thread examples/hf_ptq/README.md
The run is opened *before* the model loads, so a bad URI or a missing token fails within
seconds rather than after a full calibration. These artifacts are uploaded:

| Artifact | Contents |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

can we hide this table by default to make the readme visualization shorter.

Comment thread pyproject.toml Outdated
]

all = ["nvidia-modelopt[onnx,hf,puzzletron,mlflow]"]
dev = ["nvidia-modelopt[all,dev-docs,dev-lint,dev-test]"]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

let's add mlflow to dev as well.

No behavior change; 442 -> 417 lines.

- Hold the mlflow module on the logger after the guarded import instead of
  repeating the lazy import in four methods. Safe because every user of it
  runs after _open_run, which is also what sets _run.
- Track one _log_path instead of a directory plus a separately derived
  name, which was rebuilding the same path in three places.
- Fold the duplicated text-upload loop into _log_texts, so command.txt,
  version.txt and caller-supplied text all take one path.
- Drop Args/Returns docstring sections that restated the signature (D417
  is not enforced here), keeping the parts that carry rationale the code
  does not: why only http(s) is accepted, why texts upload at start, why
  absent files are skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/hf_ptq/hf_ptq.py (1)

1768-1814: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the MLflow status and distributed cleanup consistent on teardown failures.

_start_mlflow_run() is outside the try, so its failure bypasses cleanup_distributed(). Also, if cleanup_distributed() raises after line 1807, finish() still receives "FINISHED" even though the process exits with an error. Route startup through the cleanup guard and set status = "FAILED" before re-raising teardown failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/hf_ptq/hf_ptq.py` around lines 1768 - 1814, Move
_start_mlflow_run(args) inside the existing teardown-protected flow so startup
failures also pass through cleanup_distributed(). In the nested cleanup
handling, set status to "FAILED" before re-raising any cleanup_distributed()
exception, ensuring mlflow_logger.finish receives the failed status when
teardown fails while preserving successful completion behavior.
🧹 Nitpick comments (1)
examples/hf_ptq/requirements.txt (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep MLflow optional for the example environment.

This makes every hf_ptq requirements install pull MLflow despite the lazy import and nvidia-modelopt[mlflow] extra. Remove this baseline requirement and document/install the optional extra only for --mlflow users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/hf_ptq/requirements.txt` at line 4, Remove the baseline mlflow
requirement from the hf_ptq example dependencies, and document or install the
existing nvidia-modelopt[mlflow] optional extra only when users invoke the
--mlflow flow. Preserve the lazy import and keep non-MLflow installations free
of MLflow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/hf_ptq/hf_ptq.py`:
- Around line 1657-1661: Update the --mlflow validation flow around
validate_tracking_uri to reject tracking URIs containing userinfo credentials
such as username or password before assigning the validated URI to args.mlflow.
Raise a ValueError so parser.error reports the invalid option, while preserving
valid credential-free URI handling and directing authentication through
environment-based configuration.

In `@examples/hf_ptq/README.md`:
- Around line 314-317: Update the logs/hf_ptq.log artifact description in the
README to state that it contains Python stdout/stderr captured by the tee, while
explicitly excluding NCCL, CUDA, and other native-library output; keep the
surrounding artifact descriptions unchanged.

In `@modelopt/torch/utils/mlflow.py`:
- Line 30: Remove the # nosec B404 suppression from the subprocess import and
the # nosec B603 B607 suppression from the subprocess invocation around the git
rev-parse logic. Update the implementation as needed so Bandit passes without
inline security suppressions, while preserving the existing fixed, non-shell
command behavior.
- Around line 279-337: Update start()’s exception path after _open_run() to
close any successfully created MLflow run with FAILED status before re-raising
the original exception. Reuse the existing cleanup behavior from finish(),
ensure capture is stopped even if run closure fails, and leave self._run
consistent so a partial startup cannot remain RUNNING.

In `@tests/unit/torch/utils/test_mlflow.py`:
- Around line 79-86: Move the in-function requests imports from fake_mlflow and
test_unreachable_server_fails_before_the_work_starts to the module-level
imports, then continue using that shared module import for monkeypatching
requests.get. No justification comment is needed once the imports are moved.

---

Outside diff comments:
In `@examples/hf_ptq/hf_ptq.py`:
- Around line 1768-1814: Move _start_mlflow_run(args) inside the existing
teardown-protected flow so startup failures also pass through
cleanup_distributed(). In the nested cleanup handling, set status to "FAILED"
before re-raising any cleanup_distributed() exception, ensuring
mlflow_logger.finish receives the failed status when teardown fails while
preserving successful completion behavior.

---

Nitpick comments:
In `@examples/hf_ptq/requirements.txt`:
- Line 4: Remove the baseline mlflow requirement from the hf_ptq example
dependencies, and document or install the existing nvidia-modelopt[mlflow]
optional extra only when users invoke the --mlflow flow. Preserve the lazy
import and keep non-MLflow installations free of MLflow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 43c45989-cdeb-4b8e-8f51-0f87815c28b4

📥 Commits

Reviewing files that changed from the base of the PR and between 87c9f8c and 9862350.

📒 Files selected for processing (9)
  • CHANGELOG.rst
  • examples/hf_ptq/README.md
  • examples/hf_ptq/hf_ptq.py
  • examples/hf_ptq/requirements.txt
  • modelopt/torch/utils/__init__.py
  • modelopt/torch/utils/mlflow.py
  • pyproject.toml
  • tests/examples/hf_ptq/test_hf_ptq_args.py
  • tests/unit/torch/utils/test_mlflow.py

Comment thread examples/hf_ptq/hf_ptq.py
Comment on lines +1657 to +1661
if args.mlflow is not None:
try:
args.mlflow = validate_tracking_uri(args.mlflow)
except ValueError as e:
parser.error(f"--mlflow: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from urllib.parse import urlparse

uri = "https://user:token@mlflow.example.com"
parsed = urlparse(uri)
assert parsed.netloc
assert parsed.username == "user"
assert parsed.password == "token"
print("Credential-bearing URI is accepted by urlparse.")
PY

Repository: NVIDIA/Model-Optimizer

Length of output: 208


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the validation helper and the MLflow code paths.
rg -n "validate_tracking_uri|mlflow|sys\.argv|tracking_uri|set_tracking_uri|start_run|log_params|log_artifact|log_text" examples/hf_ptq/hf_ptq.py

# Show the relevant sections around the validation and logging.
sed -n '1600,1835p' examples/hf_ptq/hf_ptq.py

Repository: NVIDIA/Model-Optimizer

Length of output: 10323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the helper definition if it lives elsewhere in the repo.
rg -n "def validate_tracking_uri|validate_tracking_uri\(" -S .

Repository: NVIDIA/Model-Optimizer

Length of output: 700


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the helper and the MLflow setup/logging block.
FILE=examples/hf_ptq/hf_ptq.py

# Locate the helper definition range.
nl -ba "$FILE" | sed -n '1,220p' | rg -n "validate_tracking_uri|def main|mlflow"

# Print the helper definition and the MLflow setup region.
nl -ba "$FILE" | sed -n '1,260p'
nl -ba "$FILE" | sed -n '1760,1835p'

Repository: NVIDIA/Model-Optimizer

Length of output: 196


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=modelopt/torch/utils/mlflow.py

# Show the validator and any related tests.
sed -n '1,180p' "$FILE"
printf '\n--- tests ---\n'
sed -n '90,150p' tests/unit/torch/utils/test_mlflow.py

Repository: NVIDIA/Model-Optimizer

Length of output: 8851


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the MLflow logger implementation for any logging/serialization of the tracking URI.
sed -n '1,260p' modelopt/torch/utils/mlflow.py

Repository: NVIDIA/Model-Optimizer

Length of output: 10183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,180p' modelopt/torch/utils/mlflow.py
printf '\n--- tests ---\n'
sed -n '90,150p' tests/unit/torch/utils/test_mlflow.py

Repository: NVIDIA/Model-Optimizer

Length of output: 8851


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every use of run_url / tracking_uri in the MLflow helper.
rg -n "run_url|tracking_uri|command.txt|sys\.argv|log_artifact|log_text|texts\[|params\[|artifact" modelopt/torch/utils/mlflow.py

# Print the start/finish methods and any code that writes command or URL artifacts.
sed -n '180,360p' modelopt/torch/utils/mlflow.py

Repository: NVIDIA/Model-Optimizer

Length of output: 9836


Reject credentials in MLflow tracking URIs. https://user:token@host passes validate_tracking_uri(), then gets echoed in [mlflow] run: ... and captured in the uploaded run log. Strip or reject userinfo before enabling --mlflow; use environment-based auth instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/hf_ptq/hf_ptq.py` around lines 1657 - 1661, Update the --mlflow
validation flow around validate_tracking_uri to reject tracking URIs containing
userinfo credentials such as username or password before assigning the validated
URI to args.mlflow. Raise a ValueError so parser.error reports the invalid
option, while preserving valid credential-free URI handling and directing
authentication through environment-based configuration.

Source: Path instructions

Comment thread examples/hf_ptq/README.md Outdated
Comment thread modelopt/torch/utils/mlflow.py Outdated
Comment thread modelopt/torch/utils/mlflow.py Outdated
Comment thread tests/unit/torch/utils/test_mlflow.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modelopt/torch/utils/mlflow.py`:
- Around line 341-346: Update the command artifact construction in the MLflow
logging method around _log_texts to sanitize _command_text() before uploading
command.txt. Redact sensitive CLI option values and credentials embedded in
tracking URI userinfo, while preserving non-sensitive command content and the
existing version.txt/texts behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1d4721de-0bcf-4c84-978a-21e56c25a95b

📥 Commits

Reviewing files that changed from the base of the PR and between 9862350 and 201689b.

📒 Files selected for processing (2)
  • modelopt/torch/utils/mlflow.py
  • tests/unit/torch/utils/test_mlflow.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/torch/utils/test_mlflow.py

Comment thread modelopt/torch/utils/mlflow.py
cjluo-nv and others added 2 commits July 29, 2026 17:48
- Derive the MLflow params from the parsed args instead of listing ten by
  hand, so a new hf_ptq flag is tracked without touching the list and the
  run records the whole configuration rather than a chosen subset. The
  exclusions are the tracking settings themselves, which describe the
  destination rather than the run, plus dist_state, which is an object;
  world_size is pulled out of it explicitly. Param keys are now the
  argparse names, so model becomes pyt_ckpt_path -- consistent with
  command.txt, and a key pastes straight back into a CLI flag. 36 params
  for a default run, against MLflow's batch limit of 100.
- Collapse the artifact table in the README behind <details> to keep the
  page short. MD033 is disabled and <details> is already used elsewhere in
  examples/.
- List mlflow explicitly in the dev extra. It was already reachable via
  all, but stating it survives someone later trimming that list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
Six findings from the automated review.

Read the git sha out of .git instead of shelling out to git rev-parse.
The repo's security guidelines forbid # nosec as a Bandit bypass, so the
suppressions had to go; removing the subprocess call removes the finding
itself rather than arguing for an exception, and needs no codeowner
sign-off. Handles loose refs, packed-refs and a detached HEAD, and still
returns "unknown" outside a checkout.

Close the run when start() fails after start_run() has succeeded. The
except block only tore down the capture, so a network blip during
log_params left the run RUNNING on the server forever -- the caller gets
an exception rather than a logger to call finish() on. This was a real
bug; deriving params from every CLI argument made the window wider.

Mask credentials before anything is uploaded or printed: --*token* style
option values, and user:pass@ embedded in any URI, in command.txt, in the
params and in the printed run URL. A credential-bearing tracking URI still
works -- only its echo is masked. This matters more now that params are
derived from every argument, since a future secret-bearing flag would
otherwise be uploaded with no code change to catch it.

Also: qualify the log artifact as Python stdout/stderr in the README,
since native library output is excluded, and move the requests import in
the tests to module scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/torch/utils/test_mlflow.py`:
- Line 393: Move the _redact_argv import at
tests/unit/torch/utils/test_mlflow.py:393-393 into the module-level MLflow
utility imports, and move the modelopt.torch.utils.mlflow import at
tests/unit/torch/utils/test_mlflow.py:457-459 to module scope so both imports
occur during test collection.
- Around line 375-395: Update _redact_argv so recognized secret options always
mask the immediately following token, including values beginning with “-”,
rather than excluding dash-prefixed values. Add the parameterized test case for
["--hf_token", "-secret"] expecting ["--hf_token", "***"], while preserving
existing option and URI redaction behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ae52dfb2-13d1-4df6-8a9f-5bafd58fac61

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd4867 and cc058cf.

📒 Files selected for processing (3)
  • examples/hf_ptq/README.md
  • modelopt/torch/utils/mlflow.py
  • tests/unit/torch/utils/test_mlflow.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/hf_ptq/README.md
  • modelopt/torch/utils/mlflow.py

Comment thread tests/unit/torch/utils/test_mlflow.py
Comment thread tests/unit/torch/utils/test_mlflow.py Outdated

@meenchen meenchen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Design review (protocol applied)

Problem: PTQ runs today leave no durable record — command, recipe, log and summaries live only in whatever terminal/SLURM file the user kept — so a finished quantization can't be reproduced or compared later.

Alternatives I checked:

  • In-repo: modelopt/torch/utils/logging.py already owns all the stdout/stderr plumbing in this package (capture_io, no_stdout, atomic_print). TeeStream is a fourth, unrelated mechanism for the same surface and would fit better there. The repo also already has an MLflow convention on the eval side (.agents/skills/evaluation/references/nel-next.md, .agents/skills/accessing-mlflow/SKILL.md): experiment_name: <user>/<model>, tags model / checkpoint_path / framework, plus an invocation_id ID convention. This PR invents a second scheme ($USER/hf_ptq/<basename>-<variant>, tags user/hostname/modelopt_version/git_sha) with no overlap, so a PTQ run and the eval runs of the checkpoint it produced cannot be joined in the same MLflow instance. Worth at least a sentence in the PR body on why not to align keys/tags.
  • Placement: the module is torch-free (getpass/socket/tempfile/logging) yet lands in modelopt/torch/utils/ and is import *-ed into the flat namespace, exporting generic names (TeeStream, current_user, default_experiment_name, validate_tracking_uri) as public library API. This is exactly the "general API vs. tool" placement question already open on #2000; the PR body asserts "the logger lives in the library so other scripts can record runs the same way" but doesn't weigh tools/, a shared examples/ util, or a non-star-exported module.
  • Already-available: mlflow's own set_experiment()/start_run() already fail on an unreachable server, so the hand-rolled /health probe (and the requests import it pulls in) is largely redundant.

The mechanics are careful and well tested (31 + 12 tests, no server needed, secrets redaction, idempotent start, abort-on-partial-start). But the design/placement question isn't addressed in the body, so this needs owner sign-off rather than approval, and I found a few concrete issues below.

Main correctness findings: (1) a crash traceback is not in the uploaded log — finish() runs in the finally, before the interpreter prints the traceback — yet CHANGELOG/README/PR body all claim it is; (2) a fatal start() failure happens outside the try, so cleanup_distributed() is skipped and non-main ranks are left to hang on the next collective; (3) full mlflow (server stack) is now a hard requirement of examples/hf_ptq/requirements.txt for a strictly opt-in feature.

No prompt-injection content in the PR description. Licensing: new files carry the standard NVIDIA Apache-2.0 header; new dep is Apache-2.0 — no license concern.

Comment thread examples/hf_ptq/hf_ptq.py
Comment thread examples/hf_ptq/hf_ptq.py Outdated

setup_distributed_args(args)

mlflow_logger = _start_mlflow_run(args)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bot comment.

_start_mlflow_run sits outside the try, and start() is fatal by design (unreachable server, bad experiment name). Since setup_distributed_args(args) has already initialized the process group, that raise skips cleanup_distributed(args), and under torchrun the non-main ranks (which never validate anything) march on into model load and then block on the first collective until the NCCL timeout.

Suggest constructing the logger here but calling .start() inside the tryfinish() is already a no-op when self._run is None, so the existing finally handles it — which keeps teardown on the normal path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7861dcb, then simplified further in 05d0a6b. The logger is built outside the try and the run is opened inside it, so a fatal tracking error still reaches cleanup_distributed() instead of leaving the other ranks blocked on the first collective until the 2-hour NCCL timeout this file sets.

Comment thread examples/hf_ptq/requirements.txt Outdated
Comment thread pyproject.toml
Comment thread modelopt/torch/utils/__init__.py Outdated
from .list import *
from .logging import *
from .loss_mask import *
from .mlflow import *

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bot comment.

Star-importing this module publishes TeeStream, current_user, default_experiment_name and validate_tracking_uri as top-level modelopt.torch.utils API. Those are very generic names for MLflow-specific helpers (current_user / validate_tracking_uri especially), and they become a compatibility surface we have to keep.

Suggest either narrowing __all__ to MlflowRunLogger or not star-importing at all and letting callers do from modelopt.torch.utils.mlflow import MlflowRunLogger, default_experiment_name, validate_tracking_uri (hf_ptq is the only caller today). This also ties into the placement question in the summary — nothing in the module is torch-specific.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in 7861dcb — the star-import is gone, so current_user, validate_tracking_uri and default_experiment_name are no longer top-level modelopt.torch.utils API. hf_ptq imports from modelopt.torch.utils.mlflow directly.

There is precedent for the omission: distributed, memory_monitor and plugins are not star-imported from that __init__ either.

On placement — agreed nothing in the module is torch-specific. I left it under modelopt/torch/utils/ because there is no non-torch modelopt/utils/ package today and creating one felt out of scope here, but happy to move it if you would prefer that.

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

def write(self, data: str) -> int:
"""Write to both the original stream and the sink."""
self._stream.write(data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bot comment.

TeeStream is generic stream plumbing and overlaps in spirit with modelopt/torch/utils/logging.py, which already owns every other sys.stdout manipulation in this package (capture_io, no_stdout, atomic_print). Consider moving it there (and keeping mlflow.py about MLflow) so there's one place to look for stdout redirection semantics — capture_io in particular is the near-neighbour that redirects instead of tees.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in 7861dcbTeeStream now lives in modelopt/torch/utils/logging.py next to capture_io and no_stdout, and is exported from its __all__. mlflow.py imports it from there. Agreed that one place for stdout-redirection semantics is better, and it shortened mlflow.py usefully.

Comment thread modelopt/torch/utils/mlflow.py
Comment thread pyproject.toml Outdated
"mlflow",
]

all = ["nvidia-modelopt[onnx,hf,puzzletron,mlflow]"]

@kevalmorabia97 kevalmorabia97 Jul 29, 2026

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.

lets not make mlflow requirement part of all. It adds a lot of heavy dependencies so best to leave it explicitly opt-in or part of dev

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in 05d0a6ball is back to ["nvidia-modelopt[onnx,hf,puzzletron]"], with a comment recording why mlflow is deliberately excluded.

Also switched the extra itself to mlflow-skinny>=2.9, so even the opt-in install no longer pulls Flask/gunicorn/sqlalchemy/alembic/docker. Verified skinny is sufficient rather than assuming it: installed it alone (no full mlflow present) and ran a real PTQ against our tracking server — all five artifacts uploaded.

@Edwardf0t1 Edwardf0t1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Three inline comments below. Everything else I found was minor cleanup.

Comment thread modelopt/torch/utils/mlflow.py
Comment thread examples/hf_ptq/hf_ptq.py Outdated

setup_distributed_args(args)

mlflow_logger = _start_mlflow_run(args)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cleanup_distributed() is skipped when MLflow startup fails.

This call sits between setup_distributed_args(args) and the try, and it raises by design — ImportError when mlflow is absent, ConnectionError when the server is unreachable. When it does, rank 0 exits with the process group still up, so cleanup_distributed(args) in the finally never runs.

Fail-fast before load_model() is the right call; it just wants to be inside the try. Constructing the logger inert first keeps mlflow_logger bound for the finally:

mlflow_logger = MlflowRunLogger(
    args.mlflow, args.mlflow_experiment,
    run_name=args.mlflow_run_name,
    enabled=bool(args.mlflow) and args.dist_state.is_main,
)
status = "FAILED"
try:
    _start_mlflow_run(mlflow_logger, args)   # still before load_model()
    ...

Minor, same area: files=_mlflow_run_outputs(args) is evaluated inside the finally, so if it ever raised it would replace the in-flight exception. It can't today (--export_path defaults to "exported_model", so Path() can't fail), but computing it before the try costs nothing and removes the hazard.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7861dcb and restructured further in 05d0a6b. main() now wraps the tracked section in a context manager inside the try, so cleanup_distributed() runs whether or not opening the run fails:

try:
    with _mlflow_run(args):
        ...
finally:
    cleanup_distributed(args)

Your secondary point is fixed as a side effect: files=_mlflow_run_outputs(args) used to be evaluated inside the finally, and the artifact paths are now computed before the block — so there is no longer any way for them to replace an in-flight exception.

Comment thread pyproject.toml Outdated
Three bugs, then cleanups.

The uploaded log never contained the crash traceback. finish() runs from
the caller's finally, which is before the interpreter prints the traceback
to a sys.stderr that is no longer teed, so the log stopped at the last line
the script printed -- contradicting the README, the CHANGELOG and the PR
body. finish() now appends the active exception to the sink, and to the
sink only, so the console still shows it exactly once.

start() sat outside main()'s try. It is fatal by design, and the process
group is already up by then, so a bad URI skipped cleanup_distributed and
left the other ranks blocked on the first collective until the two-hour
NCCL timeout. The logger is built outside the try and opened inside it;
finish() is already inert when no run was opened.

_redact_argv left a secret unmasked when the value itself started with "-".
The pending-secret branch now masks unconditionally; an option in that
position would mean no value was passed, which argparse rejects anyway.

Cleanups: mlflow-skinny rather than the full distribution, which pulled
flask, gunicorn, sqlalchemy, alembic, docker and pandas into every hf_ptq
install for an opt-in feature (verified skinny uploads artifacts to an HTTP
tracking server); stop star-importing the module from modelopt.torch.utils,
which published current_user and validate_tracking_uri as top-level API;
move TeeStream to logging.py beside capture_io and no_stdout; emit
model/checkpoint_path tags so a PTQ run and the evaluations of the
checkpoint it produced can be correlated; test import hygiene.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/hf_ptq/hf_ptq.py (1)

1815-1821: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mark distributed-cleanup failures as failed runs.

After Line 1814, an exception from cleanup_distributed() still calls finish("FINISHED"). MLflow records success and skips traceback capture although the process exits with an error.

Proposed fix
     finally:
         try:
             cleanup_distributed(args)
+        except BaseException:
+            status = "FAILED"
+            raise
         finally:
             # Nested so a failure tearing down the process group cannot leave the run
             # stuck in RUNNING on the server.
             mlflow_logger.finish(status, files=_mlflow_run_outputs(args))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/hf_ptq/hf_ptq.py` around lines 1815 - 1821, Update the cleanup flow
around cleanup_distributed() so any exception during distributed cleanup changes
the run status to failed and preserves the exception for traceback capture
before mlflow_logger.finish is called. Keep the nested finally guarantee that
finish executes, while ensuring successful cleanup continues to use the existing
status.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/torch/utils/test_mlflow.py`:
- Around line 461-467: Update test_git_sha_is_read_without_a_subprocess to
actively prevent process execution while calling _git_sha(), such as patching
subprocess invocation APIs to raise and asserting the call still succeeds.
Remove reliance on checking module globals, while preserving the existing SHA
format validation.

---

Outside diff comments:
In `@examples/hf_ptq/hf_ptq.py`:
- Around line 1815-1821: Update the cleanup flow around cleanup_distributed() so
any exception during distributed cleanup changes the run status to failed and
preserves the exception for traceback capture before mlflow_logger.finish is
called. Keep the nested finally guarantee that finish executes, while ensuring
successful cleanup continues to use the existing status.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bdbae99e-76aa-4c4d-82c3-b1014dd89b30

📥 Commits

Reviewing files that changed from the base of the PR and between cc058cf and 7861dcb.

📒 Files selected for processing (7)
  • examples/hf_ptq/hf_ptq.py
  • examples/hf_ptq/requirements.txt
  • modelopt/torch/utils/logging.py
  • modelopt/torch/utils/mlflow.py
  • pyproject.toml
  • tests/examples/hf_ptq/test_hf_ptq_args.py
  • tests/unit/torch/utils/test_mlflow.py

Comment thread tests/unit/torch/utils/test_mlflow.py
warnings.showwarning = original_showwarning


class TeeStream:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This feels like an agent shortcut hack to me. I'd much rather just replace print() in hf_ptq.py with a proper logger. That would give us all of the benefits of logging library over print including the stream redirection you want to do for mlflow.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fair challenge, and I agree logging would be better for hf_ptq on its own merits. Two reasons I would rather not fold it into this PR:

It would not remove the tee. tqdm writes progress bars straight to sys.stderr, and transformers / huggingface_hub both print and log. A logging.FileHandler captures neither. The tee — plus the pre-bound-handler redirect in _redirect_log_handlers — is what got huggingface_hub's rate-limit warnings into the captured log at all; before that they reached the console and vanished. So a logging conversion would sit alongside this, not replace it.

Scope. 34 print() calls in hf_ptq.py, 86 across examples/hf_ptq/, in a PR already around +1400.

Happy to do the conversion as a follow-up PR — it improves hf_ptq independently of tracking. Let me know if you would rather it block this one.

Comment thread modelopt/torch/utils/mlflow.py Outdated
def _command_text() -> str:
"""The invocation, as a copy-pasteable line."""
lines = [shlex.join([sys.executable, *_redact_argv(sys.argv)])]
world_size = int(os.environ.get("WORLD_SIZE", "1"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What about just logging the environment separately from the command?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Decided against this one, on the author's call, and I think the reasoning is worth recording: environment variables are exactly where credentials live — MLFLOW_TRACKING_TOKEN, HF_TOKEN, cloud keys — and this module's own docstring tells users to pass credentials via the environment. Uploading the environment would undermine the advice the module gives.

The WORLD_SIZE / LOCAL_WORLD_SIZE note stays inline in command.txt, where it exists only to explain that the torchrun wrapper is not part of sys.argv and so is not shown in the command above it.

shutil.copy2(local, staged)
self._mlflow.log_artifact(str(staged), artifact_path=directory)

def _start_capture(self) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See earlier logger comment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Replied on the logging.py thread — short version: agreed in principle, but a logging conversion would not remove the need for the tee (tqdm writes directly to sys.stderr, and the HF libraries both print and log), and it is ~86 print() calls across examples/hf_ptq/. Offering it as a follow-up PR.

Comment thread examples/hf_ptq/hf_ptq.py Outdated
Keep mlflow out of `all`. nvidia-modelopt[all] is the documented install,
so the tracking client does not belong there for an opt-in example feature;
`dev` keeps it, listed explicitly. Pin a mlflow-skinny>=2.9 floor.

Fix _git_sha() in git worktrees, where it reported "unknown" every time.
A worktree's .git is a file naming the real git directory, and while HEAD
lives there the ref itself lives in the checkout named by commondir -- two
levels of indirection that `git rev-parse` handled for free before it was
replaced to clear the Bandit finding. The test now asserts a real sha for a
plain checkout, a worktree and a detached HEAD, rather than accepting
"unknown" as it did before and thereby hiding this.

Add MlflowRunLogger.track(), a context manager mirroring mlflow.start_run(),
and use it from hf_ptq. The block's outcome sets the status, which removes
the manual FAILED/FINISHED bookkeeping and the nested try/finally. It also
means the closing artifact paths are computed before the block rather than
inside a finally, where a raise would have replaced the in-flight exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>

@meenchen meenchen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Re-review of the MLflow tracking PR (+1457/-25, 9 files). All of the previously flagged correctness bugs now look genuinely fixed; what's left is a design/placement decision that two codeowners raised and the author deliberately deferred, so this needs a human sign-off rather than an approval.

Verified as addressed (verified against the current diff, not just the replies):

  • Crash traceback now reaches the uploaded log — finish() calls _note_active_exception() (writes traceback.format_exc() into the tee's sink only, before _log_outputs), with two tests (test_failed_run_uploads_the_traceback, test_track_marks_a_raising_block_failed) and a negative test for the FINISHED path.
  • cleanup_distributed() no longer skipped: with _mlflow_run(args): is inside the try, so a fatal start() still unwinds through the finally; files= is computed before the block, so it can't replace an in-flight exception.
  • Partial-start orphan run fixed (_abort_run() ends the run FAILED, tested).
  • Secret redaction: command.txt, params and run_url masked; dash-prefixed secret values now masked; URI userinfo masked; parametrized tests cover all cases.
  • # nosec suppressions gone; _git_sha() reads .git directly and now handles worktrees (gitdir: file + commondir indirection) and detached HEAD, with real tests for all three (the old test that accepted "unknown" is fixed).
  • TeeStream moved into modelopt/torch/utils/logging.py; the mlflow star-import is out of modelopt/torch/utils/__init__.py.
  • Deps: mlflow-skinny>=2.9, removed from all, kept in dev — matches kevalmorabia97/Edwardf0t1.
  • README table collapsed into <details>, log-artifact wording qualified, params table auto-derived from vars(args).
  • Licensing: new files use the canonical 2026 LICENSE_HEADER verbatim; new dep is Apache-2.0. No license concern.

Why this needs an owner look rather than an approve:

  • 💬 chadvoegele asked to replace hf_ptq.py's bare print() with a real logger instead of teeing streams ("feels like an agent shortcut hack"). Author replied that a logging conversion wouldn't remove the tee (tqdm writes straight to sys.stderr; HF libs both print and log) and offered it as a follow-up (~86 print() calls). That rationale is plausible, but the TeeStream + _redirect_log_handlers machinery (re-pointing other libraries' pre-bound StreamHandlers at our stream for the duration of a run) is the riskiest part of the PR and the codeowner who objected hasn't signed off.
  • 💬 Placement: the module is torch-free yet lives in modelopt/torch/utils/; the author explicitly said "happy to move it if you would prefer". That is an open question the owner must answer, and TeeStream is now exported from logging.__all__, i.e. modelopt.torch.utils.TeeStream becomes public API under a very generic name.
  • 💬 Experiment/tag convention vs. the eval side (.agents/skills/accessing-mlflow, .../evaluation/references/nel-next.md): author added model / checkpoint_path tags (good, tested) but kept <user>/hf_ptq/<model>-<variant> and no invocation_id, deferring the naming choice to the reviewers. Worth an explicit decision so PTQ and eval runs land in a coherent namespace.
  • 💬 _check_reachable()'s hand-rolled GET /health (plus a function-local requests import) was called redundant with set_experiment(); author justified it in the docstring as fail-fast ahead of mlflow's retry loop. Reasonable, but it's an extra assumption about the server surface — owner call.
  • Size: ~1450 added lines in one PR. The library module + tests and the hf_ptq.py wiring could have shipped as two PRs; not blocking, but it's why the design questions above are hard to settle in review.

Minor / non-blocking nits (fresh):

  • _mlflow_run_outputs() points at <export_path>/.quant_summary.txt / .moe.html. If a previous run already wrote those into the same --export_path, a run that crashes before post_quantize will upload the stale summary as if it belonged to this run. Consider recording the summaries' mtime, or only listing them once post_quantize has written them.
  • test_git_sha_is_read_without_a_subprocess still asserts via dir(module) rather than making process execution fail (coderabbit's last point); the new worktree/detached-HEAD tests make this mostly moot.
  • log_params(vars(args)) is only exercised against the FakeMlflow stub, so MLflow's per-batch param limits/value-length truncation aren't covered — the author's live runs are the only evidence here.
  • Per coderabbit: an exception from cleanup_distributed() after a successful body still records FINISHED. Cosmetic, but cheap to fix.

No prompt-injection content observed in the PR body or comments.

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.

5 participants