Skip to content

Use trtexec_safe on safety platforms when using remoteAutoTuning - #1378

Open
dthienan-nv wants to merge 58 commits into
NVIDIA:mainfrom
dthienan-nv:dmoodie/bugfix/trtexec_safe
Open

dthienan-nv wants to merge 58 commits into
NVIDIA:mainfrom
dthienan-nv:dmoodie/bugfix/trtexec_safe

Conversation

@dthienan-nv

@dthienan-nv dthienan-nv commented Apr 30, 2026 •

Copy link
Copy Markdown
Contributor

Previously there was a bug when using TrtExecBenchmark with the safety runtime and remoteAutoTuning that resulted in invalid latency measurements. This was due to using the local trtexec instead of the remote trtexec_safe.
This PR fixes this issue by using scp to push the resulting engine to the remote device and then remotely calling trtexec_safe to generate latency measurements.
This PR changes the behavior when specifying remote auto tuning such that an incorrect configuration will no longer gracefully fallback but rather fail hard and fast so that the user may correct the configuration.

Before:

[modelopt][onnx] - WARNING - Could not parse median latency from trtexec output
[modelopt][onnx] - WARNING - Benchmark failed: /tmp/tmp9s0yckb3/baseline.onnx
[modelopt][onnx] - INFO - Baseline latency: inf ms
[modelopt][onnx] - INFO - Baseline: inf ms

After:

[modelopt][onnx] - INFO - TrtExec benchmark (median): 3.16 ms
[modelopt][onnx] - INFO - Baseline latency: 3.165 ms
[modelopt][onnx] - INFO - Baseline: 3.16 ms

Security Exception

Requesting an exception since we need to call subprocesses to push data to a remote device for remote profiling.

Summary by CodeRabbit

  • Improvements
    • Distinct latency measurement for safe vs standard benchmark modes for more accurate results.
    • Strict parsing and validation of remote autotuning configuration as URL-style input.
    • Automatic transfer of built artifacts and SSH-based remote benchmarking with a fallback execution path.
    • Timeouts for remote operations and clearer error handling that surfaces missing dependencies and yields infinite latency on failures.

@dthienan-nv
dthienan-nv requested a review from a team as a code owner April 30, 2026 16:00
@dthienan-nv
dthienan-nv requested a review from galagam April 30, 2026 16:00
@copy-pr-bot

copy-pr-bot Bot commented Apr 30, 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 Apr 30, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Replaces a single latency regex with separate safe and standard patterns; enforces ssh:// for --remoteAutoTuningConfig and stores SSH credentials/remote paths; SCPs engines and runs remote trtexec_safe with fallback to trtexec --safe; adds subprocess timeouts and returns inf on failures.

Changes

Cohort / File(s) Summary
TensorRT Benchmark Safe Mode & Remote Autotuning
modelopt/onnx/quantization/autotune/benchmark.py
Introduces separate safe_pattern and std_pattern for latency parsing; parses --remoteAutoTuningConfig strictly as ssh:// and extracts/stores host, creds, remote_exec_path, remote_lib_path; when remote+safe, SCPs engine to remote, SSH-runs trtexec_safe and parses with safe_pattern (fallback: SSH trtexec --safe + std_pattern); adds timeouts to subprocess.run and treats failures (nonzero exit, SCP/SSH errors, missing parse) as float("inf"); ImportError during remote setup now re-raised after warning.

Sequence Diagram

sequenceDiagram
    participant Local as Local System
    participant CLI as Arg Parser
    participant Bench as TrtExecBenchmark
    participant Remote as Remote Host
    participant Parser as Latency Parser

    Local->>CLI: Parse args (--remoteAutoTuningConfig, --safe)
    CLI->>Bench: Provide parsed config
    Bench->>Bench: Persist SSH creds, host, port, remote paths

    alt remote autotuning && safe
        Bench->>Bench: Build TensorRT engine
        Bench->>Remote: scp engine -> remote_exec_path (timeout)
        Remote-->>Bench: scp result
        Bench->>Remote: ssh run `LD_LIBRARY_PATH=... trtexec_safe --loadEngine=...` (timeout)
        alt safe stdout parsed
            Remote-->>Bench: stdout
            Bench->>Parser: parse with safe_pattern
        else trtexec_safe fail
            Bench->>Remote: ssh run `trtexec --safe --loadEngine=...` (timeout fallback)
            Remote-->>Bench: stdout
            Bench->>Parser: parse with std_pattern
        end
    else local or non-safe
        Bench->>Bench: run local trtexec (timeout)
        Bench->>Parser: parse with std_pattern
    end

    Parser-->>Bench: latency or parse-failure -> return latency or inf
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.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 No security anti-patterns detected: no unsafe torch.load/numpy.load, no trust_remote_code, no eval/exec on external input, no nosec bypass comments, no new risky dependencies.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: using trtexec_safe on safety platforms during remoteAutoTuning.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modelopt/onnx/quantization/autotune/benchmark.py`:
- Line 263: The code assumes required query params exist and directly indexes
self.remote_options["remote_exec_path"] (and later "remote_lib_path"), which can
raise a KeyError; fix by validating that both "remote_exec_path" and
"remote_lib_path" are present in self.remote_options before using them (e.g., in
the constructor or the method that parses remote options), and raise a clear
ValueError or return a helpful error message if missing; after validation,
continue to compute self.remote_bin_path =
os.path.dirname(str(self.remote_options["remote_exec_path"])) and the remote lib
path usage as before.
- Around line 355-378: The code in benchmark.py (scp_cmd, trtexec_safe_cmd,
subprocess.run calls) has multiple security and correctness issues: remove all
"# nosec" comments, validate self.remote_port (ensure not None and use the
correct ssh/scp port flags), never embed self.remote_password into command
arguments, and stop passing a single shell-interpreted string to SSH
(trtexec_safe_cmd) which risks command injection from self.remote_engine_path,
self.remote_options['remote_lib_path'], and self.remote_bin_path. Fix by using
key-based SSH authentication (or only use sshpass with explicit approver
consent), build scp_cmd and ssh target as "user@host" (no password), pass port
via the correct flag, and pass remote commands/LD_LIBRARY_PATH safely: either
set environment via subprocess.run(env=...) when running locally or construct a
safely-escaped remote command using shlex.quote for each component (or send a
prepared remote script) and invoke subprocess.run with a list of args (not a
single concatenated shell string); also check subprocess.returncode and capture
output/errors for logging. Ensure changes touch scp_cmd, trtexec_safe_cmd,
subprocess.run usages, and validations for
remote_port/remote_user/remote_engine_path.
- Around line 291-293: The info log in the Benchmark (or surrounding
class/method where self._base_cmd is built and logged) prints the full base
command which can leak credentials from arguments like --remoteAutoTuningConfig
(e.g., ssh://user:password@host); before calling self.logger.info, create a
redacted copy of self._base_cmd that masks sensitive values by detecting args
that contain remoteAutoTuningConfig or ssh:// patterns (or args starting with
"--remoteAutoTuningConfig") and replace the password portion (or entire URI)
with a placeholder such as "ssh://user:****@host" or "[REDACTED]"; then log the
redacted command instead of the original to avoid credential exposure.
🪄 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: b8d2a6d0-2a7c-4a27-9841-5b576f1c8ec2

📥 Commits

Reviewing files that changed from the base of the PR and between bb08094 and 19a4961.

📒 Files selected for processing (1)
  • modelopt/onnx/quantization/autotune/benchmark.py

Comment thread modelopt/onnx/quantization/autotune/benchmark.py Outdated
Comment thread modelopt/onnx/quantization/autotune/benchmark.py Outdated
Comment thread modelopt/onnx/quantization/autotune/benchmark.py Outdated
@dthienan-nv
dthienan-nv marked this pull request as draft April 30, 2026 16:49
@dthienan-nv
dthienan-nv marked this pull request as ready for review April 30, 2026 18:31

@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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modelopt/onnx/quantization/autotune/benchmark.py`:
- Around line 293-296: The remote-config cleanup currently only removes the
"--remoteAutoTuningConfig" token from trtexec_args but leaves the following URL
token and doesn't clear the self.has_remote_config flag, so update the cleanup
in the ImportError path to remove both the flag and its following token when
present and set self.has_remote_config = False; specifically, process the
trtexec_args list in the block that handles ImportError to filter out
"--remoteAutoTuningConfig" and the next element (if any) and then set
self.has_remote_config = False so subsequent checks of
self.is_safe/self.has_remote_config behave correctly.
- Around line 376-404: The SCP/SSH subprocess.run calls (scp_cmd and the
trtexec_safe_cmd invocations) lack timeouts and can hang; update each
subprocess.run that executes scp_cmd and trtexec_safe_cmd to pass a configurable
timeout (e.g., self.ssh_timeout or a new attribute like self.remote_timeout with
a sensible default) and handle subprocess.TimeoutExpired by killing the
operation, logging an error via self.logger.error, and returning float("inf")
(same failure path as other errors). Apply this to the three subprocess.run
sites shown (the initial scp_cmd run, the first trtexec_safe_cmd run, and the
fallback trtexec_safe_cmd run), using the timeout kwarg and wrapping calls in
try/except subprocess.TimeoutExpired to ensure deterministic autotune behavior.
- Around line 256-261: The code assigns parsed.username/hostname directly to
self.remote_user and self.remote_ip, which allows None to overwrite the intended
defaults and permits missing hostnames; update the assignment logic in the
constructor/initializer that sets self.remote_user, self.remote_password,
self.remote_ip, and self.remote_port so that parsed.username only replaces the
default (e.g., "root") when not None/empty, parsed.password is handled
similarly, and parsed.hostname is validated—if hostname is missing raise a
ValueError (or similar) with a clear message; keep the fallback for remote_port
to 22 when parsed.port is None.
🪄 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: 05684e65-2b3d-4d9b-aa78-493751fc56b9

📥 Commits

Reviewing files that changed from the base of the PR and between 19a4961 and be947cf7c2232344a8c47b8707f662cbe3648ea7.

📒 Files selected for processing (1)
  • modelopt/onnx/quantization/autotune/benchmark.py

