Skip to content

specdec_bench: fix run provenance, add MLflow tracking - #2119

Open
h-guo18 wants to merge 6 commits into
mainfrom
fix/specdec-bench-provenance
Open

specdec_bench: fix run provenance, add MLflow tracking#2119
h-guo18 wants to merge 6 commits into
mainfrom
fix/specdec-bench-provenance

Conversation

@h-guo18

@h-guo18 h-guo18 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix + new feature

Fix what configuration.json records. It is the only record of how a benchmark ran once the job is gone, and three fields were wrong or missing:

  • num_speculative_tokens (new). DFLASH is configured by --block_size and ignores --draft_length, so a block_size=8 run was recorded as draft_length: 3. Resolved by algorithm, matching the engine branches in models/vllm.py; NONE records 0 instead of inheriting a default.
  • draft_huggingface_model_id (new, via env var). draft_model_dir is a local path, so two drafters benchmarked against the same verifier were indistinguishable afterwards. Validated as an <org>/<name> Hub id.
  • Redaction no longer eats engine config. Matching any key containing token masked twelve serving_config knobs, including the engine's own speculation width. Value shape now decides: credentials are strings, these knobs are ints/bools/None. Explicit names (hf_token, api_key, …) still mask anything, hfToken and hf-token included.

Add MLflow tracking. Results otherwise sit in --save_dir until someone uploads them. --mlflow (plus --mlflow_experiment / --mlflow_run_name, mirroring examples/hf_ptq) logs acceptance length, per-category AL and throughput as metrics, the configuration as params, and the result JSONs and log as artifacts. MLFLOW_TRACKING_URI opts in without changing the command.

This also fixes the same redaction bug in MlflowRunLogger, which masked the num_speculative_tokens added above — and max_num_batched_tokens and similar for any caller.

Usage

export DRAFT_HUGGINGFACE_MODEL_ID=org/My-Drafter   # optional

python3 run.py --model_dir ... --speculative_algorithm DFLASH --block_size 8 \
    --mlflow https://<your-mlflow-server>/ ...
{
  "draft_length": 3,
  "block_size": 8,
  "num_speculative_tokens": 8,
  "draft_huggingface_model_id": "org/My-Drafter",
  "serving_config": { "speculative_config": { "num_speculative_tokens": 8 } }
}

The raw flags stay for provenance; num_speculative_tokens is what consumers should read, and the nested one is the engine's own value for cross-checking.

Testing

test_dump_env.py (new) covers each algorithm's width path, NONE, absent attributes and Hub-id validation. test_redaction.py and test_mlflow.py gain engine knobs (including list-valued), credential names under scalar values, and separator-free spellings.

The provenance tests were verified to fail against the pre-fix code. The redaction change was replayed over real configuration.json files from the results bucket — knobs return, hf_token stays masked. MLflow tracking was exercised end-to-end against a live server.

69 unit + 83 example tests pass; pre-commit clean.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ — additive fields and opt-in flags. Consumers reading draft_length keep working but should move to num_speculative_tokens. Already-uploaded files keep their redacted cells until re-uploaded.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A — mlflow is already optional, imported only when tracking is on.
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A — example-level change.
  • Did you get Claude approval on this PR?: ❌ — not yet run.

Additional Information

Found while reconciling results on the SpecDec visualizer: DFLASH rows showed draft_length 3 against a histogram topping out at 9, and rows sharing a verifier could not be told apart. The MLflow half is the other end of the same problem — logged runs let the dashboard read the tracking server instead of requiring an upload per result.

configuration.json recorded the raw argparse namespace, which is
ambiguous about what the engine actually did:

- DFLASH takes --block_size (= draft_length + 1) and leaves
  --draft_length at its default, so a block_size=8 DFLASH run was
  recorded as draft_length=3. Add num_speculative_tokens, resolved from
  whichever flag the algorithm uses, for consumers to read instead of
  re-deriving it from two flags.

- draft_model_dir only records a local path, so two published drafters
  benchmarked against the same verifier are indistinguishable after the
  job is gone. Add draft_huggingface_model_id via env var, mirroring the
  existing HUGGINGFACE_MODEL_ID channel.

