mirror of
https://github.com/NVIDIA/Model-Optimizer.git
synced 2026-10-28 07:13:15 +08:00
### What does this PR do? Type of change: Backward breaking change (dependency / Python version support) Python 3.10 has reached end-of-life, so ModelOpt now requires Python 3.11+. This PR drops 3.10 support and adopts the newer Python features. **Packaging, CI and docs** - `requires-python = ">=3.11,<3.15"` in the root package, `tools/launcher` and `tools/mcp`. Removes the 3.10 classifier. - Removes the 3.10-only ONNX Runtime 1.22 / `cupy-cuda12x` dependency markers and `tomli`. - Ruff `target-version` and mypy `python_version` are now 3.11. - Removes the `unit-3.10` nox session and CI matrix entry, plus the `tomli` install in the skills job. - Updates the install docs for Linux and Windows, and the example READMEs. - CHANGELOG: adds a backward-breaking entry and removes the Python 3.10 caveat from the unreleased ONNX Runtime 1.30 entry. **Python 3.11+ features** - `enum.StrEnum` replaces `(str, Enum)`: `RecipeType` plus the diffusers and specdec_bench examples. `str()` and f-strings of these members now give the value (e.g. `"ptq"`), not `"RecipeType.PTQ"`. - `datetime.UTC`, `sys.exception()`, `hashlib.file_digest`, `contextlib.chdir`, `tomllib`. - `typing.Self`, `NotRequired` and `Required` come from `typing` instead of `typing_extensions`. - `importlib.resources.abc.Traversable` is imported directly; the fallback imports are removed. - Removes the Python 3.10 checks in `modelopt.onnx`. This includes the `preload_dlls()` skip in `ort_utils.py`. - Removes `# noqa: PERF203`. On 3.11, try/except in a loop has no runtime cost, so ruff no longer flags it. **`from __future__ import annotations`** - Removed from 78 of 85 files. - Imports that were only under `if TYPE_CHECKING:` (cheap, with no import cycle) are now regular imports. - Methods returning `self` or `cls(...)` are annotated `-> Self`. - The import stays in 7 files where it is still needed: - `fastgen/flow_matching.py`: import cycle with `fastgen/config.py`. - `fastgen/config.py`: an own-class `from_yaml` that doesn't return `cls`. - 4 puzzletron sewing_kit/validate files: forward references. - `benchmark_via_builtin.py`: lazy `torch` import. `uv.lock` is left for the weekly bump job. ### Usage N/A ### Testing Run locally on Python 3.11: - `nox -s "unit-3.11(torch_214, tf_latest)"`: 5478 passed, 18 skipped, 12 xfailed. - `pytest plugins/modelopt/skills/`: 141 passed. - `tools/mcp` tests: 71 passed. - `pre-commit` on all changed files: passed. This includes ruff, and mypy with `python_version = "3.11"`. - Imported every module whose `from __future__ import annotations` was removed, under Python 3.11, to catch eagerly evaluated annotations. Ten examples whose dependencies aren't installed locally (`nemo_automodel`, `ltx_trainer`, `vllm`) were covered by ruff F821/TC004 and an AST check instead. Not run locally: the GPU and example tests. Their CI runs on pinned Python 3.12 containers. `tests/_test_utils/examples/megatron_example_runner.py` (now `contextlib.chdir`) is only exercised there. ### Before your PR is "*Ready for review*" Make sure you read and follow [Contributor guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md) and your commits are signed (`git commit -s -S`). Make sure you read and follow the [Security Best Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors) (e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(..., weights_only=False)`, `pickle`, etc.). - Is this change backward compatible?: ❌ Python 3.10 is no longer supported. - 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 - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: ✅ - Did you get Claude approval on this PR?: ❌ ### Additional Information The CHANGELOG has announced since an earlier release that Python 3.10 support would be dropped in the next release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Breaking Changes** * Python 3.10 is no longer supported; Python 3.11 or later is required. * **Compatibility** * Linux x86-64 ONNX installations now default to CUDA 13 packages and ONNX Runtime GPU 1.30.x, regardless of Python version. GPU calibration requires a compatible CUDA 13 runtime and driver. * CUDA 12.8 remains an alternative for Linux x86-64 with CUDA 12-compatible PyTorch. ONNX package selections vary by platform, including CPU packages for aarch64 and macOS. * **Documentation** * Installation guides and example prerequisites reflect the updated Python requirement and ONNX package options. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
6.0 KiB
Python
165 lines
6.0 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2026 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.
|
|
|
|
"""Portable and contained path contract for the shared FastGen cache."""
|
|
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
import types
|
|
|
|
import pytest
|
|
|
|
torch = pytest.importorskip("torch")
|
|
pytest.importorskip("nemo_automodel")
|
|
pytest.importorskip("torchdata")
|
|
|
|
_REPO_ROOT = pathlib.Path(__file__).resolve().parents[4]
|
|
_FASTGEN_DIR = _REPO_ROOT / "examples" / "diffusers" / "fastgen"
|
|
if str(_FASTGEN_DIR) not in sys.path:
|
|
sys.path.insert(0, str(_FASTGEN_DIR))
|
|
|
|
from fastgen_data import build_text_to_image_multiresolution_dataloader
|
|
from fastgen_data.paths import resolve_cache_root, resolve_under_root
|
|
from fastgen_data.text_to_image_dataset import TextToImageDataset
|
|
|
|
|
|
def test_cache_root_resolves_configured_directory(make_fastgen_cache, tmp_path):
|
|
cache = make_fastgen_cache(tmp_path / "cache")
|
|
|
|
assert resolve_cache_root(cache) == cache.resolve()
|
|
|
|
|
|
def test_cache_root_rejects_missing_or_non_directory(tmp_path):
|
|
with pytest.raises(FileNotFoundError):
|
|
resolve_cache_root(tmp_path / "missing")
|
|
|
|
regular_file = tmp_path / "file"
|
|
regular_file.write_text("not a directory")
|
|
with pytest.raises(NotADirectoryError):
|
|
resolve_cache_root(regular_file)
|
|
|
|
|
|
def test_resolve_under_root_rejects_traversal_absolute_and_symlink_escape(tmp_path):
|
|
root = tmp_path / "cache"
|
|
root.mkdir()
|
|
inside = root / "inside.pt"
|
|
inside.write_bytes(b"inside")
|
|
outside = tmp_path / "outside.pt"
|
|
outside.write_bytes(b"outside")
|
|
(root / "escape.pt").symlink_to(outside)
|
|
|
|
assert resolve_under_root(root, "inside.pt", "sample") == inside.resolve()
|
|
for candidate in ("../outside.pt", outside, "escape.pt"):
|
|
with pytest.raises(ValueError, match="sample"):
|
|
resolve_under_root(root, candidate, "sample")
|
|
|
|
|
|
def test_dataset_rejects_metadata_shard_escape(make_fastgen_cache, tmp_path):
|
|
cache = make_fastgen_cache(tmp_path / "cache")
|
|
safe_item = json.loads((cache / "metadata_shard_0.json").read_text())[0]
|
|
outside_shard = tmp_path / "outside.json"
|
|
outside_shard.write_text(json.dumps([safe_item]))
|
|
(cache / "metadata.json").write_text(json.dumps({"shards": [str(outside_shard)]}))
|
|
|
|
with pytest.raises(ValueError, match="metadata shard"):
|
|
TextToImageDataset(cache)
|
|
|
|
|
|
def test_dataset_rejects_payload_symlink_escape(make_fastgen_cache, tmp_path):
|
|
cache = make_fastgen_cache(tmp_path / "cache")
|
|
outside = tmp_path / "outside.pt"
|
|
torch.save({"latent": torch.zeros(1)}, outside)
|
|
(cache / "escape.pt").symlink_to(outside)
|
|
shard = json.loads((cache / "metadata_shard_0.json").read_text())
|
|
shard[0]["cache_file"] = "escape.pt"
|
|
(cache / "metadata_shard_0.json").write_text(json.dumps(shard))
|
|
|
|
dataset = TextToImageDataset(cache)
|
|
with pytest.raises(ValueError, match="sample cache file"):
|
|
dataset[0]
|
|
|
|
|
|
def test_dataset_accepts_absolute_payload_beneath_root(make_fastgen_cache, tmp_path):
|
|
cache = make_fastgen_cache(tmp_path / "cache", absolute_payloads=True)
|
|
dataset = TextToImageDataset(cache)
|
|
|
|
assert "latent" in dataset[0]
|
|
|
|
|
|
def test_prompt_only_dataset_and_loader_do_not_emit_image_latents(make_fastgen_cache, tmp_path):
|
|
cache = make_fastgen_cache(tmp_path / "cache")
|
|
dataset = TextToImageDataset(cache, prompt_only=True)
|
|
assert "latent" not in dataset[0]
|
|
|
|
loader, _ = build_text_to_image_multiresolution_dataloader(
|
|
cache_dir=str(cache),
|
|
prompt_only=True,
|
|
batch_size=1,
|
|
num_workers=0,
|
|
shuffle=False,
|
|
negative_prompt_embedding_path="negative_prompt_embedding.pt",
|
|
)
|
|
batch = next(iter(loader))
|
|
|
|
assert "image_latents" not in batch
|
|
assert {"text_embeddings", "text_embeddings_mask"}.issubset(batch)
|
|
assert "negative_text_embeddings" in batch
|
|
|
|
|
|
def test_builder_rejects_negative_embedding_escape(make_fastgen_cache, tmp_path):
|
|
cache = make_fastgen_cache(tmp_path / "cache")
|
|
outside = tmp_path / "negative.pt"
|
|
torch.save(torch.zeros(2, 3), outside)
|
|
|
|
with pytest.raises(ValueError, match="negative prompt embedding"):
|
|
build_text_to_image_multiresolution_dataloader(
|
|
cache_dir=str(cache),
|
|
num_workers=0,
|
|
negative_prompt_embedding_path=str(outside),
|
|
)
|
|
|
|
|
|
def test_preprocessing_publishes_relocatable_paths(monkeypatch, tmp_path):
|
|
# The metadata publisher does not use OpenCV. Stub that optional video dependency so this
|
|
# CPU-only test executes the real publisher in the lean AutoModel test environment.
|
|
monkeypatch.setitem(sys.modules, "cv2", types.ModuleType("cv2"))
|
|
from preprocess.preprocessing_multiprocess import _save_metadata_shards
|
|
|
|
monkeypatch.chdir(tmp_path)
|
|
output = pathlib.Path("relative-cache")
|
|
output.mkdir()
|
|
payload = output / "sample.pt"
|
|
torch.save({"latent": torch.zeros(1)}, payload)
|
|
|
|
_save_metadata_shards(
|
|
[{"cache_file": str(payload)}],
|
|
output,
|
|
"qwen_image",
|
|
"Qwen/Qwen-Image",
|
|
"qwen_image",
|
|
10,
|
|
{},
|
|
)
|
|
|
|
shard = json.loads((output / "metadata_shard_s0000.json").read_text())
|
|
published = pathlib.Path(shard[0]["cache_file"])
|
|
assert published == pathlib.Path("sample.pt")
|
|
|
|
relocated = tmp_path / "relocated-cache"
|
|
output.rename(relocated)
|
|
resolved = resolve_under_root(relocated, published, "sample cache file")
|
|
assert torch.equal(torch.load(resolved, weights_only=True)["latent"], torch.zeros(1))
|