mirror of
https://github.com/NVIDIA/Model-Optimizer.git
synced 2026-10-26 06:54:04 +08:00
208 lines
8.0 KiB
Python
208 lines
8.0 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.
|
|
import copy
|
|
|
|
import pytest
|
|
import torch
|
|
import torch.distributed
|
|
import torch.distributed as dist
|
|
from packaging.version import Version
|
|
|
|
import modelopt.torch.opt as mto
|
|
import modelopt.torch.quantization as mtq
|
|
from modelopt.torch.quantization.backends.gemm_registry import enable_real_quant_gemm
|
|
from modelopt.torch.quantization.utils import is_quantized_linear
|
|
from modelopt.torch.utils import torch_to
|
|
|
|
from .checkpointing import format_modelopt_checkpoint_by_version
|
|
|
|
INT4_AWQ_FULL_CFG = mtq.INT4_AWQ_CFG.copy()
|
|
|
|
INT4_AWQ_FULL_CFG["algorithm"] = "awq_full"
|
|
|
|
INT4_AWQ_CLIP_CFG = mtq.INT4_AWQ_CFG.copy()
|
|
INT4_AWQ_CLIP_CFG["algorithm"] = "awq_clip"
|
|
|
|
# SVDQuant test cfg
|
|
INT4_SVDQUANT_CFG = mtq.INT4_AWQ_CFG.copy()
|
|
INT4_SVDQUANT_CFG["algorithm"] = {"method": "svdquant", "lowrank": 8}
|
|
|
|
# SVDQuant test cfg
|
|
FP4_SVDQUANT_CFG = mtq.NVFP4_AWQ_LITE_CFG.copy()
|
|
FP4_SVDQUANT_CFG["algorithm"] = {"method": "svdquant", "lowrank": 8}
|
|
|
|
|
|
def get_awq_config(algorithm="awq_lite", block_size=8):
|
|
config = copy.deepcopy(mtq.INT4_AWQ_CFG)
|
|
config["quant_cfg"]["*weight_quantizer"]["block_sizes"] = {-1: block_size}
|
|
config["algorithm"]["method"] = algorithm
|
|
config["algorithm"]["debug"] = True
|
|
if algorithm == "awq_clip":
|
|
config["algorithm"].pop("alpha_step")
|
|
return config
|
|
|
|
|
|
def quantize_model_and_forward(model, config, calib_data, compress=False):
|
|
def forward_loop(model, run_backward=False):
|
|
for batch in calib_data:
|
|
output = model(batch)
|
|
if run_backward:
|
|
output.sum().backward()
|
|
|
|
model = mtq.quantize(model, config, forward_loop=forward_loop)
|
|
if compress:
|
|
mtq.compress(model)
|
|
|
|
for module in model.modules():
|
|
assert not isinstance(module, torch.nn.Linear) or is_quantized_linear(module)
|
|
|
|
model.train()
|
|
|
|
# make sure we can run the forward and backward on the quantized model
|
|
forward_loop(model, run_backward=True)
|
|
|
|
|
|
def save_restore_test(model_cls, device, quant_config, compress=False, version=None):
|
|
# test restoring to an unquantized model
|
|
model_quant = model_cls().to(device)
|
|
model_ref = model_cls().to(device)
|
|
|
|
calib_data = [model_quant.get_input().to(device) for _ in range(2)]
|
|
quantize_model_and_forward(model_quant.to(device), quant_config, calib_data, compress)
|
|
|
|
state_dict = mto.modelopt_state(model_quant)
|
|
|
|
if version is not None:
|
|
state_dict = format_modelopt_checkpoint_by_version(state_dict, version)
|
|
|
|
mto.restore_from_modelopt_state(model_ref, state_dict)
|
|
model_ref.load_state_dict(model_quant.state_dict())
|
|
assert torch.allclose(model_quant(calib_data[0]), model_ref(calib_data[0]))
|
|
|
|
if version is not None and Version(version) < Version("0.29"):
|
|
# Rest of the tests are not needed for version < 0.29
|
|
return
|
|
|
|
# gpu: test restoring to a model on cpu. If the quantizer states are not initialized correctly,
|
|
# the buffers will be created on cuda and this test will fail
|
|
model_ref = model_cls().to("cpu")
|
|
state_dict = torch_to(state_dict, device="cuda" if torch.cuda.is_available() else "cpu")
|
|
mto.restore_from_modelopt_state(model_ref, state_dict)
|
|
model_ref.load_state_dict(model_quant.state_dict())
|
|
model_ref(calib_data[0].to("cpu")) # make sure all the buffers are created in the right device
|
|
model_ref.to(device)
|
|
assert torch.allclose(model_quant(calib_data[0]), model_ref(calib_data[0]))
|
|
|
|
# Test that smoothquant is restored correctly
|
|
if quant_config == mtq.INT8_SMOOTHQUANT_CFG:
|
|
for name, module in model_ref.named_modules():
|
|
if isinstance(module, torch.nn.Linear):
|
|
assert module.input_quantizer.axis is None
|
|
|
|
# test restoring to a quantized model
|
|
model_ref = model_cls().to(device)
|
|
quantize_model_and_forward(model_ref, mtq.INT4_BLOCKWISE_WEIGHT_ONLY_CFG, calib_data)
|
|
with pytest.raises(AssertionError, match="Model already has modelopt state!"):
|
|
mto.restore_from_modelopt_state(model_ref, state_dict)
|
|
|
|
|
|
def tensor_parallel_test_helper(model, config, tp_group, dp_group):
|
|
# The input to fist layer, the column parallel should be the same across all tp ranks
|
|
calib_data = model.get_dummy_input().cuda()
|
|
dist.all_reduce(calib_data, op=dist.ReduceOp.AVG, group=tp_group)
|
|
|
|
def forward_loop(model):
|
|
model(calib_data)
|
|
|
|
model = mtq.quantize(model, config, forward_loop)
|
|
|
|
# Sanity check
|
|
forward_loop(model)
|
|
|
|
if config in [mtq.INT8_DEFAULT_CFG, mtq.FP8_DEFAULT_CFG, mtq.INT8_SMOOTHQUANT_CFG]:
|
|
# Lets check the amax for row parallel input quantizer; it should be the same across all tp ranks
|
|
activation_amax = model.fc2.input_quantizer.amax.clone()
|
|
dist.all_reduce(activation_amax, op=dist.ReduceOp.MAX, group=tp_group)
|
|
assert torch.allclose(activation_amax, model.fc2.input_quantizer.amax)
|
|
|
|
# Lets check the row parallel weight amax; it should be the same across all tp ranks
|
|
weight_amax = model.fc2.weight_quantizer.amax.clone()
|
|
dist.all_reduce(weight_amax, op=dist.ReduceOp.MAX, group=tp_group)
|
|
assert torch.allclose(weight_amax, model.fc2.weight_quantizer.amax)
|
|
|
|
if config in [mtq.INT8_SMOOTHQUANT_CFG, mtq.INT4_AWQ_CFG, mtq.W4A8_AWQ_BETA_CFG]:
|
|
# Lets check the column parallel pre_quant_scale; it should be the same across all tp ranks
|
|
input_quantizer = model.fc1.input_quantizer
|
|
pre_quant_scale = input_quantizer.pre_quant_scale.clone()
|
|
dist.all_reduce(pre_quant_scale, op=dist.ReduceOp.MAX, group=tp_group)
|
|
assert torch.allclose(pre_quant_scale, input_quantizer.pre_quant_scale)
|
|
|
|
dist.destroy_process_group()
|
|
|
|
|
|
def auto_quantize_helper(model):
|
|
model, search_state = mtq.auto_quantize(
|
|
model,
|
|
constraints={"effective_bits": 8.0},
|
|
quantization_formats=[mtq.INT4_BLOCKWISE_WEIGHT_ONLY_CFG, mtq.INT8_DEFAULT_CFG],
|
|
data_loader=[model.get_dummy_input().cuda() for _ in range(2)],
|
|
forward_step=lambda model, batch: model(batch),
|
|
forward_backward_step=lambda model, batch: model(batch).sum().backward(),
|
|
num_calib_steps=2,
|
|
num_score_steps=2,
|
|
verbose=True,
|
|
)
|
|
search_state_list = [None] * torch.distributed.get_world_size()
|
|
torch.distributed.all_gather_object(search_state_list, search_state)
|
|
|
|
search_state_rank0 = search_state_list[0]
|
|
for search_state in search_state_list[1:]:
|
|
assert search_state == search_state_rank0
|
|
|
|
|
|
def compute_backward_grad(
|
|
model,
|
|
input_tensor,
|
|
config=None,
|
|
quantize: bool = False,
|
|
enable_real_quant: bool = False,
|
|
compress: bool = False,
|
|
):
|
|
if quantize:
|
|
|
|
def forward_loop(model, run_backward=False):
|
|
calib_data = [model.get_input().to(torch.float16).cuda() for _ in range(8)]
|
|
for batch in calib_data:
|
|
output = model(batch)
|
|
if run_backward:
|
|
output.sum().backward()
|
|
|
|
mtq.quantize(model, config, forward_loop)
|
|
|
|
if enable_real_quant:
|
|
enable_real_quant_gemm(model)
|
|
if compress:
|
|
mtq.compress(model)
|
|
output = model(input_tensor).sum()
|
|
model.zero_grad()
|
|
output.backward()
|
|
weight_grads = []
|
|
bias_grads = []
|
|
for layer in model.net:
|
|
if isinstance(layer, torch.nn.Linear):
|
|
weight_grads.append(layer.weight.grad)
|
|
bias_grads.append(layer.bias.grad if layer.bias is not None else None)
|
|
return weight_grads, bias_grads
|