Also stop redacting engine config. _SENSITIVE_SUBSTRINGS matches any key
containing 'token', which swallowed num_speculative_tokens,
max_num_batched_tokens, skip_tokenizer_init and ten more knobs in the
serving config — including the engine's own speculation width, the
natural cross-check on the field added above. The existing allowlist
can't keep up because the set grows with every engine release, so
redaction now also considers the value: a credential is a string, while
these knobs are ints, bools and None. hf_token still redacts. Callers
holding only a name (argv scanning) keep the name-only verdict.

Signed-off-by: Hao Guo <haoguo@nvidia.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
@h-guo18
h-guo18 requested review from a team as code owners August 10, 2026 05:20
@copy-pr-bot

copy-pr-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Aug 10, 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

The speculative decoding benchmark now validates Hub model IDs, derives effective speculative-token metadata, applies value-aware redaction, and supports optional MLflow tracking. MLflow records benchmark parameters, tags, metrics, artifacts, and run status.

Changes

Speculative decoding and MLflow updates

Layer / File(s) Summary
Value-aware configuration redaction
examples/specdec_bench/specdec_bench/utils.py, tests/examples/specdec_bench/test_redaction.py
Sensitive-key checks inspect values. Non-secret engine settings remain visible, while credentials are redacted recursively.
Hub model-ID validation
examples/specdec_bench/specdec_bench/utils.py, tests/examples/specdec_bench/test_dump_env.py
Hub model IDs use the <org>/<name> format. Blank values become None. Malformed values are omitted with warnings.
Derived speculative decoding metadata
examples/specdec_bench/specdec_bench/utils.py, tests/examples/specdec_bench/test_dump_env.py
dump_env derives num_speculative_tokens from the algorithm. DFLASH uses block_size, other algorithms use draft_length, and NONE uses 0.
MLflow benchmark tracking
examples/specdec_bench/specdec_bench/mlflow_tracking.py, examples/specdec_bench/run.py
The benchmark resolves MLflow options and runs inside an MLflow context. The context logs parameters, tags, metrics, artifacts, and completion status.
MLflow parameter secret masking
modelopt/torch/utils/mlflow.py, tests/unit/torch/utils/test_mlflow.py
Explicit credential names are always masked. Ambiguous names are masked when their values are string credentials.

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

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Benchmark
  participant Tracking
  participant MLflow
  CLI->>Benchmark: provide benchmark and MLflow options
  Benchmark->>Tracking: resolve tracking configuration
  Benchmark->>Tracking: enter MLflow run context
  Tracking->>MLflow: start tagged run
  Benchmark->>Benchmark: execute speculative decoding benchmark
  Tracking->>MLflow: log parameters, metrics, and artifacts
  Tracking->>MLflow: set FINISHED or FAILED status
Loading

Possibly related PRs

  • NVIDIA/Model-Optimizer#2120: Both changes add optional MLflow tracking and update MLflow secret-redaction behavior, but for different benchmark workloads.

Suggested reviewers: chenhanyu

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.67% 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
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 Changed Python files add no torch.load, allow_pickle=True, hardcoded trust_remote_code=True, eval/exec, or # nosec; no dependency manifests changed.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: fixing benchmark provenance and adding MLflow tracking.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/specdec-bench-provenance

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

@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 `@examples/specdec_bench/specdec_bench/utils.py`:
- Around line 219-240: Update examples/specdec_bench/specdec_bench/utils.py
lines 219-240 so _is_sensitive_key classifies explicit credential names as
sensitive regardless of scalar value, while retaining visibility for engine
configuration knobs; correct the related documentation at lines 35-42. At lines
253-253, apply this classification before _redact_value serializes each field.
At lines 352-357, validate DRAFT_HUGGINGFACE_MODEL_ID as a public model ID and
omit or reject invalid values before writing configuration.json. Add regressions
covering numeric credential values and credential-bearing environment values.
- Around line 307-316: The speculation width recorded by dump_env must match the
runtime value used by vLLM. Update the configuration assignment for
num_speculative_tokens to prefer the runtime serving_config value, or
consistently map block_size without subtracting one, while preserving the
draft_length fallback for other algorithms.
🪄 Autofix

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: 50081128-77cb-4abb-8b1b-6db4fd4dfbf2

📥 Commits

Reviewing files that changed from the base of the PR and between 9b8caf6 and 0509688.

