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
42 changes: 42 additions & 0 deletions modelopt/onnx/llm_export_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-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.

"""Deprecated shim for the legacy ``modelopt.onnx.llm_export_utils`` package.

The in-repo LLM ONNX export pipeline (formerly ``examples/torch_onnx/llm_export.py``
plus this package) was removed in 0.44.0rc1 in favor of
`TensorRT-Edge-LLM <https://github.com/NVIDIA/TensorRT-Edge-LLM>`_, which provides
a more complete and actively maintained pipeline.

This package is preserved only as a compatibility shim so external consumers that
still import ``modelopt.onnx.llm_export_utils`` (notably TensorRT-Edge-LLM 0.6.1
and earlier) continue to work. It will be removed in a future release.

New code should migrate to:

* ``modelopt.onnx.export`` — quant exporters (``FP8QuantExporter``, ``NVFP4QuantExporter``, etc.)
* ``modelopt.onnx.graph_surgery`` — graph transforms (GQA replacement, BF16 conversion, etc.)
* `TensorRT-Edge-LLM <https://github.com/NVIDIA/TensorRT-Edge-LLM>`_ — end-to-end LLM export.
"""

import warnings

warnings.warn(
"modelopt.onnx.llm_export_utils is deprecated and will be removed in a future "
"release. Use modelopt.onnx.export and modelopt.onnx.graph_surgery, or migrate "
"to TensorRT-Edge-LLM (https://github.com/NVIDIA/TensorRT-Edge-LLM).",
DeprecationWarning,
stacklevel=2,
)
162 changes: 162 additions & 0 deletions modelopt/onnx/llm_export_utils/export_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-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.

"""Utilities for exporting LLM models to ONNX."""

import json
import os
import time
from enum import Enum

import torch
from transformers import AutoModelForCausalLM, DynamicCache

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file
fd -e py "export_utils.py" -p | head -5

Repository: NVIDIA/Model-Optimizer

Length of output: 165


🏁 Script executed:

# Check if the file exists and read it
ls -la modelopt/onnx/llm_export_utils/export_utils.py 2>/dev/null && echo "File exists" || echo "File not found"

Repository: NVIDIA/Model-Optimizer

Length of output: 174


🏁 Script executed:

# Look for pyproject.toml to understand install extras
cat pyproject.toml | head -100

Repository: NVIDIA/Model-Optimizer

Length of output: 4025


🏁 Script executed:

# Search for import_plugin usage in the codebase
rg "import_plugin" --type py -A 2 -B 2 | head -50

Repository: NVIDIA/Model-Optimizer

Length of output: 3325


🏁 Script executed:

# Read the export_utils.py file to see the import and usage
cat -n modelopt/onnx/llm_export_utils/export_utils.py

Repository: NVIDIA/Model-Optimizer

Length of output: 7250


🏁 Script executed:

# Check how import_plugin is defined and used
cat -n modelopt/torch/utils/import_utils.py | head -50

Repository: NVIDIA/Model-Optimizer

Length of output: 1842


🏁 Script executed:

# Look for other files in modelopt/onnx that might use import_plugin for HF deps
rg "import_plugin|transformers" modelopt/onnx/ --type py -B 2 -A 2 | head -80

Repository: NVIDIA/Model-Optimizer

Length of output: 2585


Move the transformers imports inside the functions that use them, or gate them with import_plugin().

