Files
Shengliang Xu 9d0d97829a chore(lint): modernize typing (PEP 604/585) and enable UP032 (#1537)
### What does this PR do?

Type of change: chore / refactor (no behavior change)

Two small lint-cleanup commits:

**1. `chore(typing): modernize Union/Optional/List to PEP 604 / 585
syntax`** (8 files)

- Replace `X = Union[A, B] # noqa: UP007` with `X: TypeAlias = A | B`
for the six module-level type aliases (`ModelLike`, `Criterion`,
`NodeTarget`, `CalibrationDataType`, `Hparam.Importance` /
`ActiveSlice`). The `TypeAlias` annotation is required so mypy continues
to treat them as aliases under PEP 604.
- Modernize forward-ref unions in `modelopt/onnx/quantization/autotune/`
to full-string forward refs (e.g. `"RegionPattern | None"`).
- Update docstring type tags in
`examples/puzzletron/evaluation/hf_deployable_anymodel.py`.

**2. `chore(lint): remove UP032 ignore and convert .format() to
f-strings`** (10 files)

- Drop `UP032` from `extend-ignore` in `pyproject.toml`.
- Auto-convert 19 `"...".format(...)` calls to f-strings across export
plugins, examples, tests, and tools. One conversion in
`modelopt/torch/utils/plugins/megatron_generate.py` was wrapped manually
to stay under the 100-char limit.

**Intentionally left as-is:**

- `tools/launcher/slurm_config.py` keeps its `# ruff: noqa: UP045` —
nemo_run's CLI parser can't introspect PEP 604 optional annotations.
- `modelopt/torch/puzzletron/*` is **not** touched. The subtree disables
ruff's `UP` family entirely (per-file-ignore `"UP"`) while migration is
in progress, and converting `Optional[X]` to `X | None` there would
silently break runtime introspection in
`block_config._get_dataclass_type` that uses `get_origin(tp) is
typing.Union` (PEP 604 unions return `types.UnionType` from
`get_origin`, not `typing.Union`). Best revisited when puzzletron's lint
carve-out is narrowed.
- `UP038` (`isinstance(x, (int, float))` → `isinstance(x, int | float)`)
— ruff has officially deprecated this rule; PEP 604 in isinstance is
slightly slower and misleads readers about PEP 695 / `Optional`. Ignore
kept.

### Usage

No user-facing API changes.

### Testing

- Pre-commit hooks (ruff check, ruff format, mypy, bandit, license) pass
on both commits.
- Ruff status against `main`: 37 unrelated pre-existing findings
(W291/W293/E501/RUF005/PLR1704); zero new findings introduced by this
PR.

### Before your PR is "*Ready for review*"

- Is this change backward compatible?: ✅ — Runtime behavior of the six
type aliases changes from a `typing.Union` instance to
`types.UnionType`. Downstream code introspecting via `get_origin(...) is
typing.Union` on these aliases would break, but no in-repo caller does
this on them. (The introspection in
`modelopt/torch/puzzletron/block_config.py` operates on user-supplied
dataclass field types, none of which are these aliases.)
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: N/A (no behavior change)
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
❌ — internal style refactor; happy to add a Misc note if reviewers want
one.
- Did you get Claude approval on this PR?: ❌ — not yet.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Modernized type annotations across the codebase to use Python 3.10+
union syntax and TypeAlias where appropriate.
* Standardized string formatting to f-strings, improving clarity of
logs, errors, and validation messages.

* **Chores**
  * Updated linting configuration to reflect modern typing/style rules.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/NVIDIA/Model-Optimizer/pull/1537?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
2026-05-26 16:47:31 -07:00

124 lines
3.8 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions for computational graph."""
import itertools
from collections.abc import Callable, Sequence
from typing import TypeAlias
import torch
from torch import nn
from torch.fx import Node, symbolic_trace
__all__ = ["match"]
NodeTarget: TypeAlias = nn.Module | Callable
def _get_node_target(node: Node, root: nn.Module) -> NodeTarget | None:
"""Return node target depending on node operator."""
target_extractor = {
"call_module": lambda t: root.get_submodule(t),
"call_method": lambda t: getattr(torch.Tensor, t),
"call_function": lambda t: t,
"output": lambda _: None,
"placeholder": lambda _: None,
"get_attr": lambda _: None,
}
return target_extractor[node.op](node.target)
def _local_match(nx: Node, ny: Node, mx: nn.Module, my: nn.Module) -> bool:
# Check if input and output degrees match.
if len(nx.all_input_nodes) != len(ny.all_input_nodes):
return False
if len(nx.users) != len(ny.users):
return False
# Check if the node operator matches.
if nx.op != ny.op:
return False
# Check if the node target matches.
tx = _get_node_target(nx, mx)
ty = _get_node_target(ny, my)
if isinstance(tx, nn.Module) and isinstance(ty, nn.Module):
tx, ty = type(tx), type(ty)
return tx == ty
def _recursive_match(
nx: Node, ny: Node, mx: nn.Module, my: nn.Module, maps: dict[Node, Node]
) -> bool:
# Check if we have already matched these nodes in the current traversal.
if nx in maps:
return maps[nx] == ny
# Terminate early if the nodes do not match locally.
if not _local_match(nx, ny, mx, my):
return False
# Optimistically mark `nx` as a match for `ny`.
maps[nx] = ny
# Both nodes are inputs. We have a match!
if nx.op == "placeholder":
return True
# Enumerate all possible input node matches.
ixs = nx.all_input_nodes
for iys in itertools.permutations(ny.all_input_nodes):
if not all(_local_match(ix, iy, mx, my) for ix, iy in zip(ixs, iys)):
continue
if all(_recursive_match(ix, iy, mx, my, maps) for ix, iy in zip(ixs, iys)):
return True
# No match found.
del maps[nx]
return False
def match(module: nn.Module, patterns: Sequence[nn.Module]) -> bool:
"""Check if a module matches any of the patterns.
Args:
module: The module to be checked.
patterns: The patterns to be matched.
Returns:
True if the module matches any of the patterns, False otherwise.
"""
try:
module_g = symbolic_trace(module).graph
except Exception:
return False
for pattern in patterns:
pattern_g = symbolic_trace(pattern).graph
# Check if the number of nodes match.
if len(module_g.nodes) != len(pattern_g.nodes):
continue
# Extract output nodes from the graphs.
*_, pattern_o = pattern_g.nodes
*_, module_o = module_g.nodes
# Check whether two graphs match recursively from the output node.
if _recursive_match(pattern_o, module_o, pattern, module, {}):
return True
return False