📒 Files selected for processing (3)
  • examples/specdec_bench/specdec_bench/utils.py
  • tests/examples/specdec_bench/test_dump_env.py
  • tests/examples/specdec_bench/test_redaction.py

Comment thread examples/specdec_bench/specdec_bench/utils.py Outdated
Comment thread examples/specdec_bench/specdec_bench/utils.py Outdated
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.73%. Comparing base (9b8caf6) to head (2cc4c34).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2119   +/-   ##
=======================================
  Coverage   78.73%   78.73%           
=======================================
  Files         522      522           
  Lines       60342    60349    +7     
=======================================
+ Hits        47508    47515    +7     
  Misses      12834    12834           
Flag Coverage Δ
unit 55.30% <100.00%> (+<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.

@h-guo18

h-guo18 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

/claude review

Comment thread examples/specdec_bench/specdec_bench/utils.py Outdated
Comment thread examples/specdec_bench/specdec_bench/utils.py Outdated
Comment thread examples/specdec_bench/specdec_bench/utils.py Outdated
Comment thread tests/examples/specdec_bench/test_dump_env.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 — specdec_bench: record effective speculation width and drafter id

Scope: full review (trigger comment carried no scoping instructions). 3 files changed / +173 −3; all three reviewed: examples/specdec_bench/specdec_bench/utils.py, tests/examples/specdec_bench/test_dump_env.py, tests/examples/specdec_bench/test_redaction.py. Also traced the width value through examples/specdec_bench/run.py, specdec_bench/models/vllm.py, and specdec_bench/models/sglang.py.

Findings: CRITICAL 1 · IMPORTANT 2 · SUGGESTION 1

The intent here is right on all three counts — the value-shape redaction rule is a genuinely better design than an enumerated allowlist that can't keep up with engine releases, and both new fields close real provenance gaps. The issues are in the derivation, not the premise.

Most impactful

1. block_size - 1 disagrees with the engine by one (CRITICAL). For --block_size 8, both engine paths receive num_speculative_tokens: 8vllm.py:115 and sglang.py:80, and run.py:361 documents the flag as being num_speculative_tokens. The new derived field records 7. Since this PR un-redacts serving_config specifically so the engine's own value can serve as the cross-check, the two fields that are supposed to confirm each other will systematically differ. The 7/7 output in the PR description isn't reachable from the current code. If the −1 is deliberate — treating the engines' mapping as the real bug — the correction belongs in the engine branches, not in a second independent derivation that can drift from them.

2. The derivation never reads speculative_algorithm (IMPORTANT). --speculative_algorithm NONE runs no speculation (vllm.py:120-121) but inherits --draft_length's default of 3, so every baseline row now claims a phantom width of 3. And block_size wins unconditionally even on vLLM + EAGLE3, where only draft_length reaches the engine. Keying on the algorithm — the resolution the surrounding comment already describes — fixes both and keeps one source of truth with the engine branches.

3. List-valued knobs still redact (IMPORTANT). _NON_SECRET_VALUE_TYPES is scalars only, so encoder_cudagraph_token_budgets — named in the comment and PR body as one of the twelve knobs restored — is still masked under its real list value. The new test parametrizes it as None, the one value for which it passes, so the suite is green on a case the code doesn't handle. Recursing into containers whose leaves are all non-secret scalars covers it, and leaves the existing hf_token: ["a", "b"] assertion true.

Also one non-blocking note: test_absent_by_default reads ambient env and fails for anyone who exports DRAFT_HUGGINGFACE_MODEL_ID, which the PR presents as the normal way to run.

Risk

Moderate, and confined to examples/specdec_bench/ — no modelopt/ code, no mode registration, config schema, or modelopt_state involvement, so there is no checkpoint or export exposure. The backward-compat claim in the PR body holds: all three changes are additive or widen what gets written.

The risk that matters is provenance integrity rather than runtime breakage. configuration.json is the only surviving record of a run and the PR directs consumers to prefer the new field over the raw flags, so an off-by-one there is worse than the ambiguity it replaces — it is wrong with the appearance of authority, on exactly the DFLASH rows that motivated the fix. Findings 1 and 2 should be settled together, since both concern which flag is authoritative for which algorithm; 3 is independent and self-contained. Once the derived width provably matches serving_config.speculative_config.num_speculative_tokens for a DFLASH run, this is ready.

Skipped as covered by CodeRabbit / gated pre-merge: style and formatting, and the security anti-pattern set in .coderabbit.yaml.

num_speculative_tokens: record what the engine receives, not a derived
draft-token count. models/vllm.py passes block_size straight through as
num_speculative_tokens for DFLASH, so block_size=8 must record 8, not 7.
Confirmed against the runs that motivated this PR: their acceptance
histograms top out at 9 = 8 speculative tokens + 1 bonus.

Redaction: a scalar value is no longer taken as proof of safety for
names that are credentials outright — _is_sensitive_key('hf_token', 1234)
returned False and would have persisted a numeric token. Split the
substrings into always-sensitive (hf_token / api_key / access_key /
secret / password), which redact on the name alone, and the ambiguous
rest, where a scalar still rules the field an engine knob.

Hub ids: validate HUGGINGFACE_MODEL_ID and DRAFT_HUGGINGFACE_MODEL_ID
against <org>/<name> before recording them. They are copied verbatim
into a published configuration.json, so a URL carrying an embedded
credential must not reach it through the environment. A malformed value
warns and is omitted rather than raising, since provenance metadata
should not fail a benchmark that already ran.

Signed-off-by: Hao Guo <haoguo@nvidia.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
Three follow-ups from review, each verified against the code paths in
models/vllm.py and against the runs that motivated this PR.

Resolve num_speculative_tokens by algorithm instead of by flag
precedence, matching the engine branches that forward exactly one flag
each. NONE now records 0 rather than inheriting --draft_length's default
of 3 — baselines are the rows most often joined against specdec rows, so
a phantom width there reads as authoritative. --block_size no longer
overrides the width for algorithms that never read it.

Clear containers whose leaves are all non-secret, so a knob holding a
list survives. A sensitive-key hit replaces the whole subtree before
_redact_value can recurse, so encoder_cudagraph_token_budgets — named in
this PR as one of the knobs the fix restores — was still redacted under
its real list value. The test pinned the one scalar (None) for which it
passed; it now uses [256, 512]. A list of strings under a credential
name still redacts.

test_absent_by_default now clears DRAFT_HUGGINGFACE_MODEL_ID rather than
reading the ambient environment, which would fail for anyone who exports
it — the documented way to run a benchmark.

Signed-off-by: Hao Guo <haoguo@nvidia.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.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 `@examples/specdec_bench/specdec_bench/utils.py`:
- Around line 37-40: Update _is_sensitive_key in
examples/specdec_bench/specdec_bench/utils.py at lines 263-265 to normalize
underscores and hyphens before checking _ALWAYS_SENSITIVE_SUBSTRINGS, so
separator-free camelCase variants such as apiKey, accessKey, and hfToken are
unconditionally redacted. Add numeric-value regression coverage for these
camelCase credential names. The _ALWAYS_SENSITIVE_SUBSTRINGS declaration at
lines 37-40 requires no direct change.
- Around line 228-248: Update _hub_model_id and _HUB_MODEL_ID_RE to use
huggingface_hub.utils.validate_repo_id while retaining the explicit <org>/<name>
requirement, rejecting values such as org/foo..bar, org/foo--bar, and
org/foo.git with the existing warning-and-omit behavior. In
tests/examples/specdec_bench/test_dump_env.py lines 97-120, add these values to
the malformed-ID test cases.
🪄 Autofix

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: 953cfb8b-b5d2-4152-b6e2-745730e61634

📥 Commits

Reviewing files that changed from the base of the PR and between 0509688 and 0d62b38.

📒 Files selected for processing (3)
  • examples/specdec_bench/specdec_bench/utils.py
  • tests/examples/specdec_bench/test_dump_env.py
  • tests/examples/specdec_bench/test_redaction.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/examples/specdec_bench/test_redaction.py

Comment thread examples/specdec_bench/specdec_bench/utils.py Outdated
Comment thread examples/specdec_bench/specdec_bench/utils.py
_ALWAYS_SENSITIVE_SUBSTRINGS missed hfToken / apiKey / hf-token, so the
same numeric-credential hole stayed open for those spellings. Match
against the key with separators stripped.

Validate Hub ids with huggingface_hub's validate_repo_id in addition to
the local pattern. Both are needed: the pattern is the security boundary
(it excludes :@?# and whitespace, so a URL with an embedded credential
cannot get through) while validate_repo_id alone would accept a
single-component name; validate_repo_id adds canonicality, rejecting
foo..bar, foo--bar and foo.git. Imported at module scope rather than
guarded — utils.py already imports transformers, which hard-requires
huggingface-hub, so an ImportError branch would be unreachable.

Signed-off-by: Hao Guo <haoguo@nvidia.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

SSH security review

  • Check whether this PR touches SSH
  • Locate SSH in the repo and assess it
  • Check the intersection between SSH and this PR's redaction change

Short answer: this PR contains no SSH code. The diff is 3 files — examples/specdec_bench/specdec_bench/utils.py, tests/examples/specdec_bench/test_dump_env.py, tests/examples/specdec_bench/test_redaction.py. Grepping the diff for ssh|paramiko|scp|rsync|known_hosts|id_rsa|authorized_keys|ProxyJump|sftp returns nothing, and the whole examples/specdec_bench/ tree has no SSH references either. The only subprocess use in the changed file is the pre-existing git rev-parse HEAD at utils.py:153, which is a fixed argv, shell=False, 5s timeout.

So if you were asking about SSH in this PR, there is nothing to review. Two things it might have been about:


1. If you meant the launcher's SSH code (tools/launcher/core.py) — not in this PR, but here's the read

That code (_ControlMasterSession, ControlMasterSSHTunnel, core.py:351-487) is untouched by this branch — last change was 19e0121, before the PR base. Assessing it anyway since you asked:

What's done right:

  • core.py:378 and core.py:447 — fixed ssh argv, shell=False, so the local invocation isn't shell-injectable. The # nosec B603 B607 annotations are accurate rather than papering over something.
  • core.py:407local() uses shlex.split + no shell.
  • core.py:428_inject_controlmaster_rsh runs the injected options through shlex.quote before splicing into the rsync --rsh= string. That's the one spot where quoting actually matters and it's handled.
  • BatchMode=yes (core.py:371) — no interactive password prompt to hijack; auth must come from the pre-authenticated ControlMaster socket.
  • core.py:463TimeoutExpired on the -O check probe fails closed (return False_raise_reauth_required), rather than proceeding on an unverified socket.

Worth a look, in rough order:

  1. StrictHostKeyChecking=accept-new (core.py:372) — trust-on-first-use. It does block MITM on every connection after the first, but the first connection to a new host is accepted unverified, and a stale/absent known_hosts (fresh container, CI runner) makes every connection a first connection. For internal Slurm clusters this is a defensible convenience trade-off; if the cluster's host keys are known, distributing them (or @cert-authority) and using the default ask/yes closes it. Note this only affects the -O check/-O control paths and the rsync --rsh; the multiplexed session itself rides the already-verified master.

  2. MODELOPT_LAUNCHER_SSH_CONTROL_PATH (core.py:439) — the control socket path comes from the environment with no ownership or permission check. Anyone who can set that env var in the launcher's process can point multiplexed commands at a socket they control. This is a low-severity, env-is-trusted issue (whoever sets your env can do worse directly), but an os.stat check that the socket is owned by the current uid and not group/world-writable would be cheap. _has_live_controlmaster already stats the path for existence, so it's a two-line addition.

  3. Remote command construction (core.py:376-377)pre_command and command are concatenated with && and handed to the remote shell unquoted. That's inherent to remote exec and the inputs are the operator's own YAML, not untrusted data — but it does mean a value interpolated from a config file (<<global_vars.X>>) reaches a remote shell verbatim. Fine as long as configs stay operator-authored; it would matter if launcher YAML ever became something a third party could supply.

  4. MODELOPT_LAUNCHER_SSH_RECONNECT_COMMAND (core.py:468) — interpolated into a RuntimeError message inside backticks. It is only displayed, never executed, so this is not injection — just be aware the string is a suggestion the operator may copy-paste, which is a mild social-engineering surface if the env is hostile. Same trust boundary as (2).

Nothing here is a blocker, and none of it is something this PR should be asked to fix.

Fix the host-key TOFU →


2. If the concern is whether this PR's redaction change could leak an SSH credential

I checked this specifically, since the PR does loosen redaction. It doesn't open an SSH hole:

  • The loosening at utils.py:263-299 only un-redacts values whose shape rules out a credential — bool/int/float/None, or containers whose leaves are all of those. SSH key material and identity paths are strings, so they still redact under the ambiguous key substring.
  • A key named ssh_key, private_key, or identity_file matches key in _SENSITIVE_SUBSTRINGS, and with a string value _is_non_secret_value returns False → still ***REDACTED***.
  • identity alone (as in build_slurm_executor(identity=...), core.py:758) would not match any sensitive substring — but that field holds a filesystem path, not key material, and it never reaches dump_env anyway: configuration.json is written from the specdec_bench argparse namespace, which has no SSH fields.
  • The two new fields are env-var-sourced Hub ids, and _hub_model_id (utils.py:234-262) rejects anything containing :@?# or whitespace, so a git+ssh:// or user:tok@host URL in the environment can't be recorded.

One observation rather than a finding: _ALWAYS_SENSITIVE_SUBSTRINGS (utils.py:41) covers hftoken/apikey/accesskey/secret/password but not privatekey or sshkey. Those currently fall through to the value-shape rule, which is correct for every realistic value (always a string). It would only matter if some engine ever emitted a key-ish name with a numeric value — not a live risk, and adding privatekey to the always-list would be a one-word hardening if you want the belt-and-braces.


Bottom line: no SSH-related concerns in this PR, because there is no SSH in it. The launcher's SSH implementation is reasonable — the notable items are the accept-new host-key policy and the unvalidated control-socket path, both in tools/launcher/core.py and both out of scope here. If you'd like, I can open a separate PR for those two.

View job run · branch fix/specdec-bench-provenance

A benchmark's results existed only as JSON under --save_dir until somebody
uploaded them, so the numbers were invisible until a separate publish step
ran. Add --mlflow / --mlflow_experiment / --mlflow_run_name, mirroring
examples/hf_ptq, so a run is visible as soon as it finishes: acceptance
length, per-category AL and throughput as metrics, the configuration as
params, and the result JSONs and run log as artifacts. Experiment defaults
to $USER/specdec_bench/<model>-<algorithm>.

Metrics are read from --save_dir when the block exits rather than passed to
MlflowRunLogger.track upfront, because the benchmark writes them during the
run. A malformed or missing result file logs no metric instead of raising:
tracking must not be why a finished benchmark reports failure.

The drafter is tagged with its full Hub id. The org is what separates two
drafters trained for the same verifier, which is the comparison this
benchmark exists to make, so only a local path is shortened to its leaf.

Also stop MlflowRunLogger masking engine configuration. _SECRET_NAME matches
any param containing 'token', which masked the num_speculative_tokens this
same PR records — and would mask max_num_batched_tokens and similar knobs
for any caller. Redaction now also considers the value: a credential is a
non-trivial string, so an ambiguous name holding an int, float, bool or None
is kept, while an explicit hf_token / api_key / secret name masks whatever
it holds, separators stripped so hfToken and hf-token match too.

Signed-off-by: Hao Guo <haoguo@nvidia.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
@h-guo18
h-guo18 requested a review from a team as a code owner August 10, 2026 11:59
@h-guo18
h-guo18 requested a review from ChenhanYu August 10, 2026 11:59
@h-guo18 h-guo18 changed the title specdec_bench: record effective speculation width and drafter id specdec_bench: fix run provenance and add MLflow tracking Aug 10, 2026
@h-guo18 h-guo18 changed the title specdec_bench: fix run provenance and add MLflow tracking specdec_bench: fix run provenance, add MLflow tracking Aug 10, 2026

@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: 6

🤖 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/specdec_bench/specdec_bench/mlflow_tracking.py`:
- Around line 158-166: Update the acceptance metric mapping in the relevant
tracking function to preserve whether values come from AL or AR: emit
`Average_AL` under the AL key and `Average_AR` under a distinct AR key, and
likewise use separate `category_al/` and `category_ar/` prefixes for
`Category_AL` and `Category_AR`. Avoid fallback logic that relabels AR metrics
as AL.
- Line 157: Normalize the values returned by _first for acceptance_rate.json and
perplexity.json to mappings before any .get() access, treating scalar or
list-derived non-mappings as empty mappings. Apply this in the metric-loading
flow around acceptance and perplexity so malformed payloads cannot raise during
the finally argument evaluation and prevent logger.finish() from running.
- Around line 64-68: Update the CLI help text associated with the --mlflow
option to state that the explicit --mlflow value overrides MLflow's
MLFLOW_TRACKING_URI environment variable, matching the precedence implemented
around the argument handling near line 93. Preserve the existing best-effort
warning behavior for unusable environment-derived URIs.
- Around line 36-40: Add a module-level __all__ declaration in
mlflow_tracking.py listing the three intended public helpers: MlflowRunLogger,
default_experiment_name, and validate_tracking_uri, so imported implementation
symbols are not exposed as part of the module API.
- Line 202: Update the logger.start call in the benchmark flow to pass the
expected files mapping through its files parameter, using the same file
definitions later consumed by finish(). Keep the existing params and
_run_tags(args) arguments unchanged so MlflowRunLogger records each file’s
pre-run state and only uploads artifacts generated by this run.
- Around line 96-102: Update the ValueError handling around
validate_tracking_uri in the MLflow argument validation flow so neither
parser.error nor warnings.warn includes the exception text or supplied URI. Emit
a generic tracking-URI validation message while preserving the
required-versus-untracked control flow and args.mlflow reset behavior.
🪄 Autofix

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: dd423c8a-d936-4c17-a411-21cadc5516be

📥 Commits

Reviewing files that changed from the base of the PR and between d7e5cb2 and ea023a2.

📒 Files selected for processing (4)
  • examples/specdec_bench/run.py
  • examples/specdec_bench/specdec_bench/mlflow_tracking.py
  • modelopt/torch/utils/mlflow.py
  • tests/unit/torch/utils/test_mlflow.py

Comment thread examples/specdec_bench/specdec_bench/mlflow_tracking.py
Comment thread examples/specdec_bench/specdec_bench/mlflow_tracking.py Outdated
Comment thread examples/specdec_bench/specdec_bench/mlflow_tracking.py
Comment thread examples/specdec_bench/specdec_bench/mlflow_tracking.py Outdated
Comment thread examples/specdec_bench/specdec_bench/mlflow_tracking.py Outdated
Comment thread examples/specdec_bench/specdec_bench/mlflow_tracking.py Outdated
@github-actions

github-actions Bot commented Aug 10, 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-2119/

Built to branch gh-pages at 2026-08-10 12:23 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Name the expected outputs to logger.start() as well as finish(). start()
stats each path so finish() can tell an output this run wrote from one a
previous attempt left in --save_dir; without it, a run that produced none
of them uploaded the earlier attempt's results as its own.

_headline_metrics now yields {} for any payload that is not a mapping. A
metric file is written by a separate process that may have died mid-write,
and this runs inside the caller's finally — an AttributeError there would
stop the MLflow run from being closed at all, leaving it RUNNING forever.

Record legacy Average_AR / Category_AR as avg_ar / category_ar rather than
folding them into avg_al. The suffix records how the figure was computed,
so mapping both onto one name would silently mix two definitions.

Drop the URI from the validation failure message. validate_tracking_uri
quotes the value it rejected, which may carry credentials in its userinfo
or query, and this lands in terminals and CI logs; naming the source the
caller set is enough to act on.

Also correct the --mlflow help, which had the precedence backwards, and
declare __all__.

Signed-off-by: Hao Guo <haoguo@nvidia.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
@ChenhanYu

Copy link
Copy Markdown
Collaborator

Can we support MLFLOW_TRACKING_URI to enable tracking? https://mlflow.org/docs/latest/api_reference/python_api/mlflow.environment_variables.html

@h-guo18

h-guo18 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

(AI-generated reply)

Both are already supported.

MLFLOW_TRACKING_URI enables tracking on its own — no flag needed:

export MLFLOW_TRACKING_URI=https://<your-mlflow-server>/
python3 run.py ...   # unchanged

mlflow_tracking.py#L95: args.mlflow or os.environ.get("MLFLOW_TRACKING_URI") or None. --mlflow wins when both are set.

The two differ on failure, matching examples/hf_ptq: an unusable --mlflow is fatal (tracking was explicitly requested), while an unusable MLFLOW_TRACKING_URI warns and the benchmark continues untracked — the variable is often exported for unrelated tooling and should not fail a run that is about to spend GPU hours.

The other variables on that page are read by the mlflow client itself, which this uses directly, so they apply without any work on our side.

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