This module hard-imports transformers at the top level (line 24), which breaks on installs without the [hf] extras. Per the coding guidelines, optional dependencies must be lazy-loaded inside functions or gated by import_plugin(). Other files in modelopt/onnx/ (e.g., rope_cache.py, whisper_utils.py) correctly use lazy imports within functions. Apply the same pattern here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelopt/onnx/llm_export_utils/export_utils.py` at line 24, The top-level
import of transformers (AutoModelForCausalLM, DynamicCache) in export_utils.py
must be made lazy: remove the module-level import and instead import
AutoModelForCausalLM and DynamicCache inside the functions that actually use
them (or call import_plugin() before importing) so users without the [hf] extras
won't fail on import; locate references to AutoModelForCausalLM and DynamicCache
in this file (e.g., in any functions that construct or manipulate models/caches)
and add local imports like "from transformers import AutoModelForCausalLM,
DynamicCache" inside those functions (or gate with import_plugin()) just before
use.



class RopeType(Enum):
"""Rope type enum."""

K_NONE = 0
K_ROPE_ROTATE_GPTJ = 1
K_ROPE_ROTATE_NEOX = 2
K_MROPE = 3


class ModelLoader:
"""A class to handle HuggingFace model loading and configuration."""

def __init__(self, hf_model_path: str, config_path: str):
"""Initialize the ModelLoader."""
self.config_path = config_path
self.hf_model_path = hf_model_path
self.model_type = self.get_model_type()
self.hf_model = None
self.rope_type = RopeType.K_ROPE_ROTATE_NEOX

def get_model_type(self):
"""Get model type from config file."""
with open(self.config_path) as f:
return json.load(f).get("model_type")

def load_model(self, trust_remote_code: bool = False) -> AutoModelForCausalLM:
"""Load HuggingFace model based on model type."""
print(f"Loading HF model from {self.hf_model_path} with model type {self.model_type}")
self.hf_model = AutoModelForCausalLM.from_pretrained(
self.hf_model_path, torch_dtype=torch.float16, trust_remote_code=trust_remote_code
)

return self.hf_model.eval().cuda() # type: ignore[attr-defined]

def get_rope_type(self):
"""Get rope type."""
return self.rope_type


class WrapperModelForCausalLM(torch.nn.Module):
"""Wrapper Model to ensure all models have the same I/O."""

def __init__(self, model):
"""Initialize the WrapperModelForCausalLM."""
super().__init__()
try:
self.model = model.model
except Exception:
self.model = model
self.lm_head = model.lm_head
self.config = model.config

def forward(self, input_ids: torch.Tensor | None, past_key_values: tuple):
"""Forward pass."""
# Convert tuple cache to DynamicCache for models that require it (e.g., Qwen3)
cache = DynamicCache(config=self.config)
cache.key_cache = [kv[0] for kv in past_key_values]
cache.value_cache = [kv[1] for kv in past_key_values]
past_key_values = cache

outputs = self.model(input_ids=input_ids, past_key_values=past_key_values, use_cache=True)
hidden_states = outputs[0]
past_key_values = outputs.past_key_values.to_legacy_cache()
logits = self.lm_head(hidden_states)
return logits, past_key_values
Comment on lines +79 to +91

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
find . -name "export_utils.py" -path "*/onnx/llm_export_utils/*" | head -5

Repository: NVIDIA/Model-Optimizer

Length of output: 115


🏁 Script executed:

# Read the file to verify the code at lines 79-91 and check the llm_to_onnx function
if [ -f "modelopt/onnx/llm_export_utils/export_utils.py" ]; then
  echo "=== Lines 70-100 of export_utils.py ==="
  sed -n '70,100p' modelopt/onnx/llm_export_utils/export_utils.py
  echo ""
  echo "=== Lines 125-145 of export_utils.py (checking llm_to_onnx) ==="
  sed -n '125,145p' modelopt/onnx/llm_export_utils/export_utils.py
fi

Repository: NVIDIA/Model-Optimizer

Length of output: 2427


🏁 Script executed:

# Let's get the full context of the file to better understand the wrapper class and llm_to_onnx
wc -l modelopt/onnx/llm_export_utils/export_utils.py

Repository: NVIDIA/Model-Optimizer

Length of output: 117


🏁 Script executed:

# Let's check the torch_to_onnx function to understand how it calls the model
rg -n "def torch_to_onnx" modelopt/onnx/llm_export_utils/ -A 10

Repository: NVIDIA/Model-Optimizer

Length of output: 1053


🏁 Script executed:

# Let's also check the full context around the llm_to_onnx function to see exactly where extra_inputs is used
sed -n '90,145p' modelopt/onnx/llm_export_utils/export_utils.py

Repository: NVIDIA/Model-Optimizer

Length of output: 2427


Forward method must accept extra_inputs for VL model exports to work.

llm_to_onnx() passes additional inputs (intended for vision-language models) as keyword arguments through torch.onnx.export(), but the wrapper's forward() method signature at line 79 only accepts input_ids and past_key_values. Any non-empty extra_inputs payload will raise TypeError when the model is traced.

Update the forward method signature to accept and forward these additional inputs:

Fix
-    def forward(self, input_ids: torch.Tensor | None, past_key_values: tuple):
+    def forward(self, input_ids: torch.Tensor | None, past_key_values: tuple, **extra_inputs):
         """Forward pass."""
         # Convert tuple cache to DynamicCache for models that require it (e.g., Qwen3)
         cache = DynamicCache(config=self.config)
         cache.key_cache = [kv[0] for kv in past_key_values]
         cache.value_cache = [kv[1] for kv in past_key_values]
         past_key_values = cache
 
-        outputs = self.model(input_ids=input_ids, past_key_values=past_key_values, use_cache=True)
+        outputs = self.model(
+            input_ids=input_ids,
+            past_key_values=past_key_values,
+            use_cache=True,
+            **extra_inputs,
+        )
         hidden_states = outputs[0]
         past_key_values = outputs.past_key_values.to_legacy_cache()
         logits = self.lm_head(hidden_states)
         return logits, past_key_values
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelopt/onnx/llm_export_utils/export_utils.py` around lines 79 - 91, The
forward method signature on the export wrapper (forward) doesn't accept the
extra_inputs payload passed by llm_to_onnx via torch.onnx.export, causing
TypeError when exporting VL models; update forward(self, input_ids: torch.Tensor
| None, past_key_values: tuple) to accept **extra_inputs (or a named
extra_inputs: dict = None) and pass those through into self.model(...) and any
downstream uses (i.e., include extra_inputs when calling self.model and ensure
lm_head/logit generation remains unchanged); keep the DynamicCache conversion
logic (DynamicCache, cache.key_cache/value_cache) and return logits,
past_key_values as before so tracing works for vision-language exports.