Comment thread modelopt/onnx/quantization/autotune/benchmark.py Outdated
Comment thread modelopt/onnx/quantization/autotune/benchmark.py Outdated
Comment thread modelopt/onnx/quantization/autotune/benchmark.py Outdated

@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.

♻️ Duplicate comments (4)
modelopt/onnx/quantization/autotune/benchmark.py (4)

294-297: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ImportError cleanup leaves remote state inconsistent for split-arg form.

When args are passed as --remoteAutoTuningConfig <url>, this only removes the flag token and leaves the URL behind. self.has_remote_config also remains True, so remote-safe flow can still be entered later.

Suggested fix
-                trtexec_args = [
-                    arg for arg in trtexec_args if "--remoteAutoTuningConfig" not in arg
-                ]
+                filtered_args: list[str] = []
+                skip_next = False
+                for arg in trtexec_args:
+                    if skip_next:
+                        skip_next = False
+                        continue
+                    if arg == "--remoteAutoTuningConfig":
+                        skip_next = True
+                        continue
+                    if arg.startswith("--remoteAutoTuningConfig="):
+                        continue
+                    filtered_args.append(arg)
+                trtexec_args = filtered_args
+                self.has_remote_config = False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 294 - 297, The
removal of "--remoteAutoTuningConfig" only deletes the flag token but leaves the
URL token and doesn't clear self.has_remote_config; update the cleanup for
trtexec_args in the code around trtexec_args and self.is_safe so it removes both
the flag and its following value when the flag appears in split form (e.g.,
iterate through trtexec_args, drop "--remoteAutoTuningConfig" and the next
token), and ensure self.has_remote_config is set to False when no remote config
token remains so the remote-safe flow won't be entered erroneously.

339-341: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

# nosec bypasses on subprocess calls violate repository security policy.

These inline Bandit bypass comments are not allowed by the project policy and should be removed before merge (with explicit safety justification and required reviewer approval handled in PR process if needed).

As per coding guidelines: "Any use of '# nosec' comments to bypass Bandit security checks is not allowed."

Also applies to: 379-381, 395-400, 414-419

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 339 - 341,
Remove the "# nosec" comments on the subprocess.run calls (the one creating
result = subprocess.run(..., timeout=self.remote_timeout_sec) and the other
occurrences at the referenced ranges) and replace the bypass with secure
handling: ensure each call passes a validated list of arguments (no shell=True),
validate or sanitize the cmd input before use, handle subprocess.TimeoutExpired
and CalledProcessError exceptions and log/propagate failures instead of
silencing them, and ensure capture_output/text remain intentional; update the
code around the subprocess.run calls (refer to the result = subprocess.run(...)
invocation and the other similar subprocess.run sites) to implement these safe
patterns.

257-262: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve defaults and validate SSH authority fields before building remote targets.

parsed.username/parsed.hostname can be None; assigning directly here can produce invalid endpoints (e.g., None@None) and hides the intended "root" default user fallback.

Suggested fix
-            self.remote_user = parsed.username
-            self.remote_password = parsed.password
-            self.remote_ip = parsed.hostname
-            self.remote_port = parsed.port
-            if self.remote_port is None:
-                self.remote_port = 22
+            self.remote_user = parsed.username or self.remote_user
+            self.remote_password = parsed.password or ""
+            self.remote_ip = parsed.hostname
+            if not self.remote_ip:
+                raise ValueError(
+                    "Missing hostname in --remoteAutoTuningConfig (expected ssh://user[:pass]@host[:port]?...)."
+                )
+            self.remote_port = parsed.port or 22
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 257 - 262, Do
not blindly overwrite SSH fields with possibly-None parsed values; instead
validate and preserve defaults: only set self.remote_user = parsed.username if
parsed.username is not None (otherwise keep existing default "root"), only set
self.remote_password = parsed.password if parsed.password is not None, only set
self.remote_ip = parsed.hostname if parsed.hostname is not None (and raise or
handle missing hostname), and set self.remote_port = parsed.port if parsed.port
is not None else keep default 22; ensure the code uses these validated
attributes when constructing remote targets so you never get "None@None".

368-370: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid sshpass -p because it exposes the password in process arguments.

Passing secrets via command-line arguments can leak through process listings. Prefer SSH keys, or use sshpass -e with SSHPASS set in a transient env passed to subprocess.run.

As per coding guidelines: "Never hardcode or log credentials/tokens/passwords; ensure error/log messages do not expose secrets."

