Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Changelog

- Fix ``ShapeInferenceError`` during ONNX INT8 + FP16 quantization (``--high_precision_dtype fp16``) of weakly-typed models (e.g. TensorFlow exports) that carry stale rank-0 ``graph.output`` shapes or ops such as ``TopK`` that ONNX's static shape inference cannot resolve. ``clear_stale_value_info`` now reconciles stale output shapes via symbolic shape inference (keeping every output's shape field populated), and AutoCast runs ONNX shape inference in strict mode and falls back to schema-based standalone type inference when it fails, so unresolved ops no longer leave tensors untyped.
- Fused MoE expert auto-detection (``register_fused_experts_on_the_fly``) no longer requires an ``act_fn`` attribute. Some fused-expert modules (e.g. ``MiniMaxM3VLExperts``) apply a custom gated activation between the two ``F.linear`` calls instead of exposing ``act_fn``; they were silently skipped, leaving routed experts unquantized (an experts-only recipe matched nothing) and failing HF export with ``NotImplementedError``. ``_QuantFusedExperts`` is activation-agnostic (it only intercepts the two ``F.linear`` calls), so the requirement was unnecessary. This enables NVFP4/FP8 quantization and export for MiniMax-M2 / MiniMax-M3.
- Fix unified HF export emitting transformers' *in-memory* (post-``conversion_mapping``) tensor names instead of the original model-hub names, breaking the unified-checkpoint contract (observed on MiniMax-M3: exported ``model.language_model.*`` / ``mlp.experts.*.gate_proj`` instead of hub ``language_model.model.*`` / ``block_sparse_moe.experts.*.w{1,2,3}``). transformers' own save-side ``revert_weight_conversion`` is disabled by ModelOpt because it raises ``RuntimeError`` on 0-d scalar scale tensors, so a new quant-aware reverse conversion (``modelopt/torch/export/quant_aware_conversion.py``) derives rename/split rules from the model's conversion mapping via transformers' ``reverse_transform()`` and carries each weight's companion scale tensors (``weight_scale``, ``weight_scale_2``, ``input_scale``, ``weight_scale_inv``, ``bias``) through the renames and un-fusions, so quantized exports round-trip to the hub names. Any mapping op that cannot be reversed quant-aware yet (e.g. still-stacked fused experts) falls back to the previous in-memory names instead of aborting the export.

0.45 (2026-07-02)
^^^^^^^^^^^^^^^^^
Expand Down
61 changes: 41 additions & 20 deletions examples/hf_ptq/example_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,19 +863,47 @@ def _resolve_model_path(model_name_or_path: str, trust_remote_code: bool = False


def copy_custom_model_files(source_path: str, export_path: str, trust_remote_code: bool = False):
"""Copy custom model files (configuration_*.py, modeling_*.py, *.json, etc.) from source to export directory.

This function copies custom Python files and JSON configuration files that are needed for
models with custom code. It excludes config.json and model.safetensors.index.json as these
are typically handled separately by the model export process.
"""Copy processor/tokenizer artifacts (and, with trust_remote_code, custom code) to export.

Processor and tokenizer *data* artifacts -- e.g. a VLM's ``preprocessor_config.json``,
``merges.txt``/``vocab.json``, and the processor helper modules -- are needed by the
deployment stack (vLLM/SGLang) even when the model itself runs on native (non-remote)
transformers code. transformers 5.x restructured many VLM configs and no longer
re-saves these on ``save_pretrained`` for models loaded natively, so without copying
them a native-path export is missing e.g. ``preprocessor_config.json`` and fails to
load (``Can't load image processor``). These are copied regardless of
``trust_remote_code``. Executable model/config code (``modeling*.py``,
``configuration_*.py``, ``tokenization_*.py``, and other custom JSON) is only meaningful
with ``trust_remote_code`` and is copied only then. ``config.json`` and
``model.safetensors.index.json`` are always skipped (handled by the export itself).

Args:
source_path: Path to the original model directory or HuggingFace model ID
export_path: Path to the exported model directory
trust_remote_code: Whether trust_remote_code was used (only copy files if True)
trust_remote_code: Whether trust_remote_code was used (gates the executable code files)
"""
if not trust_remote_code:
return
# Deployment-critical processor/tokenizer artifacts: safe to copy regardless of
# trust_remote_code (data + processor helpers, not model code).
always_copy_patterns = [
"preprocessor_config.json",
"processor_config.json",
"image_processing*.py",
"processing_*.py",
"video_processing*.py",
"feature_extraction_*.py",
"added_tokens.json",
"special_tokens_map.json",
"vocab.json",
"merges.txt",
"tokenizer.model",
]
# Executable custom model/config code + other custom JSON: only used with trust_remote_code.
code_patterns = [
"configuration_*.py",
"modeling*.py",
"tokenization_*.py",
"*.json",
]

# Resolve the source path (handles both local paths and HF model IDs)
resolved_source_path = _resolve_model_path(source_path, trust_remote_code)
Expand All @@ -897,24 +925,17 @@ def copy_custom_model_files(source_path: str, export_path: str, trust_remote_cod
print(f"Warning: Export directory {export_path} does not exist")
return

# Common patterns for custom model files that need to be copied
custom_file_patterns = [
"configuration_*.py",
"modeling*.py",
"tokenization_*.py",
"processing_*.py",
"image_processing*.py",
"feature_extraction_*.py",
"*.json",
]
patterns = [*always_copy_patterns, *(code_patterns if trust_remote_code else [])]

copied_files = []
for pattern in custom_file_patterns:
copied_files: list[str] = []
for pattern in patterns:
for file_path in source_dir.glob(pattern):
if file_path.is_file():
# Skip config.json and model.safetensors.index.json as they're handled separately
if file_path.name in ["config.json", "model.safetensors.index.json"]:
continue
if file_path.name in copied_files: # e.g. matched by both pattern lists
continue
dest_path = export_dir / file_path.name
try:
shutil.copy2(file_path, dest_path)
Expand Down
Loading
Loading