def llm_to_onnx(model, output_dir, extra_inputs={}, extra_dyn_axes={}):

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.

⚠️ Potential issue | 🟡 Minor

Mutable default argument is a Python antipattern.

Using {} as a default value for extra_inputs and extra_dyn_axes can lead to subtle bugs since the same dict object is shared across all calls. Use None and initialize inside the function.

🐛 Suggested fix
-def llm_to_onnx(model, output_dir, extra_inputs={}, extra_dyn_axes={}):
+def llm_to_onnx(model, output_dir, extra_inputs=None, extra_dyn_axes=None):
     """Export the WrapperModelForCausalLM to ONNX with fixed I/O names and shape definitions and save to `output_dir`.

     Parameters:
         model: torch.Module
         output_dir: str, the output_dir of the original ONNX.
         extra_inputs: dict, append additional inputs after kv_cache. Usually for VL models
         extra_dyn_axes: dict. Usually for VL models
     """
+    if extra_inputs is None:
+        extra_inputs = {}
+    if extra_dyn_axes is None:
+        extra_dyn_axes = {}
     start_time = time.time()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelopt/onnx/llm_export_utils/export_utils.py` at line 94, The function
llm_to_onnx uses mutable default arguments extra_inputs={} and extra_dyn_axes={}
which can cause shared-state bugs; change the signature to use None for those
defaults and inside llm_to_onnx initialize them with empty dicts (e.g., if
extra_inputs is None: extra_inputs = {} and similarly for extra_dyn_axes) so
each call gets a fresh mapping while preserving current behavior.

"""Export the WrapperModelForCausalLM to ONNX with fixed I/O names and shape definitions and save to `output_dir`.

Parameters:
model: torch.Module
output_dir: str, the output_dir of the original ONNX.
extra_inputs: dict, append additional inputs after kv_cache. Usually for VL models
extra_dyn_axes: dict. Usually for VL models

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.

Mutable default arguments (extra_inputs={}, extra_dyn_axes={}). Since this is a deprecated compat shim restored from history, not blocking, but worth noting — the canonical fix is None + if extra_inputs is None: extra_inputs = {}.

"""
start_time = time.time()
config = model.config
num_layers = config.num_hidden_layers
num_attention_heads = config.num_attention_heads
num_key_value_heads = config.num_key_value_heads
hidden_size = config.hidden_size
hidden_size_per_layer = hidden_size // num_attention_heads

dummy_bs = 1
dummy_len = 10
dummy_input_ids = torch.randint(100, (dummy_bs, dummy_len), dtype=torch.int64).cuda()
input_names = ["input_ids"]
output_names = ["logits"]
dynamic_axes = {"input_ids": {0: "batch_size", 1: "seq_len"}}
dummy_kv_cache = ()
for i in range(num_layers):
dummy_k = torch.rand(
(dummy_bs, num_key_value_heads, dummy_len, hidden_size_per_layer), dtype=torch.float16
).cuda()
dummy_v = torch.rand(
(dummy_bs, num_key_value_heads, dummy_len, hidden_size_per_layer), dtype=torch.float16
).cuda()
dummy_kv_cache = (*dummy_kv_cache, (dummy_k, dummy_v))
input_names.extend([f"past_key_values.{i}.key", f"past_key_values.{i}.value"])
output_names.extend([f"present_key_values.{i}.key", f"present_key_values.{i}.value"])
input_dynamic_axes = {0: "batch_size", 2: "past_len"}
dynamic_axes[f"past_key_values.{i}.key"] = input_dynamic_axes
dynamic_axes[f"past_key_values.{i}.value"] = input_dynamic_axes

torch_to_onnx(
model,
(dummy_input_ids, {"past_key_values": dummy_kv_cache, **extra_inputs}),
output_dir,
"model.onnx",
input_names=input_names + list(extra_inputs.keys()),
output_names=output_names,
dynamic_axes=dynamic_axes | extra_dyn_axes,
Comment on lines +114 to +139

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

fd -t f "export_utils.py" | head -5

Repository: NVIDIA/Model-Optimizer

Length of output: 165


🏁 Script executed:

if git ls-files --error-unmatch modelopt/onnx/llm_export_utils/export_utils.py 2>/dev/null; then
  echo "File exists"
  wc -l modelopt/onnx/llm_export_utils/export_utils.py
else
  find . -name "export_utils.py" -type f | head -5
fi

Repository: NVIDIA/Model-Optimizer

Length of output: 176


🏁 Script executed:

# Read the relevant section of the file
head -150 modelopt/onnx/llm_export_utils/export_utils.py | tail -60

Repository: NVIDIA/Model-Optimizer

Length of output: 2557


🏁 Script executed:

# Get the exact lines mentioned (114-139)
sed -n '114,145p' modelopt/onnx/llm_export_utils/export_utils.py

Repository: NVIDIA/Model-Optimizer

Length of output: 1462


🏁 Script executed:

# Search for torch.onnx.export usage to understand dynamic_axes patterns
rg "torch\.onnx\.export|dynamic_axes" modelopt/onnx --context 5 | head -80

Repository: NVIDIA/Model-Optimizer

Length of output: 4490


🏁 Script executed:

# Look for tests related to llm_to_onnx
find . -name "*test*" -type f | xargs grep -l "llm_to_onnx\|export_utils" 2>/dev/null | head -5

Repository: NVIDIA/Model-Optimizer

Length of output: 48


🏁 Script executed:

# Check the torch_to_onnx function implementation
sed -n '145,162p' modelopt/onnx/llm_export_utils/export_utils.py

Repository: NVIDIA/Model-Optimizer

Length of output: 604


🏁 Script executed:

# Search for ONNX export tests or documentation in the repo
find . -name "*test*" -type f -name "*.py" | xargs grep -l "dynamic_axes\|torch.onnx.export" 2>/dev/null | head -5

Repository: NVIDIA/Model-Optimizer

Length of output: 167


🏁 Script executed:

# Check if there are any comments or documentation about dynamic_axes
rg "logits|present_key_values" modelopt/onnx --type py -B 2 -A 2 | head -40

Repository: NVIDIA/Model-Optimizer

Length of output: 3000


🌐 Web query:

torch.onnx.export dynamic_axes outputs documentation behavior

💡 Result:

The dynamic_axes parameter in torch.onnx.export specifies which dimensions of inputs and outputs should be dynamic (variable-sized) in the exported ONNX model. It is a dictionary where keys are input_names or output_names (provided via input_names and output_names parameters), and values are either a list of axis indices [e.g., {0}] or a dict mapping axis index to name {0: 'batch_size'}. This makes those dimensions symbolic (e.g., dim_param in ONNX) instead of fixed [1][2][3]. For outputs specifically: Specifying dynamic axes for output names (e.g., dynamic_axes={'output': {0: 'batch'}}) marks the corresponding output tensor dimensions as dynamic in the ONNX graph, allowing the runtime to handle variable output sizes along those axes, provided the model computation supports it (e.g., batch size matching input) [4][5]. The exporter infers output shapes from tracing but overrides specified axes to be dynamic [3]. Legacy exporter (dynamo=False): Fully supports dynamic_axes for both inputs/outputs; custom names are preserved on outputs (e.g., "sum_dynamic_axes_1") [6][7]. New exporter (dynamo=True, default since ~2.9): dynamic_axes is converted to dynamic_shapes internally, but only applied to inputs (skips if key in output_names) [8]. Output dynamic axes may use auto-generated names like "s77" or require dynamic_shapes directly [7][9]. For full dynamic support, prefer dynamic_shapes [10]. Examples: dynamic_axes = { 'input': {0: 'batch_size'}, 'output': {0: 'batch_size'} # Makes output batch dynamic [5] } torch.onnx.export(model, x, 'model.onnx', input_names=['input'], output_names=['output'], dynamic_axes=dynamic_axes) This produces ONNX with dynamic batch dim on both [3]. Behavior notes: Requires matching input_names/output_names. Some ops (e.g., InstanceNorm with track_running_stats=True) fail with dynamic batch [11]. For cyclic inference or complex dynamics, verify runtime outputs match PyTorch [12].

Citations:


Add missing output dimensions to dynamic_axes to prevent baking dummy shapes into the exported ONNX model.

The logits and present_key_values.* outputs are not specified in the dynamic_axes dictionary, so their dimensions will be fixed to the dummy sizes (batch_size=1, seq_len=10) during export. This prevents the exported ONNX model from handling variable-length outputs at runtime.

🛠️ Suggested fix
     input_names = ["input_ids"]
     output_names = ["logits"]
-    dynamic_axes = {"input_ids": {0: "batch_size", 1: "seq_len"}}
+    dynamic_axes = {
+        "input_ids": {0: "batch_size", 1: "seq_len"},
+        "logits": {0: "batch_size", 1: "seq_len"},
+    }
     dummy_kv_cache = ()
     for i in range(num_layers):
         dummy_k = torch.rand(
             (dummy_bs, num_key_value_heads, dummy_len, hidden_size_per_layer), dtype=torch.float16
         ).cuda()
@@
         output_names.extend([f"present_key_values.{i}.key", f"present_key_values.{i}.value"])
         input_dynamic_axes = {0: "batch_size", 2: "past_len"}
         dynamic_axes[f"past_key_values.{i}.key"] = input_dynamic_axes
         dynamic_axes[f"past_key_values.{i}.value"] = input_dynamic_axes
+        output_dynamic_axes = {0: "batch_size", 2: "total_seq_len"}
+        dynamic_axes[f"present_key_values.{i}.key"] = output_dynamic_axes
+        dynamic_axes[f"present_key_values.{i}.value"] = output_dynamic_axes
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelopt/onnx/llm_export_utils/export_utils.py` around lines 114 - 139, The
export currently only sets dynamic axes for inputs and past_key_values but omits
output dimensions, which bakes dummy sizes into the ONNX; update the
dynamic_axes dict before calling torch_to_onnx to include variable axes for the
model outputs: add dynamic_axes["logits"] = {0: "batch_size", 1: "seq_len"} and
for each layer i add dynamic_axes[f"present_key_values.{i}.key"] = {0:
"batch_size", 2: "seq_len"} and dynamic_axes[f"present_key_values.{i}.value"] =
{0: "batch_size", 2: "seq_len"} so logits and present_key_values outputs are
exported with dynamic batch/sequence dimensions (the relevant symbols are
dynamic_axes, output_names, logits, present_key_values.*, and the torch_to_onnx
call).

)

end_time = time.time()
print(
f"Native ONNX Export from torch completed in {end_time - start_time}s. ONNX file is saved to {output_dir}."
)


def torch_to_onnx(model, inputs, onnx_dir, onnx_name, input_names, output_names, dynamic_axes):
"""Export the model to ONNX."""
os.makedirs(onnx_dir, exist_ok=True)
with torch.inference_mode():
torch.onnx.export(
model,
inputs,
f"{onnx_dir}/{onnx_name}",
input_names=input_names,
output_names=output_names,
dynamic_axes=dynamic_axes,
opset_version=19,
do_constant_folding=True,
dynamo=False,
)
146 changes: 146 additions & 0 deletions modelopt/onnx/llm_export_utils/quantization_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# SPDX-FileCopyrightText: Copyright (c) 2023-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.

"""Quantization utilities for LLM models."""

import copy
import time

import modelopt.torch.quantization as mtq
from modelopt.torch.utils.dataset_utils import get_dataset_dataloader


def _quantize_model(model, quant_config, calib_dataloader=None):
"""The calibration loop for the model can be setup using the modelopt API.

Example usage:
from modelopt.torch.utils.dataset_utils import create_forward_loop
model = ... # Initialize the model
tokenizer = ... # Initialize the tokenizer
quant_cfg = ... # Setup quantization configuration
forward_loop = create_forward_loop(model=model, dataset_name="cnn_dailymail", tokenizer=tokenizer)
mtq.quantize(model, quant_cfg, forward_loop=forward_loop)
"""