Does `sshpass -p <password>` expose credentials via process listings, and is `sshpass -e` (SSHPASS env var) the recommended safer alternative?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 368 - 370, The
current code appends "sshpass", "-p", and self.remote_password to ssh_pass (in
the block building the SSH command), which exposes the password via process
arguments; change this to use "sshpass", "-e" and remove the "-p" and
self.remote_password from ssh_pass, then pass the password via a transient
environment variable SSHPASS when invoking subprocess.run/call (add/merge an env
dict with {"SSHPASS": self.remote_password} or use an existing env copy) so the
secret is not visible in argv; also prefer using SSH keys if available and
remove any logging of self.remote_password in functions building or executing
the SSH command (e.g., the code that constructs ssh_pass and the caller that
runs subprocess).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@modelopt/onnx/quantization/autotune/benchmark.py`:
- Around line 294-297: The removal of "--remoteAutoTuningConfig" only deletes
the flag token but leaves the URL token and doesn't clear
self.has_remote_config; update the cleanup for trtexec_args in the code around
trtexec_args and self.is_safe so it removes both the flag and its following
value when the flag appears in split form (e.g., iterate through trtexec_args,
drop "--remoteAutoTuningConfig" and the next token), and ensure
self.has_remote_config is set to False when no remote config token remains so
the remote-safe flow won't be entered erroneously.
- Around line 339-341: Remove the "# nosec" comments on the subprocess.run calls
(the one creating result = subprocess.run(..., timeout=self.remote_timeout_sec)
and the other occurrences at the referenced ranges) and replace the bypass with
secure handling: ensure each call passes a validated list of arguments (no
shell=True), validate or sanitize the cmd input before use, handle
subprocess.TimeoutExpired and CalledProcessError exceptions and log/propagate
failures instead of silencing them, and ensure capture_output/text remain
intentional; update the code around the subprocess.run calls (refer to the
result = subprocess.run(...) invocation and the other similar subprocess.run
sites) to implement these safe patterns.
- Around line 257-262: Do not blindly overwrite SSH fields with possibly-None
parsed values; instead validate and preserve defaults: only set self.remote_user
= parsed.username if parsed.username is not None (otherwise keep existing
default "root"), only set self.remote_password = parsed.password if
parsed.password is not None, only set self.remote_ip = parsed.hostname if
parsed.hostname is not None (and raise or handle missing hostname), and set
self.remote_port = parsed.port if parsed.port is not None else keep default 22;
ensure the code uses these validated attributes when constructing remote targets
so you never get "None@None".
- Around line 368-370: The current code appends "sshpass", "-p", and
self.remote_password to ssh_pass (in the block building the SSH command), which
exposes the password via process arguments; change this to use "sshpass", "-e"
and remove the "-p" and self.remote_password from ssh_pass, then pass the
password via a transient environment variable SSHPASS when invoking
subprocess.run/call (add/merge an env dict with {"SSHPASS":
self.remote_password} or use an existing env copy) so the secret is not visible
in argv; also prefer using SSH keys if available and remove any logging of
self.remote_password in functions building or executing the SSH command (e.g.,
the code that constructs ssh_pass and the caller that runs subprocess).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: cb68d482-2dee-449b-ba0a-2c4b8e306e3a

📥 Commits

Reviewing files that changed from the base of the PR and between be947cf7c2232344a8c47b8707f662cbe3648ea7 and 28e4e8e8438e00c902e9aa2543807acc758b484f.

📒 Files selected for processing (1)
  • modelopt/onnx/quantization/autotune/benchmark.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.

♻️ Duplicate comments (3)
modelopt/onnx/quantization/autotune/benchmark.py (3)

297-299: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redact --remoteAutoTuningConfig before logging command template

At Line 299, logging the full command can leak credentials from SSH URLs in remote autotuning config.

Proposed fix
-        self.logger.debug(f"Base command template: {' '.join(self._base_cmd)}")
+        redacted_cmd: list[str] = []
+        skip_next_remote_cfg_value = False
+        for arg in self._base_cmd:
+            if skip_next_remote_cfg_value:
+                skip_next_remote_cfg_value = False
+                redacted_cmd.append("[REDACTED_REMOTE_CONFIG]")
+                continue
+            if arg == "--remoteAutoTuningConfig":
+                redacted_cmd.append(arg)
+                skip_next_remote_cfg_value = True
+                continue
+            if arg.startswith("--remoteAutoTuningConfig="):
+                redacted_cmd.append("--remoteAutoTuningConfig=[REDACTED_REMOTE_CONFIG]")
+                continue
+            redacted_cmd.append(arg)
+        self.logger.debug(f"Base command template: {' '.join(redacted_cmd)}")

Based on learnings: “Avoid logging sensitive inputs, paths, tokens, proprietary model details, or overly verbose operational information that could leak security-relevant data.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 297 - 299, The
debug log prints the full command template via self._base_cmd (extended with
trtexec_args), which can leak sensitive SSH URLs passed with the
--remoteAutoTuningConfig flag; before calling self.logger.debug, create a
sanitized copy of the command list (do not mutate self._base_cmd) and replace
any argument that startswith "--remoteAutoTuningConfig=" or follows the
"--remoteAutoTuningConfig" token with a redacted placeholder like
"--remoteAutoTuningConfig=<REDACTED>" and then log ' '.join(sanitized_cmd) in
the logger.debug call so the sensitive value is not exposed (keep use of
self._base_cmd, trtexec_args, and self.logger.debug).

257-262: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate SSH authority fields before assigning remote target

On Line 257 and Line 259, parsed.username/parsed.hostname can be None. That can produce invalid targets and fail later with unclear errors.

Proposed fix
-            self.remote_user = parsed.username
-            self.remote_password = parsed.password
-            self.remote_ip = parsed.hostname
-            self.remote_port = parsed.port
-            if self.remote_port is None:
-                self.remote_port = 22
+            self.remote_user = parsed.username or self.remote_user
+            self.remote_password = parsed.password or ""
+            self.remote_ip = parsed.hostname
+            if not self.remote_ip:
+                raise ValueError(
+                    "Missing hostname in --remoteAutoTuningConfig "
+                    "(expected ssh://user[:pass]@host[:port]?...)."
+                )
+            self.remote_port = parsed.port or 22
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 257 - 262,
Validate parsed.username and parsed.hostname before assigning to
self.remote_user and self.remote_ip: if either is None, raise a clear ValueError
(or custom exception) that includes the original target string and explains that
SSH username and host are required; keep the existing fallback for parsed.port
(set to 22 if None) and still assign parsed.password to self.remote_password
(you may allow None for password but document it), and update the assignment
block that sets self.remote_user, self.remote_password, self.remote_ip,
self.remote_port to perform these checks and raise the informative error instead
of silently producing invalid targets.

340-340: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove forbidden # nosec suppressions on subprocess calls

# nosec is still present on Line 340, Line 380, Line 399, and Line 418. This violates the repo’s Python security policy for modelopt/**/*.py.

Proposed fix
-            result = subprocess.run(
-                cmd, capture_output=True, text=True, timeout=self.remote_timeout_sec
-            )  # nosec B603
+            result = subprocess.run(
+                cmd, capture_output=True, text=True, timeout=self.remote_timeout_sec
+            )
...
-                result = subprocess.run(
-                    scp_cmd, capture_output=True, text=True, timeout=self.remote_timeout_sec
-                )  # nosec B603
+                result = subprocess.run(
+                    scp_cmd, capture_output=True, text=True, timeout=self.remote_timeout_sec
+                )
...
-                result = subprocess.run(
-                    trtexec_safe_cmd,
-                    capture_output=True,
-                    text=True,
-                    timeout=self.remote_timeout_sec,
-                )  # nosec B603
+                result = subprocess.run(
+                    trtexec_safe_cmd,
+                    capture_output=True,
+                    text=True,
+                    timeout=self.remote_timeout_sec,
+                )
...
-                    result = subprocess.run(
-                        trtexec_safe_cmd,
-                        capture_output=True,
-                        text=True,
-                        timeout=self.remote_timeout_sec,
-                    )  # nosec B603
+                    result = subprocess.run(
+                        trtexec_safe_cmd,
+                        capture_output=True,
+                        text=True,
+                        timeout=self.remote_timeout_sec,
+                    )

As per coding guidelines: “Any use of '# nosec' comments to bypass Bandit security checks is not allowed.”

Also applies to: 380-380, 399-399, 418-418

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` at line 340, Remove the
forbidden "# nosec" suppressions on subprocess calls and fix the underlying
security issue: locate the subprocess invocations (e.g., calls to subprocess.run
/ subprocess.Popen in benchmark.py) that currently have "# nosec B603", remove
those comments, and make the calls secure by passing arguments as a list (not a
single shell string), ensuring shell=False, sanitizing/validating any dynamic
inputs before use (or using shlex.split on trusted strings), and properly
handling exceptions/return codes; update the relevant call sites (the
subprocess.run/subprocess.Popen usages) to follow these patterns so Bandit
warnings are resolved without silencing them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@modelopt/onnx/quantization/autotune/benchmark.py`:
- Around line 297-299: The debug log prints the full command template via
self._base_cmd (extended with trtexec_args), which can leak sensitive SSH URLs
passed with the --remoteAutoTuningConfig flag; before calling self.logger.debug,
create a sanitized copy of the command list (do not mutate self._base_cmd) and
replace any argument that startswith "--remoteAutoTuningConfig=" or follows the
"--remoteAutoTuningConfig" token with a redacted placeholder like
"--remoteAutoTuningConfig=<REDACTED>" and then log ' '.join(sanitized_cmd) in
the logger.debug call so the sensitive value is not exposed (keep use of
self._base_cmd, trtexec_args, and self.logger.debug).
- Around line 257-262: Validate parsed.username and parsed.hostname before
assigning to self.remote_user and self.remote_ip: if either is None, raise a
clear ValueError (or custom exception) that includes the original target string
and explains that SSH username and host are required; keep the existing fallback
for parsed.port (set to 22 if None) and still assign parsed.password to
self.remote_password (you may allow None for password but document it), and
update the assignment block that sets self.remote_user, self.remote_password,
self.remote_ip, self.remote_port to perform these checks and raise the
informative error instead of silently producing invalid targets.
- Line 340: Remove the forbidden "# nosec" suppressions on subprocess calls and
fix the underlying security issue: locate the subprocess invocations (e.g.,
calls to subprocess.run / subprocess.Popen in benchmark.py) that currently have
"# nosec B603", remove those comments, and make the calls secure by passing
arguments as a list (not a single shell string), ensuring shell=False,
sanitizing/validating any dynamic inputs before use (or using shlex.split on
trusted strings), and properly handling exceptions/return codes; update the
relevant call sites (the subprocess.run/subprocess.Popen usages) to follow these
patterns so Bandit warnings are resolved without silencing them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 25d0d07d-dd26-491d-b62d-0b95541d748f

📥 Commits

Reviewing files that changed from the base of the PR and between 28e4e8e8438e00c902e9aa2543807acc758b484f and 36600920454ad8e5d3f5ca10b15c4e6a3007c055.

📒 Files selected for processing (1)
  • modelopt/onnx/quantization/autotune/benchmark.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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
modelopt/onnx/quantization/autotune/benchmark.py (2)

300-302: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Potential credential exposure in logs

Line 302 logs the full base command including trtexec_args, which may contain --remoteAutoTuningConfig=ssh://user:password@host.... This logs credentials at debug level.

Proposed fix: Redact credentials before logging
         self._base_cmd.extend(trtexec_args)

-        self.logger.debug(f"Base command template: {' '.join(self._base_cmd)}")
+        # Redact credentials from log output
+        redacted_cmd = [
+            re.sub(r"(ssh://[^:]*:)[^@]+(@)", r"\1****\2", arg) for arg in self._base_cmd
+        ]
+        self.logger.debug(f"Base command template: {' '.join(redacted_cmd)}")

As per coding guidelines (SECURITY.md): "Don't log secrets/credentials/tokens/SSH details."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 300 - 302, The
debug log prints the full base command including trtexec_args which may contain
SSH credentials (e.g., --remoteAutoTuningConfig=ssh://user:password@host);
update the logging in the code path after self._base_cmd.extend(trtexec_args) so
you sanitize trtexec_args (or a copy of self._base_cmd) before calling
self.logger.debug. Implement a small redaction step in benchmark.py that detects
and masks credential patterns like ssh://user:password@host or key=value pairs
containing passwords/tokens (e.g., replace the password portion with **** or
remove the value) and then log the sanitized command using the same logger.debug
call instead of the raw command.

341-343: ⚠️ Potential issue | 🔴 Critical

Remove # nosec B603 comments—they violate security guidelines without required justification

The code contains four # nosec B603 comments (lines 343, 383, 402, 421) that bypass Bandit security checks. Per SECURITY.md and coding guidelines, # nosec comments are forbidden unless accompanied by:

  1. An inline comment explaining why the pattern is necessary and why it is safe in this specific context
  2. A PR description requesting review from @NVIDIA/modelopt-setup-codeowners with explicit justification

Currently, neither is present. The subprocess calls themselves are safe (list arguments, no shell=True, proper use of shlex.quote on user-controlled values), so removing the # nosec comments should allow Bandit to pass without lowering security posture.

Fix: Remove # nosec comments from all four subprocess.run calls
             result = subprocess.run(
                 cmd, capture_output=True, text=True, timeout=self.remote_timeout_sec
-            )  # nosec B603
+            )
...
                 result = subprocess.run(
                     scp_cmd, capture_output=True, text=True, timeout=self.remote_timeout_sec
-                )  # nosec B603
+                )
...
                 result = subprocess.run(
                     trtexec_safe_cmd,
                     capture_output=True,
                     text=True,
                     timeout=self.remote_timeout_sec,
-                )  # nosec B603
+                )
...
                     result = subprocess.run(
                         trtexec_safe_cmd,
                         capture_output=True,
                         text=True,
                         timeout=self.remote_timeout_sec,
-                    )  # nosec B603
+                    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 341 - 343,
Remove the forbidden "# nosec B603" annotations from the subprocess.run calls in
this module (the four occurrences that call subprocess.run(...,
timeout=self.remote_timeout_sec) and the other subprocess.run(...) invocations
using list arguments and shlex.quote); leave the subprocess.run call signatures
and argument lists unchanged. Do not add any other code or suppressions; if a
justification is later required, include an inline safety comment plus the PR
review request to `@NVIDIA/modelopt-setup-codeowners` per guidelines.
🧹 Nitpick comments (3)
modelopt/onnx/quantization/autotune/benchmark.py (3)

439-441: 💤 Low value

Consider adding specific timeout handling for better error messages

The generic except Exception at line 439 catches subprocess.TimeoutExpired, but the error message "Benchmark failed: {e}" doesn't clearly indicate a timeout occurred. A specific handler would improve debugging.

Proposed fix
+        except subprocess.TimeoutExpired as e:
+            self.logger.error(f"Benchmark timed out after {self.remote_timeout_sec}s: {e}")
+            return float("inf")
         except Exception as e:
             self.logger.error(f"Benchmark failed: {e}")
             return float("inf")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 439 - 441, The
generic except in the benchmarking routine (the block that currently does except
Exception as e and calls self.logger.error(f"Benchmark failed: {e}") then
returns float("inf")) should be split to handle subprocess.TimeoutExpired
specifically: add an except subprocess.TimeoutExpired as e clause before the
generic except to log a clear timeout message (e.g.,
self.logger.error(f"Benchmark timed out after {timeout}s: {e}")) and return
float("inf"), then keep the broad except Exception as e for other failures;
reference the existing self.logger.error usage and the function/method
containing this try/except to locate where to add the new except.

256-265: 💤 Low value

Validate empty username in addition to None

urlparse("ssh://:password@host") returns username="" which passes the is None check but produces an invalid SSH target. Consider validating that the username is non-empty.

Proposed fix
             self.remote_user = parsed.username
             self.remote_password = parsed.password
             self.remote_ip = parsed.hostname
             self.remote_port = parsed.port
-            if self.remote_user is None:
+            if not self.remote_user:
                 raise ValueError("Unable to parse remote user from --remoteAutoTuningConfig")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` around lines 256 - 265,
