# # SPDX-FileCopyrightText: Copyright (c) 1993-2023 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. # """tests of tensor quantization function and module""" import contextlib import pytest import numpy as np import torch from torch.nn.parameter import Parameter from pytorch_quantization import cuda_ext from pytorch_quantization import tensor_quant import pytorch_quantization.utils as quant_utils import tests.utils as test_utils from tests.fixtures import verbose np.random.seed(123456) # seed 1234 causes 1 number mismatch at 6th decimal in one of the tests # pylint:disable=missing-docstring, no-self-use class TestTensorQuant(): def test_simple_run(self): """ quantizer passes gradcheck """ x = Parameter(torch.randn(2, 3, dtype=torch.float64).cuda()) * 100 tensor_quant.tensor_quant(x, torch.max(torch.abs(x)), 7) def test_per_tensor_scale(self): """ tensor_quant matches numpy quantization """ torch.set_default_tensor_type('torch.cuda.FloatTensor') # Test on GPU x_np = np.random.rand(1023) x_torch = torch.Tensor(x_np) quant_x_np = test_utils.quant_np(x_np, np.max(np.abs(x_np))) quant_x_torch, _ = tensor_quant.tensor_quant(x_torch, torch.max(torch.abs(x_torch))) np.testing.assert_array_equal(quant_x_torch.cpu().numpy(), quant_x_np) torch.set_default_tensor_type('torch.FloatTensor') def test_per_channel_scale(self): """ fake_tensor_quant performs per channel quantization """ x_np = np.random.rand(15, 15, 64, 128).astype('float32') x_torch = torch.Tensor(x_np).cuda() # Pytorch filter layout seems to be KCRS, reduce max to shape [K, 1, 1, 1] to test per channel scale # Shrink max a little, so that clip behavior is tested amax_x_np = 0.7 * np.max(np.abs(x_np), axis=(1, 2, 3), keepdims=True) # Pytorch's max function doesn't support reduces multiple axis, and returns (max, argmax) tuple, # so it has to be reduced by multiple torch.max amax_x_torch = 0.7 * torch.max( torch.max(torch.max(x_torch, dim=1, keepdim=True)[0], dim=2, keepdim=True)[0], dim=3, keepdim=True)[0] quant_x_np = test_utils.quant_np(x_np, amax_x_np) quant_x_torch, _ = tensor_quant.tensor_quant(x_torch, amax_x_torch) # np.testing.assert_array_equal(quant_x_torch.cpu().numpy(), quant_x_np) # Pytorch numerics is not the same as numpy, it will be off by 1 np.testing.assert_array_less(np.abs(quant_x_torch.cpu().numpy() - quant_x_np), 2) if verbose: mismatches = np.where(np.abs(quant_x_torch.cpu().numpy() - quant_x_np) >= 1) print("Mismatches:") print(" Original: ", x_np[mismatches]) print(" numpy: ", quant_x_np[mismatches]) print(" Pytorch: ", quant_x_torch.cpu().numpy()[mismatches]) def test_backward(self): """ tensor_quant implements straight through estimator on the backward pass Note: this does not work for integer output_dtype """ x = torch.randn(3, 7, requires_grad=True).cuda() labels = torch.randint(6, (3,)).type(torch.LongTensor).cuda() quant_x, _ = tensor_quant.tensor_quant(x, x.abs().max(), 7) float_quant_x = quant_x.type(torch.FloatTensor).cuda() x.retain_grad() float_quant_x.retain_grad() criterion = torch.nn.CrossEntropyLoss().cuda() loss = criterion(float_quant_x, labels) loss.backward() np.testing.assert_array_equal(float_quant_x.grad.cpu().numpy(), x.grad.cpu().numpy()) def test_unsigned(self): x_np = np.random.rand(1023).astype('float32') x_torch = torch.Tensor(x_np) quant_x_np = test_utils.quant_np(x_np, np.max(np.abs(x_np)), num_bits=9, fake=False) quant_x_torch, _ = tensor_quant.tensor_quant(x_torch, torch.max(torch.abs(x_torch)), 8, True) np.testing.assert_array_almost_equal(quant_x_torch.cpu().numpy(), quant_x_np) x_torch = torch.randn(3, 7) with pytest.raises(TypeError, match="Negative values encountered"): tensor_quant.tensor_quant(x_torch, torch.max(torch.abs(x_torch)), 8, True) def test_overflow_fp16(self): x_torch = torch.randn(1023).cuda().half() with pytest.raises(ValueError, match="scale is too large for FP16"): quant_x_torch, scale = tensor_quant.tensor_quant(x_torch, torch.tensor(1e-4).cuda().half(), 8, False) def test_clip_gradient(self): x = torch.randn(3, 7, requires_grad=True).cuda() x.retain_grad() amax = x.abs().max() / 2 x_in_range = (-amax <= x) * (x <= amax) quant_x, _ = tensor_quant.tensor_quant(x, amax, 8) loss = torch.sum((quant_x - 0.5)**2) loss.backward() np.testing.assert_array_equal(x.grad.cpu().numpy() != 0, x_in_range.cpu().numpy()) def test_full_range(self): """ fake_tensor_quant uses the full integer range when narrow=False """ x_np = np.random.rand(1023).astype('float32') x_torch = torch.Tensor(x_np).cuda() amax = np.max(np.abs(x_np)) quant_x_np = test_utils.quant_np(x_np, amax, num_bits=9, fake=False, narrow_range=False) quant_x_torch, _ = tensor_quant.tensor_quant(x_torch, torch.max(torch.abs(x_torch)), 8, True, False) np.testing.assert_array_almost_equal(quant_x_torch.cpu().numpy(), quant_x_np) class TestFakeTensorQuant(): def test_simple_run(self): x = Parameter(torch.randn(3, 7).cuda()) tensor_quant.fake_tensor_quant(x, torch.max(torch.abs(x))) def test_per_tensor_scale(self): """ fake_tensor_quant matches numpy quantization """ x_np = np.random.rand(13).astype('float32') print(x_np) x_torch = torch.Tensor(x_np).cuda() quant_x_np = test_utils.quant_np(x_np, np.max(np.abs(x_np)), fake=True) quant_x_torch = tensor_quant.fake_tensor_quant(x_torch, torch.max(torch.abs(x_torch))) np.testing.assert_array_almost_equal(quant_x_torch.cpu().numpy(), quant_x_np) def test_per_channel_scale(self): """ fake_tensor_quant performs per channel quantization """ x_np = np.random.rand(15, 15, 64, 128).astype('float32') x_torch = torch.Tensor(x_np).cuda() # Pytorch filter layout seems to be KCRS, reduce max to shape [K, 1, 1, 1] to test per channel scale # Shrink max a little, so that clip behavior is tested amax_x_np = 0.9 * np.max(np.abs(x_np), axis=(1, 2, 3), keepdims=True) # Pytorch's max function doesn't support reduces multiple axis, and returns (max, argmax) tuple, # so it has to be reduced by multiple torch.max amax_x_torch = 0.9 * torch.max( torch.max(torch.max(x_torch, dim=1, keepdim=True)[0], dim=2, keepdim=True)[0], dim=3, keepdim=True)[0] quant_x_np = test_utils.quant_np(x_np, amax_x_np, fake=True) quant_x_torch = tensor_quant.fake_tensor_quant(x_torch, amax_x_torch) # Pytorch numerics is not the same as numpy, results will be off a little # np.testing.assert_array_equal(quant_x_torch.cpu().numpy(), quant_x_np) np.testing.assert_array_almost_equal(quant_x_torch.cpu().numpy(), quant_x_np, decimal=2) if verbose: mismatches = np.where(np.abs(quant_x_torch.cpu().numpy() - quant_x_np) >= 1e-5) print("Mismatches:") print(" Original: ", x_np[mismatches]) print(" numpy: ", quant_x_np[mismatches]) print(" Pytorch: ", quant_x_torch.cpu().numpy()[mismatches]) def test_backward(self): """ fake_tensor_quant implements straight through estimator on the backward pass """ x = torch.randn(3, 7, requires_grad=True).cuda() labels = torch.randint(6, (3,)).type(torch.LongTensor).cuda() quant_x = tensor_quant.fake_tensor_quant(x, torch.max(torch.abs(x)), 7) x.retain_grad() quant_x.retain_grad() criterion = torch.nn.CrossEntropyLoss().cuda() loss = criterion(quant_x, labels) loss.backward() np.testing.assert_array_equal(quant_x.grad.cpu().numpy(), x.grad.cpu().numpy()) def test_unsigned(self): x_np = np.random.rand(1023).astype('float32') x_torch = torch.Tensor(x_np).cuda() quant_x_np = test_utils.quant_np(x_np, np.max(np.abs(x_np)), num_bits=9, fake=True) quant_x_torch = tensor_quant.fake_tensor_quant(x_torch, torch.max(torch.abs(x_torch)), 8, True) np.testing.assert_array_almost_equal(quant_x_torch.cpu().numpy(), quant_x_np) def test_cuda_ext(self): x_np = np.random.rand(1023).astype('float32') x_torch = torch.Tensor(x_np).cuda() for num_bits in [3, 4, 5, 7, 8, 11]: for unsigned in [True, False]: test_utils.compare(cuda_ext.fake_tensor_quant(x_torch, torch.max(torch.abs(x_torch)), num_bits, unsigned), tensor_quant.fake_tensor_quant(x_torch, torch.max(torch.abs(x_torch)), num_bits, unsigned), rtol=0, atol=0) # Test fp16 and bf16 for dtype in [torch.float16, torch.bfloat16]: x_np = np.random.rand(1023) x_torch = torch.Tensor(x_np).cuda().to(dtype) cuda_ext_out = cuda_ext.fake_tensor_quant(x_torch, torch.max(torch.abs(x_torch))).to(torch.float32) pytorch_out = tensor_quant.fake_tensor_quant(x_torch, torch.max(torch.abs(x_torch))).to(torch.float32) test_utils.compare(cuda_ext_out, pytorch_out, rtol=0, atol=0) @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16]) def test_cuda_ext_with_axis(self, dtype): x_np = np.random.rand(3, 4, 5, 6) x_torch = torch.Tensor(x_np).cuda().to(dtype) # amax along axis 1 amax_torch = torch.tensor([0.8, 0.9, 0.7, 0.6], device="cuda") for num_bits in [3, 4, 5, 7, 8, 11]: for unsigned in [True, False]: cuda_ext_out = cuda_ext.fake_tensor_quant_with_axis(x_torch, amax_torch, 1, num_bits, unsigned).to(torch.float32) pytorch_out = tensor_quant.fake_tensor_quant(x_torch, amax_torch.view(1, -1, 1, 1), num_bits, unsigned).to(torch.float32) test_utils.compare(cuda_ext_out, pytorch_out, rtol=0, atol=0) def test_cuda_ext_inplace(self): x_np = np.random.rand(1023).astype('float32') x_torch = torch.Tensor(x_np).cuda() quant_x_np = test_utils.quant_np(x_np, np.max(np.abs(x_np)), fake=True) cuda_ext.fake_tensor_quant_(x_torch, torch.max(torch.abs(x_torch))) np.testing.assert_array_equal(x_torch.cpu().numpy(), quant_x_np) # Test fp16 and bf16 for dtype in [torch.float16, torch.bfloat16]: x_np = np.random.rand(1023) x_torch = torch.Tensor(x_np).cuda().to(dtype) quant_x_np = test_utils.quant_np(x_np, np.max(np.abs(x_np)), fake=True) cuda_ext.fake_tensor_quant_(x_torch, torch.max(torch.abs(x_torch))) x_torch = x_torch.to(torch.float32) np.testing.assert_array_almost_equal(x_torch.cpu().numpy(), quant_x_np, decimal=2) def test_cuda_ext_tiny_amax(self): x_torch = torch.rand(2, 3, 4, device="cuda") amax = torch.tensor([1., 1.e-26, 1.], device="cuda").unsqueeze(-1).unsqueeze(1) quant_x = cuda_ext.fake_tensor_quant_with_axis(x_torch, amax, axis=1) assert quant_x[:, 1, :].sum() == 0 def test_overflow_fp16(self): x_torch = torch.randn(1023).cuda().half() quant_x_torch = tensor_quant.fake_tensor_quant(x_torch, torch.tensor(1e-4).cuda().half(), 8, False) assert not (torch.isinf(quant_x_torch).any() or torch.isnan(quant_x_torch).any()) def test_clip_gradient(self): x = torch.randn(3, 7, requires_grad=True).cuda() x.retain_grad() amax = x.abs().max() / 2 x_in_range = (-amax <= x) * (x <= amax) quant_x = tensor_quant.fake_tensor_quant(x, amax, 8) loss = torch.sum((quant_x - 0.5)**2) loss.backward() np.testing.assert_array_equal(x.grad.cpu().numpy() != 0, x_in_range.cpu().numpy()) def test_full_range(self): """ fake_tensor_quant uses the full integer range when narrow=False """ x_np = np.random.rand(1023).astype('float32') x_torch = torch.Tensor(x_np).cuda() amax = np.max(np.abs(x_np)) quant_x_np = test_utils.quant_np(x_np, amax, num_bits=9, fake=True, narrow_range=False) quant_x_torch = tensor_quant.fake_tensor_quant(x_torch, torch.max(torch.abs(x_torch)), 8, True, False) np.testing.assert_array_almost_equal(quant_x_torch.cpu().numpy(), quant_x_np) @pytest.mark.parametrize("dtype", ["float32", "float16"]) def test_against_legacy(self, dtype): x_np = np.random.rand(3, 4, 5, 6).astype(dtype) x_torch = torch.Tensor(x_np).cuda() amax_torch = torch.tensor(0.7, device="cuda") for num_bits in [3, 4, 5, 7, 8, 11]: for unsigned in [True, False]: legacy_out = tensor_quant.legacy_fake_tensor_quant(x_torch, amax_torch, num_bits, unsigned) test_out = tensor_quant.fake_tensor_quant(x_torch, amax_torch, num_bits, unsigned) test_utils.compare(legacy_out, test_out, rtol=0, atol=0) def test_against_legacy_noncontiguous(self): x_np = np.random.rand(3, 4, 5, 6) x_torch = torch.Tensor(x_np).cuda() amax_torch = torch.tensor(0.7, device="cuda") x_torch_noncontiguous = x_torch[:, 2, :, 3] assert not x_torch_noncontiguous.is_contiguous() legacy_out = tensor_quant.legacy_fake_tensor_quant(x_torch_noncontiguous, amax_torch) test_out = tensor_quant.fake_tensor_quant(x_torch_noncontiguous, amax_torch) test_utils.compare(legacy_out, test_out, rtol=0, atol=0) @pytest.mark.parametrize("dtype", ["float32", "float16"]) def test_against_legacy_with_axis(self, dtype): x_np = np.random.rand(3, 4, 5, 6).astype(dtype) x_torch = torch.Tensor(x_np).cuda() # amax along axis 1 amax_torch = torch.tensor([0.8, 0.9, 0.7, 0.6], device="cuda").view(1, -1, 1, 1) for num_bits in [3, 4, 5, 7, 8, 11]: for unsigned in [True, False]: legacy_out = tensor_quant.legacy_fake_tensor_quant(x_torch, amax_torch, num_bits, unsigned) test_out = tensor_quant.fake_tensor_quant(x_torch, amax_torch, num_bits, unsigned) test_utils.compare(legacy_out, test_out, rtol=0, atol=0) class TestQuantDescriptor(): def test_scaled_mode(self): num_bits = np.random.randint(0, 16) test_quant_desc = tensor_quant.QuantDescriptor(num_bits=num_bits) assert test_quant_desc.num_bits == num_bits assert test_quant_desc.axis is None assert test_quant_desc.amax is None assert not test_quant_desc.learn_amax axis = (0, 1, 3) test_quant_desc = tensor_quant.QuantDescriptor(axis=axis) assert test_quant_desc.num_bits == 8 # default value assert test_quant_desc.axis == axis assert test_quant_desc.amax is None amax = 0.7 test_quant_desc = tensor_quant.QuantDescriptor(amax=amax, unsigned=True) assert test_quant_desc.axis is None assert test_quant_desc.amax == np.float32(amax) assert test_quant_desc.unsigned amax = 0.7 test_quant_desc = tensor_quant.QuantDescriptor(amax=amax, learn_amax=True) assert test_quant_desc.amax == np.float32(amax) assert test_quant_desc.learn_amax # Test the print string once if verbose is set. if verbose: print(test_quant_desc) with pytest.raises(TypeError, match="must be float, list or ndarray"): tensor_quant.QuantDescriptor(amax='oops') with pytest.raises(TypeError, match="amax must be float, list or ndarray"): tensor_quant.QuantDescriptor(amax='oops', learn_amax=True) with pytest.raises(TypeError, match="axis is ignored and must be None"): tensor_quant.QuantDescriptor(axis=(1, 2), amax=0.7, learn_amax=True) def test_amax(self): test_quant_desc = tensor_quant.QuantDescriptor() assert test_quant_desc.amax is None test_quant_desc = tensor_quant.QuantDescriptor(amax=1.2) assert isinstance(test_quant_desc.amax, np.ndarray) np.testing.assert_array_equal(test_quant_desc.amax, np.float32(1.2)) test_quant_desc = tensor_quant.QuantDescriptor(amax=[1.3, 1.4]) assert isinstance(test_quant_desc.amax, np.ndarray) np.testing.assert_array_equal(test_quant_desc.amax, np.float32([1.3, 1.4])) with pytest.raises(TypeError, match="must be float, list or ndarray"): tensor_quant.QuantDescriptor(amax='oops') def test_from_to_dict(self): quant_desc_1 = tensor_quant.QuantDescriptor(num_bits=2, name='a', fake_quant=True, axis=(1, 2), amax=3.1415926536) quant_desc_2 = tensor_quant.QuantDescriptor(**quant_desc_1.dict()) if verbose: print(quant_desc_1.dict()) assert quant_desc_1 == quant_desc_2 quant_desc_1 = tensor_quant.QuantDescriptor(num_bits=2, amax=0.1, unsigned=True) quant_desc_2 = tensor_quant.QuantDescriptor(**quant_desc_1.dict()) assert quant_desc_1 == quant_desc_2 def test_from_to_yaml(self): quant_desc_1 = tensor_quant.QuantDescriptor(num_bits=2, name='a', fake_quant=True, axis=(1, 2), amax=3.1415926536) quant_desc_2 = tensor_quant.QuantDescriptor.from_yaml(quant_desc_1.to_yaml()) if verbose: print(quant_desc_1.to_yaml()) assert quant_desc_1 == quant_desc_2 quant_desc_1 = tensor_quant.QuantDescriptor(num_bits=2, amax=0.1) quant_desc_2 = tensor_quant.QuantDescriptor.from_yaml(quant_desc_1.to_yaml()) assert quant_desc_1 == quant_desc_2 class TestFakeAffineTensorQuant(): def test_simple_run(self, verbose): x = np.array([-1., -13., -101., -128., 0., 2., 5., 13., 93., 111., 127.], dtype=np.float32) torch_x = torch.tensor(x).cuda() quant_x = tensor_quant.fake_affine_tensor_quant(torch_x, torch.min(torch_x), torch.max(torch_x)) if verbose: print(quant_x) np.testing.assert_array_almost_equal(quant_x.cpu().numpy(), x) def test_clip_gradient(self): x = torch.randn(3, 7, requires_grad=True).cuda() x.retain_grad() xmin = x.min() / 2 xmax = x.max() / 2 x_in_range = (xmin <= x) * (x <= xmax) quant_x = tensor_quant.fake_affine_tensor_quant(x, xmin, xmax, 8) loss = torch.sum((quant_x - 0.5)**2) loss.backward() np.testing.assert_array_equal(x.grad.cpu().numpy() != 0, x_in_range.cpu().numpy()) class TestScaledE4M3(): x = [[-2.0000, -1.8000, -1.6000, -1.4000, -1.2000], [-1.0000, -0.8000, -0.6000, -0.4000, -0.2000], [-0.0000, 0.2000, 0.4000, 0.6000, 0.8000], [1.0000, 1.2000, 1.4000, 1.6000, 1.8000]] xq_unscaled = [[-2.0000, -1.7500, -1.6250, -1.3750, -1.2500], [-1.0000, -0.8125, -0.6250, -0.4062, -0.2031], [0.0000, 0.2031, 0.4062, 0.6250, 0.8125], [1.0000, 1.2500, 1.3750, 1.6250, 1.7500]] xq_scaled = [[-2.0000, -1.8571, -1.5714, -1.4286, -1.1429], [-1.0000, -0.7857, -0.5714, -0.3929, -0.1964], [0.0000, 0.1964, 0.3929, 0.5714, 0.7857], [1.0000, 1.1429, 1.4286, 1.5714, 1.8571]] def test_e4m3_no_scale(self): x = torch.tensor(TestScaledE4M3.x, device="cuda") xq_ref = torch.tensor(TestScaledE4M3.xq_unscaled, device="cuda") e4m3_x = tensor_quant.scaled_e4m3(x, None) test_utils.compare(e4m3_x, xq_ref, atol=1e-4, rtol=1e-4) def test_e4m3_no_cpu(self): x = torch.tensor(TestScaledE4M3.x) xq_ref = torch.tensor(TestScaledE4M3.xq_unscaled) e4m3_x = tensor_quant.scaled_e4m3(x, None) test_utils.compare(e4m3_x, xq_ref, atol=1e-4, rtol=1e-4) def test_with_amax(self): x = torch.tensor(TestScaledE4M3.x, device="cuda").unsqueeze(-1) xq_ref = torch.tensor(TestScaledE4M3.xq_scaled, device="cuda").unsqueeze(-1) amax = quant_utils.reduce_amax(x, axis=None, keepdims=True) e4m3_x = tensor_quant.scaled_e4m3(x, amax) test_utils.compare(e4m3_x, xq_ref, atol=1e-4, rtol=1e-4) def test_e4m3_incontiguous(self): x = torch.tensor(TestScaledE4M3.x, device="cuda").transpose(1, 0) xq_ref = torch.tensor(TestScaledE4M3.xq_unscaled, device="cuda").transpose(1, 0) assert not x.is_contiguous() e4m3_x = tensor_quant.scaled_e4m3(x, None) test_utils.compare(e4m3_x, xq_ref, atol=1e-4, rtol=1e-4) def test_backward(self): x = torch.randn(3, 7, requires_grad=True).cuda() labels = torch.randint(6, (3,)).type(torch.LongTensor).cuda() quant_x = tensor_quant.scaled_e4m3(x, None) x.retain_grad() quant_x.retain_grad() criterion = torch.nn.CrossEntropyLoss().cuda() loss = criterion(quant_x, labels) loss.backward() np.testing.assert_array_equal(quant_x.grad.cpu().numpy(), x.grad.cpu().numpy())