def calibrate_loop(model):
"""Adjusts weights and scaling factors based on selected algorithms."""
for idx, data in enumerate(calib_dataloader):
if idx % 10 == 0:
print(f"Calibrating batch {idx}...")
if isinstance(data, dict):
data = {k: v.to(model.device) for k, v in data.items()}
model(**data)
else:
data = data.to(model.device)
model(data)

print("Starting quantization...")
start_time = time.time()
mtq.quantize(model, quant_config, forward_loop=calibrate_loop)
end_time = time.time()
print(f"Quantization finishes in {end_time - start_time}s.")

return model


def get_quant_config(precision, lm_head_precision="fp16"):
"""Get the quantization configuration."""
if precision == "fp8":
quant_cfg = copy.deepcopy(mtq.FP8_DEFAULT_CFG)

elif precision == "nvfp4":
quant_cfg = copy.deepcopy(mtq.NVFP4_DEFAULT_CFG)

elif precision == "int4_awq":
quant_cfg = copy.deepcopy(mtq.INT4_AWQ_CFG) # type: ignore[arg-type]

else:
raise ValueError(f"Unsupported precision: {precision}")

quant_cfg_list: list = [
e for e in quant_cfg["quant_cfg"] if isinstance(e, dict) and "quantizer_name" in e
]

if lm_head_precision == "fp8":
quant_cfg_list.append(
{
"quantizer_name": "*lm_head.input_quantizer",
"cfg": {"num_bits": (4, 3), "axis": None},
}
)
quant_cfg_list.append(
{
"quantizer_name": "*lm_head.weight_quantizer",
"cfg": {"num_bits": (4, 3), "axis": None},
}
)
elif lm_head_precision == "nvfp4":
quant_cfg_list.append(
{
"quantizer_name": "*lm_head.input_quantizer",
"cfg": {
"num_bits": (2, 1),
"block_sizes": {-1: 16, "type": "dynamic", "scale_bits": (4, 3)},
"axis": None,
},
"enable": True,
}
)
quant_cfg_list.append(
{
"quantizer_name": "*lm_head.weight_quantizer",
"cfg": {
"num_bits": (2, 1),
"block_sizes": {-1: 16, "type": "dynamic", "scale_bits": (4, 3)},
"axis": None,
},
"enable": True,
}
)
quant_cfg["quant_cfg"] = quant_cfg_list
return quant_cfg


def quantize(
model, tokenizer, precision, lm_head_precision="fp16", dataset_dir=None, calib_size=512
):
"""Quantize the PyTorch model to fp8 or int4_awq."""
assert precision in [
"fp8",
"int4_awq",
"nvfp4",
], (
f"Only fp8(W8A8), int4_awq(W4A16), nvfp4(W4A4) is supported. You passed an unsupported precision: {precision}."
)

assert lm_head_precision in ["fp16"], (
f"Only fp16(unquantized) is supported for lm_head. You passed an unsupported precision: {lm_head_precision}."
)

if tokenizer.pad_token != "<unk>": # nosec B105
tokenizer.pad_token = tokenizer.eos_token
Comment thread
kevalmorabia97 marked this conversation as resolved.
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
if not dataset_dir:
dataset_dir = "cnn_dailymail"

batch_size = 1
data_loader = get_dataset_dataloader(
dataset_name=dataset_dir, tokenizer=tokenizer, batch_size=batch_size, num_samples=calib_size
)
quant_config = get_quant_config(precision, lm_head_precision)
quantized_model = _quantize_model(model, quant_config, data_loader)
mtq.print_quant_summary(quantized_model)
return quantized_model
Loading
Loading