After parsing the SSH URL into parsed, tighten validation for parsed fields by
ensuring self.remote_user is non-empty (not None and not an empty string) and
likewise ensure self.remote_ip is a non-empty value; replace the existing "if
self.remote_user is None" and "if self.remote_ip is None" checks with truthy
checks (e.g., if not self.remote_user / if not self.remote_ip) so that cases
like urlparse("ssh://:password@host") are rejected, while leaving the
remote_port defaulting behavior (set to 22 when None) unchanged.

286-286: 💤 Low value

Redundant is_safe assignment

Line 286 sets self.is_safe = True inside the conditional block, but line 299 unconditionally reassigns self.is_safe = "--safe" in trtexec_args immediately after the block. Line 286 is redundant.

Proposed fix
                     self.trtexec_args.append("--safe")
-                    self.is_safe = True

Also applies to: 299-299

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/quantization/autotune/benchmark.py` at line 286, The assignment
self.is_safe = True inside the conditional is redundant because self.is_safe is
immediately overwritten later with self.is_safe = "--safe" in trtexec_args; in
the class/method where trtexec args are prepared (look for the method that
builds trtexec_args and sets self.is_safe in
modelopt/onnx/quantization/autotune/benchmark.py), remove the earlier
self.is_safe = True so the flag is set only once by the final assignment,
leaving any conditional logic that affects trtexec_args intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modelopt/onnx/quantization/autotune/benchmark.py`:
- Around line 292-297: The warning message inside the except ImportError block
in benchmark.py (the "except ImportError as e" handler that currently calls
self.logger.warning(...)) is misleading because it says
"--remoteAutoTuningConfig" will be removed but the code immediately raises the
exception; update the log and error handling in that except block of the
benchmarking logic (reference: the except ImportError as e handler and
self.logger) so the message accurately reflects that remote autotuning is
unsupported and the operation will abort: either change the log to a clear
error/exception-level message (e.g., use self.logger.error or
self.logger.exception) that includes e (the exception details) and then
re-raise, or, if you intend to continue by removing the arg, implement the
actual removal logic for "--remoteAutoTuningConfig" before proceeding; do not
leave the old "Removing --remoteAutoTuningConfig" text while still raising.

---

Duplicate comments:
In `@modelopt/onnx/quantization/autotune/benchmark.py`:
- Around line 300-302: The debug log prints the full base command including
trtexec_args which may contain SSH credentials (e.g.,
--remoteAutoTuningConfig=ssh://user:password@host); update the logging in the
code path after self._base_cmd.extend(trtexec_args) so you sanitize trtexec_args
(or a copy of self._base_cmd) before calling self.logger.debug. Implement a
small redaction step in benchmark.py that detects and masks credential patterns
like ssh://user:password@host or key=value pairs containing passwords/tokens
(e.g., replace the password portion with **** or remove the value) and then log
the sanitized command using the same logger.debug call instead of the raw
command.
- Around line 341-343: Remove the forbidden "# nosec B603" annotations from the
subprocess.run calls in this module (the four occurrences that call
subprocess.run(..., timeout=self.remote_timeout_sec) and the other
subprocess.run(...) invocations using list arguments and shlex.quote); leave the
subprocess.run call signatures and argument lists unchanged. Do not add any
other code or suppressions; if a justification is later required, include an
inline safety comment plus the PR review request to
`@NVIDIA/modelopt-setup-codeowners` per guidelines.

---

Nitpick comments:
In `@modelopt/onnx/quantization/autotune/benchmark.py`:
- Around line 439-441: The generic except in the benchmarking routine (the block
that currently does except Exception as e and calls
self.logger.error(f"Benchmark failed: {e}") then returns float("inf")) should be
split to handle subprocess.TimeoutExpired specifically: add an except
subprocess.TimeoutExpired as e clause before the generic except to log a clear
timeout message (e.g., self.logger.error(f"Benchmark timed out after {timeout}s:
{e}")) and return float("inf"), then keep the broad except Exception as e for
other failures; reference the existing self.logger.error usage and the
function/method containing this try/except to locate where to add the new
except.
- Around line 256-265: After parsing the SSH URL into parsed, tighten validation
for parsed fields by ensuring self.remote_user is non-empty (not None and not an
empty string) and likewise ensure self.remote_ip is a non-empty value; replace
the existing "if self.remote_user is None" and "if self.remote_ip is None"
checks with truthy checks (e.g., if not self.remote_user / if not
self.remote_ip) so that cases like urlparse("ssh://:password@host") are
rejected, while leaving the remote_port defaulting behavior (set to 22 when
None) unchanged.
- Line 286: The assignment self.is_safe = True inside the conditional is
redundant because self.is_safe is immediately overwritten later with
self.is_safe = "--safe" in trtexec_args; in the class/method where trtexec args
are prepared (look for the method that builds trtexec_args and sets self.is_safe
in modelopt/onnx/quantization/autotune/benchmark.py), remove the earlier
self.is_safe = True so the flag is set only once by the final assignment,
leaving any conditional logic that affects trtexec_args intact.
🪄 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: 05a6afb2-73a0-4d73-a098-c67808a0e42a

📥 Commits

Reviewing files that changed from the base of the PR and between 36600920454ad8e5d3f5ca10b15c4e6a3007c055 and 2f75b5ddd96f307bf822ff07b3dd22b45d4966c5.

📒 Files selected for processing (1)
  • modelopt/onnx/quantization/autotune/benchmark.py

Comment thread modelopt/onnx/quantization/autotune/benchmark.py Outdated
@galagam

galagam commented May 6, 2026

Copy link
Copy Markdown
Contributor

@willg-nv @ajrasane Can you take a look please?

@ajrasane ajrasane 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.

Code Review Findings

Critical

1. Existing mocked tests will fail (regression).
tests/gpu/onnx/quantization/autotune/test_benchmark.py:196 and :214 mock subprocess.run to return "[I] Latency: min = ... median = X ms" and assert bench.run(...) == X. The PR replaces latency_pattern with r"\[I\]\s+GPU Compute Time:.*?median\s*=\s*([\d.]+)\s*ms", which doesn't match the "[I] Latency:" mocks. Running the tests against this PR's benchmark.py confirms it:

FAILED tests/gpu/onnx/quantization/autotune/test_benchmark.py::test_trtexec_run_returns_parsed_latency
FAILED tests/gpu/onnx/quantization/autotune/test_benchmark.py::test_trtexec_run_accepts_bytes_input

Fix: update the mock stdout in those two tests to use the new [I] GPU Compute Time: min = ..., max = ..., median = X ms format.

2. No tests cover any of the new logic.
133 lines of new code — URL parsing, query-param validation, port/user/host fallbacks, scp/ssh invocation, primary-vs-fallback remote command selection, two distinct latency regexes — and zero tests added or updated. At minimum, please add unit tests with subprocess.run mocked for:

  • --remoteAutoTuningConfig parsing happy-path (both =URL and URL arg forms).
  • Each ValueError branch in the parser (missing value, non-ssh://, missing user/host, missing query params, duplicate config arg).
  • The safe_pattern regex against a realistic Average over N runs - GPU latency: X ms line.
  • The trtexec_safe → trtexec --safe fallback path (returncode-based selection of latency_pattern).

Important

3. # nosec B603 comments remain unresolved per SECURITY.md.
Per project security guidelines, # nosec is forbidden without @NVIDIA/modelopt-setup-codeowners approval and explicit PR-description justification. CodeRabbit raised this twice; the author replied "Partially addressed" but the four # nosec B603 comments at the new subprocess.run sites are still in benchmark.py (all on the new ssh/scp calls). Either remove them and seek codeowner approval with justification in the PR body, or align with whatever approved pattern the project uses for subprocess invocations.

4. FileNotFoundError handler misattributes the error.
The outer except FileNotFoundError in run() catches errors from any of the new subprocess calls (scp, ssh, sshpass) and logs:

trtexec binary not found: {self.trtexec_path}
Please ensure TensorRT is installed and trtexec path is correct

A missing sshpass or scp (likely on minimal containers) will produce that misleading message. Either differentiate by logging the actual e.filename, or scope the existing handler narrowly to the local trtexec call and add a separate handler for the remote path.

5. self.is_safe is set in two places; the inner assignment is dead.
self.is_safe = True inside the if "--safe" not in trtexec_args: block is followed by an unconditional self.is_safe = "--safe" in trtexec_args after the block (and the inner self.trtexec_args.append("--safe") mutates the same list trtexec_args aliases, so the outer assignment ends up True anyway). The inner one isn't a bug but is confusing — please drop it so there's a single source of truth.

6. Multi-valued query params silently corrupt remote paths.

self.remote_options = {
    k: v[0] if len(v) == 1 else v for k, v in parse_qs(parsed.query).items()
}

If remote_exec_path appears more than once in the URL (?remote_exec_path=a&remote_exec_path=b), self.remote_options["remote_exec_path"] becomes a list, and os.path.dirname(str([...])) produces nonsense like ''. Either reject duplicates (raise ValueError), or always take v[-1]/v[0].

7. Fixed remote engine filename → no concurrency safety.
self.remote_engine_path = "trtexec_benchmark_model.trt" is hardcoded and lands in the remote user's home dir. Two autotune runs against the same target will clobber each other's engines mid-benchmark. Suggest deriving from a tempfile-style suffix (PID + uuid) or the local self.engine_path basename.

8. --useCudaGraph is silently injected into the remote command.
The remote trtexec_safe and trtexec --safe invocations always append --useCudaGraph, regardless of what the user passed in trtexec_args. This was added in commit 110f3d7f ("more stable when using cudaGraphs"). Worth either documenting the override on --remoteAutoTuningConfig or making it conditional on user opt-in — silently flipping a kernel-launch mode for latency measurement is surprising.

Suggestions

9. Module-level regex constants should be private.
safe_pattern/std_pattern are exported by virtue of being module-level. They're internal implementation. _SAFE_LATENCY_PATTERN / _STD_LATENCY_PATTERN would scope and signal intent.

10. Stale comment on warning text.
The except ImportError warning was trimmed in commit 2512076 after CodeRabbit flagged it — please double-check the final string still makes sense in isolation; it now reads as a bare statement rather than an actionable warning.

11. Document that sshpass is now an implicit runtime dependency.
If --remoteAutoTuningConfig includes a password, sshpass must be installed on the host running ModelOpt. That's not in any docstring or README; users will discover it via the misleading "trtexec binary not found" message above.

12. Consider redacting credentials from remote-config error messages.
The ValueError raises that include arg or config_arg_value substrings can echo a ssh://user:pass@host back at the user. Most are caught early enough that the password isn't yet parsed, but f"Malformed --remoteAutoTuningConfig argument: {arg}" could echo a password. Strip the password before formatting.

Positive

  • Good iteration on review feedback: query-param validation, shlex.quote on every interpolated remote-side token, explicit ValueError on missing user/host, urlparse-based URL parsing rather than regex.
  • Switching from the rolled-up [I] Latency: line to [I] GPU Compute Time: is the right call — Latency: includes H2D/D2H copies, which is noisy for autotune comparisons.
  • ImportError is now re-raised (was previously swallowed and silently stripped from args), so a TRT < 10.15 environment fails fast.
  • Hard fail on multiple --remoteAutoTuningConfig args closes off an ambiguous configuration.

Test Coverage Assessment

  • Unit-test coverage for new code: 0.
  • Existing tests: 2 mocked tests will regress (see Critical #1).
  • No GPU-CI run posted on the PR (vetting still pending).
  • No regression test for the original bug — a mock that returns the [I] GPU Compute Time: format (or simulates the remote SSH path) would lock the fix in place.

Final Verdict

Request Changes.

Blocking items: the test regression (Critical #1), the missing tests for new functionality (Critical #2), and the unresolved # nosec situation (Important #3). The remote-execution logic itself is reasonable, and the iteration history shows the author has been responsive to security feedback — but the PR can't land in its current state because it breaks pre-existing tests.

@dthienan-nv

dthienan-nv commented May 13, 2026 •

Copy link
Copy Markdown
Contributor Author

I've addressed all comments. The following remain unresolved:

3. # nosec B603 comments remain unresolved per SECURITY.md. -> This still requires an exception.

7. Fixed remote engine filename → no concurrency safety. -> Added a note, should not use a remote device's GPU concurrently anyways since that would result in inaccurate timing.

@ajrasane ajrasane 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.

Review

Fixes a real bug (safety-runtime + remoteAutoTuning returning inf latencies) and the refactor into testable helpers (_parse_remote_autotuning_url, _RemoteAutotuningConfig, _redact_url_password, etc.) is clean. New unit tests in test_trtexec_benchmark.py (728 lines) and test_cli_pipeline.py (211 lines) close the prior "no tests for new logic" gap. A few items below are blocking; the rest are quick-fix follow-ups.

Critical

  1. All three remote subprocess.run calls have no timeout= — modelopt/onnx/quantization/autotune/benchmark.py lines 430 (scp), 453 (trtexec_safe ssh), and 471 (fallback ssh). A hung SSH (network blip, host-key prompt because StrictHostKeyChecking isn't set, keyboard-interactive auth fallback) will block the entire autotuning workflow forever. CodeRabbit flagged this and marked it resolved against commit 28e4e8e, but it is not in HEAD — please re-add timeout= on all three calls and a subprocess.TimeoutExpired handler that logs and returns float("inf").

  2. remote_user / remote_ip not validated against leading - (argv smuggling). The scp destination is built as f"{self.remote_user}@{self.remote_ip}:..." and remote_user / remote_ip come from urlparse. urlparse does not reject usernames or hostnames that look like options. A URL like ssh://-oProxyCommand=evil@host/... lets a crafted username be reinterpreted as an ssh/scp flag (CVE-2017-1000117 class). Please reject remote_user/remote_ip that start with - in _parse_remote_autotuning_url.

Important

  1. Fallback path uses _STD_PATTERN but ran trtexec --safe (line 475). The --safe invocation of trtexec typically emits the Average over N runs - GPU latency: line (i.e., _SAFE_PATTERN), not GPU Compute Time: ... median = .... If the safety-proxy target emits the SAFE format, parsing silently returns inf. Either try both patterns in the fallback, or switch the fallback to _SAFE_PATTERN.

  2. stderr is dropped in the consolidated failure log (lines 478–480): f\"Failed to run trtexec_safe or trtexec with '--safe'\n {result.stdout}\". SSH/sshpass auth failures, host-key prompts, and connection-refused errors are emitted on stderr. Include result.stderr so users can diagnose connection failures.

  3. --useCudaGraph is silently injected into the remote command (lines 445, 467) regardless of the user's trtexec_args. Document this behavior, and consider gating it (only add if not already present).

  4. No cleanup of the remote engine file. remote_engine_path = \"trtexec_benchmark_model.trt\" is scp'd every run and never removed; repeated sessions accumulate stale files on the target. Add an ssh ... rm -f <shlex.quote(remote_engine_path)> in a finally.

  5. Hardcoded remote_engine_path. Concurrency is disallowed by design, but unrelated users on the same target box can still clash. Suggest f\"trtexec_benchmark_model_{os.getpid()}_{uuid.uuid4().hex}.trt\".

  6. self.remote_options is populated but never read in run(). Either remove it or document why it's kept on the instance.

  7. Backward compatibility of _STD_PATTERN. It now matches [I] GPU Compute Time: ... median = X ms instead of [I] Latency: ... median = X ms. The remote path is gated at >=10.15, but the local std path has no version guard — older trtexec may still emit Latency:. Either try both patterns or pin the minimum TensorRT version.

Suggestions

  • Add -oStrictHostKeyChecking=accept-new to the ssh/scp invocations so first-connect doesn't hang (becomes critical without timeouts).
  • _ensure_remote_autotuning_flags appends --safe if missing, which means self.is_safe ends up coupled to "remote autotuning is configured". Worth a docstring note.
  • Docstring typo in docs/source/guides/9_autotune.rst: "Only once instance" → "Only one instance".
  • ["-P", str(self.remote_port)] would be more consistent with the ssh invocation right below it than f\"-P{self.remote_port}\".

CI

  • unit-pr-required-check and check-dco / wait are failing on HEAD — must be green before merge.

Verdict

Request Changes. Primary blockers: missing timeout= on all three remote subprocess.run calls, and argv-smuggling validation on remote_user/remote_ip. The fallback latency-pattern mismatch is also worth fixing in the same pass.

… push engine to the remote device for correct latency testing

Signed-off-by: dmoodie <dmoodie@nvidia.com>
@dthienan-nv
dthienan-nv force-pushed the dmoodie/bugfix/trtexec_safe branch from fff917e to 745bc49 Compare May 14, 2026 19:31
Signed-off-by: dmoodie <dmoodie@nvidia.com>
Signed-off-by: dmoodie <dmoodie@nvidia.com>
Signed-off-by: dmoodie <dmoodie@nvidia.com>
Signed-off-by: dmoodie <dmoodie@nvidia.com>
Signed-off-by: dmoodie <dmoodie@nvidia.com>
…Model-Optimizer into dmoodie/bugfix/trtexec_safe
…t on completion

Signed-off-by: dmoodie <dmoodie@nvidia.com>
Signed-off-by: dmoodie <dmoodie@nvidia.com>
… build

Signed-off-by: dmoodie <dmoodie@nvidia.com>
@dthienan-nv

Copy link
Copy Markdown
Contributor Author

All three remote subprocess.run calls have no timeout=

  • This has been addressed for the remote calls.

remote_user / remote_ip not validated against leading - (argv smuggling).

  • Addressed

Fallback path uses _STD_PATTERN but ran trtexec --safe

  • The pattern is dependent on the executable, not the runtime.

stderr is dropped in the consolidated failure log

  • Addressed

--useCudaGraph is silently injected into the remote command

  • This is documented in 9_autotune.rst

No cleanup of the remote engine file.

  • Addressed

Hardcoded remote_engine_path

  • Concurrency shouldn't be allowed across users either.

self.remote_options is populated but never read

  • Removed

Backward compatibility of _STD_PATTERN

  • False positive, GPU compute time is available in older builds.

Docstring typo ...

  • Addressed

Add -oStrictHostKeyChecking=accept-new

  • Addressed

…w wasn't available before, this is NOT a breaking change

Signed-off-by: dmoodie <dmoodie@nvidia.com>
@kevalmorabia97

Copy link
Copy Markdown
Collaborator

/claude review

Comment on lines +621 to +626
# trtexec_safe / trtexec --safe emit "GPU Compute Time"; local trtexec emits "Latency"
_latency_pattern = (
r"\[I\]\s+GPU Compute Time:.*?median\s*=\s*([\d.]+)\s*ms"
if self._remote_use_trtexec_safe
else r"\[I\]\s+Latency:.*?median\s*=\s*([\d.]+)\s*ms"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL Algorithm] The pattern is selected from _remote_use_trtexec_safe, but it must be selected from has_remote_config — the fallback branch parses the wrong line and re-introduces the bug this PR fixes.

What's wrong: the comment on line 621 states the invariant correctly — "trtexec_safe / trtexec --safe emit GPU Compute Time; local trtexec emits Latency". The condition contradicts it. When the probe finds no trtexec_safe, _remote_use_trtexec_safe is False, the remote command becomes trtexec --safe … (line 571-572), and this expression then picks \[I\]\s+Latency: and applies it to that remote output.

Why it matters: on any remote host without a trtexec_safe binary — i.e. the entire documented fallback path, and the one the CHANGELOG advertises ("falling back to trtexec --safe otherwise") — the regex never matches, so run() returns inf for every candidate. Autotune then sees inf everywhere and degenerates to the exact failure mode in the PR description (Baseline latency: inf ms). The binary_name expression at line 611-615 keys off has_remote_config for the same distinction, which shows the intent.

It also makes two of the new unit tests red — test_probe_absent_uses_trtexec_safe_flag (test_trtexec_benchmark.py:757) and test_remote_run_uses_safe_flag_when_probe_absent (:835) both set/produce _remote_use_trtexec_safe = False, feed [I] GPU Compute Time: median = …, and assert a parsed latency (3.5 and 5.55). With this condition both get inf. I could not execute pytest in this environment, so that part is from reading the code, but the regex has no Latency: substring to match against those mocks.

Fix:

Suggested change
# trtexec_safe / trtexec --safe emit "GPU Compute Time"; local trtexec emits "Latency"
_latency_pattern = (
r"\[I\]\s+GPU Compute Time:.*?median\s*=\s*([\d.]+)\s*ms"
if self._remote_use_trtexec_safe
else r"\[I\]\s+Latency:.*?median\s*=\s*([\d.]+)\s*ms"
)
# trtexec_safe / trtexec --safe emit "GPU Compute Time"; local trtexec emits "Latency"
_latency_pattern = (
r"\[I\]\s+GPU Compute Time:.*?median\s*=\s*([\d.]+)\s*ms"
if self.has_remote_config
else r"\[I\]\s+Latency:.*?median\s*=\s*([\d.]+)\s*ms"
)

mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "[I] Latency: min = 2.50 ms, max = 4.00 ms, median = 3.14 ms"
mock_result.stdout = "[I] GPU Compute Time: min = 2.50 ms, max = 4.00 ms, median = 3.14 ms"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL Algorithm] This mock (and the identical one at line 214) now feeds a string the local code path cannot parse, so both tests fail.

The trtexec_bench fixture (line 61-66) is constructed with no trtexec_args, so has_remote_config is False and _remote_use_trtexec_safe stays None. run() therefore selects the local pattern r"\[I\]\s+Latency:.*?median\s*=\s*([\d.]+)\s*ms" (benchmark.py:625), which has nothing to match in "[I] GPU Compute Time: …" → re.search returns None → run() returns inf, and the pytest.approx(3.14) / pytest.approx(5.0) assertions fail.

An earlier review round flagged these two mocks as stale against a version of the code that used GPU Compute Time unconditionally; the current code correctly keeps Latency for the local trtexec path, so the mocks should go back to the original strings. Both of these are local-benchmark tests — remote coverage now lives in tests/unit/onnx/quantization/autotune/test_trtexec_benchmark.py.

Suggested change
mock_result.stdout = "[I] GPU Compute Time: min = 2.50 ms, max = 4.00 ms, median = 3.14 ms"
mock_result.stdout = "[I] Latency: min = 2.50 ms, max = 4.00 ms, median = 3.14 ms"

mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "[I] Latency: min = 4.00 ms, max = 6.00 ms, median = 5.00 ms"
mock_result.stdout = "[I] GPU Compute Time: min = 4.00 ms, max = 6.00 ms, median = 5.00 ms"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL Algorithm] Same as line 196 — trtexec_bench is a local (non-remote) benchmark, so run() matches [I] Latency:, this mock yields inf, and pytest.approx(5.0) fails.

Suggested change
mock_result.stdout = "[I] GPU Compute Time: min = 4.00 ms, max = 6.00 ms, median = 5.00 ms"
mock_result.stdout = "[I] Latency: min = 4.00 ms, max = 6.00 ms, median = 5.00 ms"

Comment on lines +540 to +547
scp_cmd = [
"scp",
"-P",
str(self.remote_port),
"-oStrictHostKeyChecking=accept-new",
self.engine_path,
f"{self.remote_user}@{self.remote_ip}:{shlex.quote(self.remote_engine_path)}",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[IMPORTANT Performance] Neither scp/ssh invocation sets -oBatchMode=yes, so an auth failure blocks for the full network_timeout_seconds (default 10 minutes) — per candidate.

The docs added in this PR now require key-based auth, and _parse_remote_autotuning_url deliberately drops parsed.password. So a user who passes ssh://user:pass@host (the form the old code supported, and the form _redact_url_password still exists to scrub) gets no key auth configured. subprocess.run inherits the parent's stdin, so ssh falls through to an interactive password prompt and hangs until the timeout expires. With one autotune run evaluating dozens of candidates that is hours of wall-clock producing nothing but inf.

Adding -oBatchMode=yes to all three commands (scp at line 540, the run at line 573, the probe at _probe_remote_trtexec_safe) makes a misconfigured host fail in seconds with a clear stderr instead of stalling, which matches this PR's stated goal of failing fast on bad configuration:

scp_cmd = [
    "scp",
    "-P",
    str(self.remote_port),
    "-oStrictHostKeyChecking=accept-new",
    "-oBatchMode=yes",
    self.engine_path,
    f"{self.remote_user}@{self.remote_ip}:{shlex.quote(self.remote_engine_path)}",
]

Comment on lines +253 to +254
* TensorRT 10.15 or later — passing ``--remoteAutoTuningConfig`` on an older build raises ``ImportError`` immediately rather than falling back to local benchmarking.
* Valid remote autotuning configuration — a malformed or missing ``--remoteAutoTuningConfig`` URL raises ``ValueError`` with a descriptive message.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] These two documented contracts don't hold for any of the public entry points — the exceptions are swallowed one frame up.

TrtExecBenchmark.__init__ does raise ImportError / ValueError now, but every caller goes through init_benchmark_instance (autotune/workflows.py), which wraps construction in a blanket handler:

    except Exception as e:
        logger.error(f"TensorRT initialization failed: {e}", exc_info=True)
        return None

So what users actually observe is:

  • modelopt.onnx.quantization.quantize(...) → RuntimeError("Failed to initialize TensorRT benchmark") (quantize.py:372), not ValueError/ImportError.
  • python -m modelopt.onnx.quantization.autotune → logger.error("Failed to initialize TensorRT benchmark") and exit code 1 (autotune/__main__.py:116-118).

The descriptive message does reach the log via exc_info=True, so it isn't invisible — but "raises ImportError immediately" / "raises ValueError with a descriptive message" is not the API behaviour, and a caller writing except ValueError: around quantize() to detect a bad URL will never catch it. This is also the mechanism behind the PR description's "no longer gracefully fallback but rather fail hard and fast": the hard failure is downgraded to a generic RuntimeError/exit-1 before it reaches the user's except.

Pick one and make doc and code agree — either let these two exception types escape init_benchmark_instance (narrow the except Exception so ValueError/ImportError propagate), or reword these bullets to describe the RuntimeError / exit-1 behaviour. If you go the propagate route, note that none of the new tests cover it — test_cli_pipeline.py mocks init_benchmark_instance out entirely — so a test asserting pytest.raises(ValueError) through init_benchmark_instance would be worth adding.

Comment on lines +477 to +482
except Exception as e:
self.logger.warning(
f"Remote binary probe failed ({e}); "
"will use 'trtexec --safe' for all candidates in this run"
)
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] The probe conflates "trtexec_safe is genuinely absent" with "the probe could not run", and caches the latter for the whole autotune run.

Any ssh transport failure — host unreachable, key rejected, ssh not on PATH (FileNotFoundError), timeout — lands in this except Exception and returns False, which is then cached in _remote_use_trtexec_safe and never re-evaluated. The run continues, every candidate goes down the trtexec --safe branch, and each one fails at the scp/ssh step and returns inf. The user gets one WARNING at the top and then a wall of inf latencies with no indication the host was never reachable.

Since the probe's transport is identical to the one the benchmark itself needs, a transport failure here reliably predicts that the whole run is doomed — distinguishing the two cases would let it fail fast per this PR's stated intent:

        try:
            result = subprocess.run(
                probe_cmd, capture_output=True, text=True, timeout=self.network_timeout_seconds
            )  # nosec B603 — list-form, no shell=True; user/host validated against leading -
        except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
            raise RuntimeError(f"Cannot reach remote host for autotuning: {e}") from e
        if result.returncode == 0:
            self.logger.debug(f"trtexec_safe found at {safe_binary!r} on remote host")
            return True
        self.logger.warning(
            f"trtexec_safe not found at {safe_binary!r} on remote host "
            f"(probe exit {result.returncode}); "
            "will use 'trtexec --safe' for all candidates in this run"
        )
        return False

Related, on the same method: if remote_exec_path has no directory component, remote_bin_path is "", so safe_binary is the bare relative name trtexec_safe and test -x trtexec_safe resolves against the remote home directory rather than PATH — it always reports absent. The fallback trt_path (line 571) becomes bare trtexec, which does resolve via PATH inside the ssh command, so the two paths disagree for that input. Either reject a remote_exec_path without a directory in _parse_remote_autotuning_url, or probe with command -v when remote_bin_path is empty.

self.logger.error(f"stderr: {_redact_url_password(result.stderr)}")
return float("inf")
if self.has_remote_config:
# need to push the model to the device and use trtexec_safe to run

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] Stale comment: what gets pushed is the built engine (self.engine_path, line 545), not the model. The local trtexec call above already consumed the ONNX and wrote the engine under --skipInference. Suggest # push the built engine to the device and measure it with trtexec_safe.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review — 1 blocking correctness bug (with a red test suite behind it)

Scope: trigger comment was a bare /claude review, so full scope. 11 files changed (+2025/-55). Reviewed all 5 modelopt/ files, docs/source/guides/9_autotune.rst, CHANGELOG.rst, and the four test files. I could not execute pytest in this environment, so the test-failure claims below are from reading the code paths, not from a run.

Findings: CRITICAL 3, IMPORTANT 2, SUGGESTION 2

The one that matters

benchmark.py:621-626 selects the latency regex from self._remote_use_trtexec_safe instead of self.has_remote_config. The comment directly above it states the correct invariant — trtexec_safe / trtexec --safe emit GPU Compute Time; local trtexec emits Latency — and the condition contradicts it. When the probe finds no trtexec_safe, the remote command becomes trtexec --safe … but the parser looks for [I] Latency:, never matches, and run() returns inf for every candidate.

That is the entire documented fallback path — the one the CHANGELOG advertises as falling back to trtexec --safe otherwise — reproducing the exact Baseline latency: inf ms failure this PR sets out to fix. The binary_name expression 10 lines up (:611-615) already keys off has_remote_config for the same distinction, so the intent is clear; it is a one-word fix.

It also leaves four tests red, in both directions:

  • tests/unit/.../test_trtexec_benchmark.py:757 and :835 set _remote_use_trtexec_safe = False, feed [I] GPU Compute Time: median = …, and assert 3.5 / 5.55 — they get inf.
  • tests/gpu/.../test_benchmark.py:196 and :214 had their mocks switched to GPU Compute Time, but the trtexec_bench fixture has no remote config, so those go through the local Latency pattern — they also get inf. Those two mocks should go back to [I] Latency:; a prior review round flagged them against an earlier revision that used GPU Compute Time unconditionally, and the source has since (correctly) kept Latency for the local path.

Also worth fixing before merge

  • Docs promise exceptions the code swallows (9_autotune.rst:253-254). TrtExecBenchmark.__init__ raises ImportError/ValueError as documented, but every caller routes through init_benchmark_instance, whose blanket except Exception: return None converts them to RuntimeError('Failed to initialize TensorRT benchmark') for quantize() and to exit-1 for the CLI. The descriptive text still reaches the log via exc_info=True, so it is not invisible — but the PR's headline fail hard and fast so the user may correct the configuration does not survive that frame, and except ValueError around quantize() will never fire. Nothing in the new tests covers this path (test_cli_pipeline.py mocks init_benchmark_instance out entirely).
  • No -oBatchMode=yes on any scp/ssh call. _parse_remote_autotuning_url now drops parsed.password, and the docs require key auth — so a ssh://user:pass@host URL (still the reason _redact_url_password exists) gets no usable credential, ssh inherits stdin and prompts interactively, and each candidate stalls for the full network_timeout_seconds (default 10 min) before returning inf.

Suggestions (non-blocking)

  • _probe_remote_trtexec_safe returns False for transport failures (unreachable host, rejected key, missing ssh) just as it does for a genuinely absent binary, and caches that for the whole run. Also, a remote_exec_path with no directory component makes test -x trtexec_safe resolve against the remote home dir instead of PATH, so the probe and the fallback trt_path disagree for that input.
  • Stale comment at :539 — the engine is pushed, not the model.

Assessment

Risk: medium-high. The structural work here is good and clearly responsive to earlier rounds — urlparse-based validation, duplicate-param rejection, the argv-smuggling guard on user/host, shlex.quote on every remote-side token, per-instance UUID engine names, credential redaction in logs, once-per-run binary probing so candidates stay comparable, and finally-scoped remote cleanup are all the right calls. The single inverted predicate is what blocks it: it silently zeroes out the fallback path and takes four tests with it. Everything else is small.

🤖 Generated with Claude Code

Signed-off-by: dmoodie <dmoodie@nvidia.com>
…delopt into dmoodie/bugfix/trtexec_safe

Signed-off-by: dmoodie <dmoodie@nvidia.com>
…ofiling

Signed-off-by: dmoodie <dmoodie@nvidia.com>

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Prior concerns are resolved and the remote path now reads correctly, but two open questions remain before this can merge.

Needs action:

  • Narrow the new except (ValueError, ImportError): raise in autotune/workflows.py:141 — it also escapes for TensorRTPyBenchmark's "TensorRT not available" / "PyTorch CUDA not available" ImportError, so a plain missing-TRT install now produces a raw traceback out of run_autotune()/quantize() instead of the previous "Failed to initialize TensorRT benchmark" + exit 1. Confirm intent or scope the re-raise to the remote-config path, and note it in CHANGELOG.rst.
  • 💬 Security Exception section is now in the PR body and each site has a why-safe comment — still needs an explicit review request to @NVIDIA/modelopt-setup-codeowners for the four # nosec B603 and the # nosec B404 per SECURITY.md step 2.
  • Use self.has_remote_config instead of self._remote_use_trtexec_safe is not None for latency-pattern selection in benchmark.py:622 — equivalent today but fragile.
  • Add --warmUp/--iterations to the forwarded-flags bullet in docs/source/guides/9_autotune.rst:259.

No action needed:

  • ✔️ 5 previous concerns addressed, including the FileNotFoundError filename and moving scp inside the try/finally.

@kevalmorabia97

Copy link
Copy Markdown
Collaborator

/claude review

@kevalmorabia97

Copy link
Copy Markdown
Collaborator

/ok to test 65cd70d

Comment on lines +575 to +586
remote_run_cmd = [
"ssh",
"-oBatchMode=yes",
"-oStrictHostKeyChecking=accept-new",
"-p",
f"{self.remote_port}",
f"{self.remote_user}@{self.remote_ip}",
f"{ld_path} {shlex.quote(trt_path)} {extra_flags}--useCudaGraph "
f"--warmUp={self.warmup_runs} --iterations={self.timing_runs} "
f"--avgRuns={self.timing_runs} --duration=0 "
f"--loadEngine={shlex.quote(self.remote_engine_path)}",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[IMPORTANT Algorithm] User-supplied measurement flags from --trtexec_benchmark_args are not forwarded to the remote run, so for dynamic-shape engines the measurement happens at a shape the user never asked for.

self.trtexec_args is folded into _base_cmd (line 511) and therefore reaches the local build, but remote_run_cmd here is fully hardcoded: --useCudaGraph --warmUp --iterations --avgRuns --duration=0 --loadEngine. Every inference-affecting flag the user passed is silently dropped at measurement time — most consequentially --shapes / --minShapes / --optShapes / --maxShapes, but also --useSpinWait, --infStreams, --streams, --noDataTransfers.

Why it matters: a user who builds with --minShapes=... --optShapes=... --maxShapes=... gets an engine with a multi-shape profile, and the remote --loadEngine call selects a profile shape on its own rather than the one the user intended to tune for. Every candidate is then ranked on a shape nobody requested, which is exactly the kind of silently-wrong ranking autotune cannot recover from — unlike the pre-fix inf, this failure is invisible.

The docs added in this PR compound it — docs/source/guides/9_autotune.rst:76 states:

--useCudaGraph, --avgRuns, and --duration=0 are forwarded to the remote trtexec_safe (or trtexec --safe) call so measurement settings match those used during local engine builds.

Neither half holds: these three are hardcoded rather than forwarded, and --useCudaGraph / --duration=0 never appear in the local build command at all.

Suggested fix — forward a whitelist of measurement-relevant user args into the remote command, e.g.:

_REMOTE_FORWARDED_PREFIXES = (
    "--shapes", "--minShapes", "--optShapes", "--maxShapes",
    "--useSpinWait", "--infStreams", "--streams", "--noDataTransfers",
)

forwarded = [
    shlex.quote(a)
    for a in self.trtexec_args
    if a.startswith(_REMOTE_FORWARDED_PREFIXES)
]
remote_run_cmd = [
    "ssh", "-oBatchMode=yes", "-oStrictHostKeyChecking=accept-new",
    "-p", f"{self.remote_port}",
    f"{self.remote_user}@{self.remote_ip}",
    " ".join([
        ld_path, shlex.quote(trt_path), *( [extra_flags.strip()] if extra_flags else [] ),
        "--useCudaGraph",
        f"--warmUp={self.warmup_runs}",
        f"--iterations={self.timing_runs}",
        f"--avgRuns={self.timing_runs}",
        "--duration=0",
        *forwarded,
        f"--loadEngine={shlex.quote(self.remote_engine_path)}",
    ]),
]

At minimum, if forwarding is deliberately out of scope for this PR, please correct the docs sentence to say the remote measurement flags are fixed and that shape/stream flags in --trtexec_benchmark_args apply to the build only.

@dthienan-nv dthienan-nv Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dynamic shapes are not permitted in safety thus not used in remote benchmarking. We should not need to reproduce the safety restrictions here as we want to maintain one source of truth. Thus assume the user is already familiar with the external restrictions.

user=parsed.username,
ip=parsed.hostname,
port=port,
bin_path=os.path.dirname(options["remote_exec_path"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] Deriving the remote binary directory by discarding remote_exec_path's basename makes two inputs fail quietly.

  1. No directory component. remote_exec_path=trtexec (relying on the remote PATH) yields bin_path == "", so os.path.join("", "trtexec_safe") is the bare relative string "trtexec_safe". The probe then runs test -x trtexec_safe, which resolves against the SSH login cwd rather than PATH, returns non-zero, and the run falls back to trt_path = "trtexec" (line 573) — also relative, also likely not found. Every candidate returns inf with only a "not found at ... on remote host" warning to go on.
  2. Non-default basename. remote_exec_path=/opt/trt/bin/trtexec_rel discards trtexec_rel and reconstructs /opt/trt/bin/trtexec for the fallback path, which need not exist even though the user gave a working binary.

Both become loud and correct with one validation plus reusing the user's own path for the fallback:

exec_path = options["remote_exec_path"]
if not os.path.isabs(exec_path):
    raise ValueError(
        "remote_exec_path in --remoteAutoTuningConfig must be an absolute path "
        f"on the remote device (got {exec_path!r})"
    )
return _RemoteAutotuningConfig(
    ...
    exec_path=exec_path,                 # used as the `trtexec --safe` fallback binary
    bin_path=os.path.dirname(exec_path), # used to locate `trtexec_safe`
    ...
)

tests/unit/.../test_trtexec_benchmark.py:170 already asserts bin_path == "/opt/trt/bin" for an absolute input, so an absolute-path requirement costs nothing in coverage.

Comment on lines +625 to +630
# trtexec_safe / trtexec --safe emit "GPU Compute Time"; local trtexec emits "Latency"
_latency_pattern = (
r"\[I\]\s+GPU Compute Time:.*?median\s*=\s*([\d.]+)\s*ms"
if self._remote_use_trtexec_safe is not None
else r"\[I\]\s+Latency:.*?median\s*=\s*([\d.]+)\s*ms"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] The regex selector keys off the wrong state variable. It happens to be correct today, but only by an invariant that isn't stated anywhere.

self._remote_use_trtexec_safe is not None is a proxy for "the probe has run", which is a proxy for "we are in remote mode". It works only because the probe at line 567-568 is unconditionally reached whenever has_remote_config is true and we get this far. Any future early-return or reordering between the probe and this line silently flips a remote run onto the local Latency pattern, which yields inf for every candidate — the exact failure mode this PR exists to fix, and the one a prior review round already caught in an earlier form of this expression.

The condition the comment above actually describes is has_remote_config, which is also what binary_name uses 10 lines up (line 617):

Suggested change
# trtexec_safe / trtexec --safe emit "GPU Compute Time"; local trtexec emits "Latency"
_latency_pattern = (
r"\[I\]\s+GPU Compute Time:.*?median\s*=\s*([\d.]+)\s*ms"
if self._remote_use_trtexec_safe is not None
else r"\[I\]\s+Latency:.*?median\s*=\s*([\d.]+)\s*ms"
)
# trtexec_safe / trtexec --safe emit "GPU Compute Time"; local trtexec emits "Latency"
_latency_pattern = (
r"\[I\]\s+GPU Compute Time:.*?median\s*=\s*([\d.]+)\s*ms"
if self.has_remote_config
else r"\[I\]\s+Latency:.*?median\s*=\s*([\d.]+)\s*ms"
)

Note tests/unit/.../test_trtexec_benchmark.py:757 and :835 set _remote_use_trtexec_safe directly on a remote_bench fixture, so they pass under either predicate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Somehow the agents are still wrong. We must key off of latency when using the trtexec binary which can still be remote with --safe and we need to key off of GPU Compute Time when using the trtexec_safe binary.

Comment on lines +112 to +113
network_timeout_minutes=args.network_timeout_minutes,
remote_engine_path=args.remote_engine_path,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] Now that init_benchmark_instance re-raises ValueError/ImportError instead of returning None, this call site loses the CLI's documented exit-code contract.

The init_benchmark_instance(...) call sits above the try: at line 120, so a malformed --remoteAutoTuningConfig, a plugin_libraries + remote clash, or TensorRT < 10.15 now escapes run_autotune as an uncaught exception. run_autotune's own docstring (lines 89-93) promises "1: Autotuning failed (exception occurred)", and the if benchmark_instance is None: return 1 branch at line 116 becomes dead for exactly the config errors this PR is about. The user gets a Python traceback rather than the descriptive message.

Failing fast is the right call per the PR description — this is just about presenting it cleanly:

    try:
        benchmark_instance = init_benchmark_instance(
            use_trtexec=args.use_trtexec,
            plugin_libraries=args.plugin_libraries,
            timing_cache_file=args.timing_cache,
            warmup_runs=args.warmup_runs,
            timing_runs=args.timing_runs,
            trtexec_args=trtexec_args,
            network_timeout_minutes=args.network_timeout_minutes,
            remote_engine_path=args.remote_engine_path,
        )
    except (ValueError, ImportError) as e:
        logger.error(f"Invalid benchmark configuration: {e}")
        return 1

    if benchmark_instance is None:
        logger.error("Failed to initialize TensorRT benchmark")
        return 1

test_cli_pipeline.py mocks init_benchmark_instance out entirely, so neither the raise nor the exit code is covered today.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review — CRITICAL 0, IMPORTANT 1, SUGGESTION 3

Scope: bare /claude review, so full scope. 10 files changed (+2030/-53). Reviewed all 5 modelopt/onnx/ source files, docs/source/guides/9_autotune.rst, CHANGELOG.rst, and targeted reads of the new tests. Note that git diff origin/main HEAD here also surfaces reverse-diffs of modelopt/onnx/autocast/, modelopt/onnx/export/, and the graph_utils split because main has advanced past this branch; those were excluded using the PR's authoritative file list and are not reviewed below.

Prior rounds are well addressed. Every blocking item from the two earlier reviews is resolved in this revision: the inverted _remote_use_trtexec_safe predicate that killed the trtexec --safe fallback, -oBatchMode=yes on all three ssh/scp invocations, plugin_libraries + remote now raising in __init__ with a matching docs note, init_benchmark_instance re-raising ValueError/ImportError so config errors are not flattened into RuntimeError, the changelog entries moved under Backward Breaking Changes, and the stale gpu-test mocks reverted. The defensive work is genuinely solid — urlparse-based validation with duplicate-key and port-range rejection, the leading-- argv-smuggling guard on user and host, shlex.quote on every remote-side token, _redact_url_password across all logged output, per-run UUID engine filenames, finally-scoped remote cleanup, and probing the remote binary once so all candidates stay comparable.

Most impactful finding

User measurement flags are not forwarded to the remote run (IMPORTANT, benchmark.py:575-586). self.trtexec_args reaches the local build via _base_cmd, but remote_run_cmd is fully hardcoded, so --shapes / --minShapes / --optShapes / --maxShapes, --useSpinWait, --infStreams, --streams, and --noDataTransfers are all dropped at measurement time. For a dynamic-shape engine that means every candidate is ranked at a profile shape trtexec picks on its own rather than the one the user tuned for — and unlike the pre-fix inf, that failure is silent. docs/source/guides/9_autotune.rst:76 compounds it by asserting these flags are forwarded "so measurement settings match those used during local engine builds"; in fact they are hardcoded, and --useCudaGraph / --duration=0 never appear in the local build at all. Either forward a whitelist (patch in the inline comment) or correct the docs to say the remote measurement flags are fixed.

Suggestions (non-blocking)

  • benchmark.py:306 — bin_path = dirname(remote_exec_path) fails quietly on two inputs: a PATH-relative remote_exec_path=trtexec makes both the probe and the fallback resolve against the SSH login cwd instead of PATH, and a non-default basename like trtexec_rel is discarded in favour of a reconstructed dirname/trtexec that need not exist. Requiring an absolute path and reusing it as the fallback binary makes both loud.
  • benchmark.py:625-630 — the regex selector keys off _remote_use_trtexec_safe is not None (a proxy for "the probe ran") where the comment directly above describes has_remote_config, which binary_name already uses 10 lines up. Correct today only by an unstated invariant; any future early-return between the probe and this line puts a remote run back on the Latency pattern and returns inf for everything.
  • autotune/__main__.py:105-118 — the init_benchmark_instance call sits above the try:, so the newly-raised config errors escape run_autotune as a traceback, breaking its documented "1: Autotuning failed" contract and making the benchmark_instance is None branch dead for those errors. Failing fast is right; catching and logging keeps it clean.
  • Still open from a prior round: log_file records only the build output (benchmark.py:517), never the remote timing output the latency regex actually parses — so an inf candidate is not diagnosable from the saved artifact, which is the main reason the log exists.

Risk assessment

Low-moderate, well-contained. Everything sits behind --use_trtexec in the ONNX autotune path; no modelopt_state schema, mode registration, export path, or public modelopt/torch API is touched, so there is no checkpoint or restore-fidelity exposure. The GPU Compute Time metric switch is now correctly scoped to remote mode, leaving purely local --use_trtexec users on Latency as before. Residual risk is concentrated in the one IMPORTANT item, which affects dynamic-shape models specifically and is a silent mis-ranking rather than a hard failure.

🤖 Generated with Claude Code

@kevalmorabia97

Copy link
Copy Markdown
Collaborator

please rebase your PR so merge conflicts is fixed and also see if any claude review comment is pending to be addressed or not

Signed-off-by: dmoodie <dmoodie@nvidia.com>
…izer into dmoodie/bugfix/trtexec_safe

Signed-off-by: dmoodie <dmoodie@nvidia.com>
@dthienan-nv

Copy link
Copy Markdown
Contributor Author

/claude review

…izer into dmoodie/bugfix/trtexec_safe

Signed-off-by: dmoodie <dmoodie@nvidia.com>
@gcunhase

gcunhase commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

@claude review

@claude

claude Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Claude review in progress

  • Gather context (repo guidelines, PR diff)
  • Review benchmark.py remote-autotuning logic
  • Review CLI/workflow plumbing and docs/CHANGELOG
  • Review new tests
  • Post review

View job run

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (gpt-6.1-sol) — DM the bot to share feedback.

Changes requested: 548 added logic lines require splitting, and remote-path handling and malformed-config logging still need fixes.

Needs action:

  • ✂️ Split into [1/3] autotune/benchmark.py helpers, [2/3] remote execution plus autotune/workflows.py/__main__.py, [3/3] parent quantization/ wiring. The 520-line autotune slice needs this further cut. Link siblings; state linear merge order; each builds, includes tests, and passes CI independently.
  • Redact malformed remote-config arguments in benchmark.py; add a credential-leak regression test (inline).
  • Preserve the supplied remote_exec_path for fallback execution in benchmark.py; test a nonstandard binary basename (inline).
  • 💬 Author requested a security exception — still request explicit @NVIDIA/modelopt-setup-codeowners approval for the five new Bandit suppressions, as required by SECURITY.md.
  • Explain in the PR body why extending TrtExecBenchmark with stdlib SSH subprocesses is preferable to reusing tools/launcher/core.py or adding Paramiko/Fabric; incorporate the QNX/manual-execution rationale from the replies.

No action needed:

  • ✔️ Prior timeout, key-authentication, UUID-default, cleanup, plugin-rejection, CLI-error-handling, and remote-log concerns are resolved. New test headers match LICENSE_HEADER; existing integration coverage is extended, not weakened. Tests were inspected, not run.

ip: str
port: int
bin_path: str # dirname of ``remote_exec_path`` — directory containing trtexec_safe / trtexec
lib_path: str # value of ``remote_lib_path``

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot comment.

The malformed-argument branch logs and raises with raw arg. For example, --remoteAutoTuningConfigssh://alice:secret@host?... reaches this branch and exposes the password through both the debug log and the CLI's logger.error(str(e)). The normal command/output paths are now redacted, but this validation path still bypasses that protection. Redact arg before formatting either message (or omit the supplied value), and add a regression test checking that neither logs nor the exception contain the password.

# Flags from trtexec_args forwarded to the remote measurement run (--loadEngine=).
# --minShapes / --optShapes / --maxShapes are build-time only: they define optimization profiles
# embedded in the engine and are meaningless (or error-prone) with --loadEngine. They already
# reach the local engine build via _base_cmd.extend(trtexec_args); do not repeat them here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot comment.

Requiring an absolute remote_exec_path fixes the relative-path case, but the parser still discards the supplied basename. A valid /opt/trt/bin/trtexec_rel config builds using that configured executable, then the fallback reconstructs /opt/trt/bin/trtexec, which may not exist. Preserve an exec_path field and use it when the probe selects the fallback; use only its dirname to locate trtexec_safe. Add a fallback test with a nonstandard executable basename.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants