chore: import upstream snapshot with attribution
Docker Image CI / build-ubuntu2004 (push) Waiting to run
Docker Image CI / build-ubuntu2004 (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
[run]
|
||||
omit = pytorch_quantization/utils/amp_wrapper.py, pytorch_quantization/utils/quant_logging.py, *__init__.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
def backward
|
||||
RNNReLUCell
|
||||
RNNTanhCell
|
||||
GRU
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
## copied from PyTorch
|
||||
|
||||
.coverage
|
||||
.mypy_cache
|
||||
*/*.pyc
|
||||
*/*.so*
|
||||
*/**/__pycache__
|
||||
*/**/*.dylib*
|
||||
*/**/*.pyc
|
||||
*/**/*.pyd
|
||||
*/**/*.so*
|
||||
*/**/**/*.pyc
|
||||
*/**/**/**/*.pyc
|
||||
*/**/**/**/**/*.pyc
|
||||
aten/build/
|
||||
aten/src/ATen/Config.h
|
||||
aten/src/ATen/cuda/CUDAConfig.h
|
||||
build/
|
||||
dist/
|
||||
|
||||
|
||||
# IPython notebook checkpoints
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Editor temporaries
|
||||
*.swn
|
||||
*.swo
|
||||
*.swp
|
||||
*.swm
|
||||
*~
|
||||
|
||||
# macOS dir files
|
||||
.DS_Store
|
||||
|
||||
# Symbolic files
|
||||
tools/shared/cwrap_common.py
|
||||
|
||||
# Ninja files
|
||||
.ninja_deps
|
||||
.ninja_log
|
||||
compile_commands.json
|
||||
*.egg-info/
|
||||
docs/source/scripts/activation_images/
|
||||
|
||||
## General
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.cuo
|
||||
*.obj
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Compiled protocol buffers
|
||||
*.pb.h
|
||||
*.pb.cc
|
||||
*_pb2.py
|
||||
|
||||
# Compiled python
|
||||
*.pyc
|
||||
*.pyd
|
||||
|
||||
# Compiled MATLAB
|
||||
*.mex*
|
||||
|
||||
# IPython notebook checkpoints
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Editor temporaries
|
||||
*.swn
|
||||
*.swo
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Sublime Text settings
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# Eclipse Project settings
|
||||
*.*project
|
||||
.settings
|
||||
|
||||
# QtCreator files
|
||||
*.user
|
||||
|
||||
# PyCharm files
|
||||
.idea
|
||||
|
||||
# OSX dir files
|
||||
.DS_Store
|
||||
|
||||
## Caffe2
|
||||
|
||||
# build, distribute, and bins (+ python proto bindings)
|
||||
build
|
||||
build_host_protoc
|
||||
build_android
|
||||
build_ios
|
||||
/build_*
|
||||
.build_debug/*
|
||||
.build_release/*
|
||||
distribute/*
|
||||
*.testbin
|
||||
*.bin
|
||||
cmake_build
|
||||
.cmake_build
|
||||
gen
|
||||
.setuptools-cmake-build
|
||||
.pytest_cache
|
||||
aten/build/*
|
||||
|
||||
# Bram
|
||||
plsdontbreak
|
||||
|
||||
# Generated documentation
|
||||
docs/_site
|
||||
docs/gathered
|
||||
_site
|
||||
doxygen
|
||||
docs/dev
|
||||
|
||||
# LevelDB files
|
||||
*.sst
|
||||
*.ldb
|
||||
LOCK
|
||||
LOG*
|
||||
CURRENT
|
||||
MANIFEST-*
|
||||
|
||||
# generated version file
|
||||
caffe2/version.py
|
||||
|
||||
# setup.py intermediates
|
||||
.eggs
|
||||
caffe2.egg-info
|
||||
|
||||
# Atom/Watchman required file
|
||||
.watchmanconfig
|
||||
|
||||
# BEGIN NOT-CLEAN-FILES (setup.py handles this marker. Do not change.)
|
||||
#
|
||||
# Below files are not deleted by "setup.py clean".
|
||||
|
||||
# Visual Studio Code files
|
||||
.vscode
|
||||
.vs
|
||||
@@ -0,0 +1,407 @@
|
||||
[MASTER]
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
jobs=1
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Allow optimization of some AST trees. This will activate a peephole AST
|
||||
# optimizer, which will apply various small optimizations. For instance, it can
|
||||
# be used to obtain the result of joining multiple strings with the addition
|
||||
# operator. Joining a lot of strings can lead to a maximum recursion error in
|
||||
# Pylint and this flag can prevent that. It has one side effect, the resulting
|
||||
# AST will be different than the one from reality. This option is deprecated
|
||||
# and it will be removed in Pylint 2.0.
|
||||
optimize-ast=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
#enable=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=old-raise-syntax,unpacking-in-except,using-cmp-argument,filter-builtin-not-iterating,nonzero-method,dict-iter-method,basestring-builtin,unicode-builtin,oct-method,cmp-method,delslice-method,round-builtin,range-builtin-not-iterating,cmp-builtin,old-division,old-octal-literal,raising-string,coerce-method,input-builtin,buffer-builtin,parameter-unpacking,hex-method,map-builtin-not-iterating,getslice-method,execfile-builtin,long-builtin,print-statement,raw_input-builtin,apply-builtin,standarderror-builtin,xrange-builtin,setslice-method,import-star-module-level,intern-builtin,indexing-exception,reduce-builtin,no-absolute-import,long-suffix,coerce-builtin,suppressed-message,dict-view-method,metaclass-assignment,backtick,unichr-builtin,useless-suppression,old-ne-operator,zip-builtin-not-iterating,reload-builtin,file-builtin,next-method-called,import-error
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||
# (visual studio) and html. You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Put messages in a separate file for each module / package specified on the
|
||||
# command line instead of printing them on stdout. Reports (if any) will be
|
||||
# written in a file name "pylint_global.[txt|html]". This option is deprecated
|
||||
# and it will be removed in Pylint 2.0.
|
||||
files-output=no
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=yes
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,_cb
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,future.builtins
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=i,j,k,ex,Run,_,x,lr,s
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Naming hint for class names
|
||||
class-name-hint=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for function names
|
||||
function-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct attribute names
|
||||
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for attribute names
|
||||
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct constant names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Naming hint for constant names
|
||||
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Regular expression matching correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Naming hint for module names
|
||||
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for variable names
|
||||
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct argument names
|
||||
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for argument names
|
||||
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct class attribute names
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Naming hint for class attribute names
|
||||
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression matching correct inline iteration names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Naming hint for inline iteration names
|
||||
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for method names
|
||||
method-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
|
||||
[ELIF]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=120
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,dict-separator
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=numpy,torch
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=optparse
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=10
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=5
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=Exception
|
||||
@@ -0,0 +1,7 @@
|
||||
[style]
|
||||
based_on_style = google
|
||||
indent_width = 4
|
||||
COLUMN_LIMIT = 120
|
||||
SPACES_BEFORE_COMMENT = 2
|
||||
EACH_DICT_ENTRY_ON_SEPARATE_LINE = 1
|
||||
CONTINUATION_INDENT_WIDTH = 4
|
||||
@@ -0,0 +1,119 @@
|
||||
# Contributing guidelines
|
||||
|
||||
## Style
|
||||
|
||||
#### C++ coding style
|
||||
|
||||
Follow [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html), with max-line-length extended to 120.
|
||||
|
||||
Run `cpplint` before committing code.
|
||||
|
||||
#### Python coding style
|
||||
|
||||
Follow [Google Python Style Guide,](https://google.github.io/styleguide/pyguide.html) with max-line-length extended to 120. Exceptions are allowed if it feels more natural to follow Pytorch style. For example, Pytorch allows import relative path, also class name.
|
||||
|
||||
Run `pylint` before committing code. It doesn't mean every issue has to be corrected nor check has to be manually disabled. Just make sure you are aware of the remaining issues and you are comfort with all of them. But, don't leave lint error there. Disable it explicitly if it is not a real error.
|
||||
|
||||
Install `pylint`
|
||||
|
||||
```bash
|
||||
pip install pylint
|
||||
```
|
||||
|
||||
To check a file with `pylint`:
|
||||
|
||||
```bash
|
||||
pylint --rcfile=.pylintrc myfile.py
|
||||
```
|
||||
|
||||
#### Yapf
|
||||
|
||||
[yapf](https://github.com/google/yapf/) is an auto format tool owned by Google (not a Google product). To save the time of arguing code style during code review, use yapf to format the code is a good option. Note that it doesn't reformat comment.
|
||||
|
||||
Install `yapf`
|
||||
|
||||
```bash
|
||||
pip install yapf
|
||||
```
|
||||
|
||||
Format code with yapf
|
||||
|
||||
```bash
|
||||
yapf myfile.py --style .style.yapf
|
||||
```
|
||||
|
||||
There are Sublime and Vim plugins.
|
||||
|
||||
## Test
|
||||
|
||||
Use [googletest](https://github.com/google/googletest) for c++ code.
|
||||
|
||||
Use [pytest](https://docs.pytest.org/en/latest/) for python code.
|
||||
|
||||
To run all the tests:
|
||||
|
||||
```shell
|
||||
pytest --verbose
|
||||
```
|
||||
|
||||
To run a particular test file
|
||||
|
||||
```shell
|
||||
pytest --verbose mytestfile.py
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Conventions
|
||||
|
||||
Quantization is a very overloaded word, many things related to it can create a lot of confusions. Let's try to avoid confusions as much as possible by following existing conventions. Generally, if there is a similar Tensorflow or numpy function, follow its convention. Though Tensorflow uses `quantized`, `quantization` and `quant`, let's stick with the shortest one only.
|
||||
|
||||
### Naming
|
||||
|
||||
##### Function and class name
|
||||
|
||||
When developing quantized version of a function or module, add`Quant` to class name, add `quant_` to function name, e.g.
|
||||
|
||||
```python
|
||||
class Linear(...)
|
||||
class QuantLinear(...)
|
||||
|
||||
def linear(...)
|
||||
def quant_linear(...)
|
||||
```
|
||||
|
||||
##### Variable name
|
||||
|
||||
Add prefix `quant_mode_`, `num_bits_` etc. to name of tensors will be quantized, e.g.
|
||||
|
||||
```python
|
||||
def matmul(a, b)
|
||||
def quant_matmul(a, b)
|
||||
```
|
||||
|
||||
Don't use prefix/suffix `weight` or `act` if tensor being quantized doesn't have them explicitly in name. From function's perspective, it takes tensors, not necessarily weight and activation tensors. e.g. `a` and `b` of `matmul` can both be either weight or activation.
|
||||
|
||||
##### Quantization mode
|
||||
|
||||
There only convention here we can adopt is `per_channel`. Other things, like there is no convention to follow of per row/column scale of matrix multiply. And though we usually absolute max value based scaling factor, there are other ways to decide it, like KL-divergence.
|
||||
|
||||
Our API design is flexible enough to support any granularity of quantization. The main concept is `axis`.
|
||||
|
||||
```python
|
||||
# axis=None means per tensor
|
||||
# For 2d convolution weight with layout KCRS, axis=(1, 2, 3) means perchannel quantization
|
||||
# more example below
|
||||
QUANT_DESC_8BIT_PER_TENSOR = QuantDescriptor(num_bits=8)
|
||||
QUANT_DESC_UNSIGNED_8BIT_PER_TENSOR = QuantDescriptor(num_bits=8, unsigned=True)
|
||||
QUANT_DESC_8BIT_CONV1D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
|
||||
QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
|
||||
QUANT_DESC_8BIT_CONV3D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
|
||||
QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW = QuantDescriptor(num_bits=8, axis=(0))
|
||||
QUANT_DESC_8BIT_CONVTRANSPOSE1D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
|
||||
QUANT_DESC_8BIT_CONVTRANSPOSE2D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
|
||||
QUANT_DESC_8BIT_CONVTRANSPOSE3D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
|
||||
|
||||
```
|
||||
|
||||
### Misc
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2020 NVIDIA Corporation
|
||||
|
||||
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.
|
||||
|
||||
|
||||
PORTIONS LICENSED AS FOLLOWS
|
||||
|
||||
> pytorch_quantization/nn/modules/quant_bert.py
|
||||
|
||||
Copyright 2018 The Google AI Language Team Authors and The HuggingFace Inc. team.
|
||||
|
||||
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.
|
||||
|
||||
> pytorch_quantization/nn/modules/*
|
||||
|
||||
From PyTorch:
|
||||
|
||||
Copyright (c) 2016- Facebook, Inc (Adam Paszke)
|
||||
Copyright (c) 2014- Facebook, Inc (Soumith Chintala)
|
||||
Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert)
|
||||
Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu)
|
||||
Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu)
|
||||
Copyright (c) 2011-2013 NYU (Clement Farabet)
|
||||
Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston)
|
||||
Copyright (c) 2006 Idiap Research Institute (Samy Bengio)
|
||||
Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz)
|
||||
|
||||
From Caffe2:
|
||||
|
||||
Copyright (c) 2016-present, Facebook Inc. All rights reserved.
|
||||
|
||||
All contributions by Facebook:
|
||||
Copyright (c) 2016 Facebook Inc.
|
||||
|
||||
All contributions by Google:
|
||||
Copyright (c) 2015 Google Inc.
|
||||
All rights reserved.
|
||||
|
||||
All contributions by Yangqing Jia:
|
||||
Copyright (c) 2015 Yangqing Jia
|
||||
All rights reserved.
|
||||
|
||||
All contributions from Caffe:
|
||||
Copyright(c) 2013, 2014, 2015, the respective contributors
|
||||
All rights reserved.
|
||||
|
||||
All other contributions:
|
||||
Copyright(c) 2015, 2016 the respective contributors
|
||||
All rights reserved.
|
||||
|
||||
Caffe2 uses a copyright model similar to Caffe: each contributor holds
|
||||
copyright over their contributions to Caffe2. The project versioning records
|
||||
all such contribution and copyright details. If a contributor wants to further
|
||||
mark their specific copyright on a particular contribution, they should
|
||||
indicate their copyright solely in the commit message of the change when it is
|
||||
committed.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America
|
||||
and IDIAP Research Institute nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,3 @@
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
recursive-include src *
|
||||
@@ -0,0 +1,45 @@
|
||||
**Note: Pytorch Quantization development has transitioned to the [TensorRT Model Optimizer](https://github.com/NVIDIA/TensorRT-Model-Optimizer). All developers are encouraged to use the TensorRT Model Optimizer to benefit from the latest advancements on quantization and compression. While the Pytorch Quantization code will remain available, it will no longer receive further development.**
|
||||
|
||||
# Pytorch Quantization
|
||||
|
||||
PyTorch-Quantization is a toolkit for training and evaluating PyTorch models with simulated quantization. Quantization can be added to the model automatically, or manually, allowing the model to be tuned for accuracy and performance. Quantization is compatible with NVIDIAs high performance integer kernels which leverage integer Tensor Cores. The quantized model can be exported to ONNX and imported by TensorRT 8.0 and later.
|
||||
|
||||
## Install
|
||||
|
||||
#### Binaries
|
||||
|
||||
```bash
|
||||
pip install pytorch-quantization --extra-index-url https://pypi.ngc.nvidia.com
|
||||
```
|
||||
|
||||
#### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NVIDIA/TensorRT.git
|
||||
cd tools/pytorch-quantization
|
||||
```
|
||||
|
||||
Install PyTorch and prerequisites
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
# for CUDA 10.2 users
|
||||
pip install torch>=1.9.1
|
||||
# for CUDA 11.1 users
|
||||
pip install torch>=1.9.1+cu111
|
||||
```
|
||||
|
||||
Build and install pytorch-quantization
|
||||
```bash
|
||||
# Python version >= 3.7, GCC version >= 5.4 required
|
||||
python setup.py install
|
||||
```
|
||||
|
||||
#### NGC Container
|
||||
|
||||
`pytorch-quantization` is preinstalled in NVIDIA NGC PyTorch container, e.g. `nvcr.io/nvidia/pytorch:22.12-py3`
|
||||
|
||||
## Resources
|
||||
|
||||
* Pytorch Quantization Toolkit [userguide](https://docs.nvidia.com/deeplearning/tensorrt/pytorch-quantization-toolkit/docs/index.html)
|
||||
* Quantization Basics [whitepaper](https://arxiv.org/abs/2004.09602)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
2.2.1
|
||||
@@ -0,0 +1,36 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends '!layout.html' %}
|
||||
|
||||
{% block extrahead %}
|
||||
<script src="//assets.adobedtm.com/5d4962a43b79/c1061d2c5e7b/launch-191c2462b890.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<script type="text/javascript">_satellite.pageBottom();</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
.. role:: hidden
|
||||
:class: hidden-section
|
||||
|
||||
pytorch_quantization.calib
|
||||
===================================
|
||||
|
||||
.. automodule:: pytorch_quantization.calib
|
||||
.. currentmodule:: pytorch_quantization.calib
|
||||
|
||||
:hidden:`MaxCalibrator`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: MaxCalibrator
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
:hidden:`HistogramCalibrator`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: HistogramCalibrator
|
||||
:members:
|
||||
:inherited-members:
|
||||
@@ -0,0 +1,228 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
import sphinx_glpi_theme
|
||||
from pytorch_quantization import __version__
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'pytorch-quantization'
|
||||
copyright = '2021, NVIDIA'
|
||||
|
||||
# The short X.Y version
|
||||
version = __version__
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = 'master'
|
||||
|
||||
author = 'NVIDIA'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.8'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
# Napoleon settings
|
||||
napoleon_google_docstring = True
|
||||
napoleon_numpy_docstring = True
|
||||
napoleon_include_init_with_doc = True
|
||||
napoleon_include_private_with_doc = False
|
||||
napoleon_include_special_with_doc = False
|
||||
napoleon_use_admonition_for_examples = True
|
||||
napoleon_use_admonition_for_notes = True
|
||||
napoleon_use_admonition_for_references = False
|
||||
napoleon_use_ivar = True
|
||||
napoleon_use_param = False
|
||||
napoleon_use_rtype = False
|
||||
|
||||
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
#html_theme = 'sphinx_rtd_theme'
|
||||
html_theme = 'glpi'
|
||||
html_theme_path = sphinx_glpi_theme.get_html_themes_path()
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'pytorch-quantizationdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'pytorch-quantization.tex', 'pytorch-quantization Documentation',
|
||||
'NVIDIA', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'pytorch-quantization', 'pytorch-quantization Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'pytorch-quantization', 'pytorch-quantization Documentation',
|
||||
author, 'pytorch-quantization', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#
|
||||
# epub_identifier = ''
|
||||
|
||||
# A unique identification for the text.
|
||||
#
|
||||
# epub_uid = ''
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
# Disable docstring inheritance
|
||||
autodoc_inherit_docstrings = False
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'numpy': ('https://docs.scipy.org/doc/numpy/', None),
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.. role:: hidden
|
||||
:class: hidden-section
|
||||
|
||||
pytorch_quantization.nn.functional
|
||||
===================================
|
||||
|
||||
.. automodule:: pytorch_quantization.nn.functional
|
||||
.. currentmodule:: pytorch_quantization.nn.functional
|
||||
|
||||
:hidden:`ClipFunction`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: ClipFunction
|
||||
:members:
|
||||
|
||||
|
||||
``clip`` is alias of ``ClipFunction.apply``
|
||||
@@ -0,0 +1,33 @@
|
||||
pytorch-quantization's documentation
|
||||
================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: User Guide
|
||||
|
||||
userguide
|
||||
|
||||
.. toctree::
|
||||
:caption: Tutorials
|
||||
:titlesonly:
|
||||
:maxdepth: 2
|
||||
|
||||
tutorials/quant_resnet50
|
||||
tutorials/creating_custom_quantized_modules
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Package Reference
|
||||
|
||||
calib
|
||||
nn
|
||||
functional
|
||||
optim
|
||||
tensor_quant
|
||||
utils
|
||||
|
||||
Indices
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
@@ -0,0 +1,140 @@
|
||||
.. role:: hidden
|
||||
:class: hidden-section
|
||||
|
||||
pytorch_quantization.nn
|
||||
===================================
|
||||
|
||||
.. automodule:: pytorch_quantization.nn
|
||||
.. currentmodule:: pytorch_quantization.nn
|
||||
|
||||
:hidden:`TensorQuantizer`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: TensorQuantizer
|
||||
:members:
|
||||
|
||||
Quantized Modules
|
||||
----------------------------------
|
||||
.. currentmodule:: pytorch_quantization.nn.modules.quant_conv
|
||||
|
||||
_QuantConvNd
|
||||
~~~~~~~~~~~~
|
||||
.. autoclass:: _QuantConvNd
|
||||
:members:
|
||||
|
||||
.. currentmodule:: pytorch_quantization.nn
|
||||
|
||||
:hidden:`QuantConv1d`
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantConv1d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantConv2d`
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantConv2d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantConv3d`
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantConv3d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantConvTranspose1d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantConvTranspose1d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantConvTranspose2d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
.. autoclass:: QuantConvTranspose2d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantConvTranspose3d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantConvTranspose3d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantLinear`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantLinear
|
||||
:members:
|
||||
|
||||
:hidden:`QuantMaxPool1d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantMaxPool1d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantMaxPool2d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantMaxPool2d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantMaxPool3d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantMaxPool3d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantAvgPool1d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantAvgPool1d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantAvgPool2d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantAvgPool2d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantAvgPool3d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantAvgPool3d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantAdaptiveAvgPool1d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantAdaptiveAvgPool1d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantAdaptiveAvgPool2d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantAdaptiveAvgPool2d
|
||||
:members:
|
||||
|
||||
:hidden:`QuantAdaptiveAvgPool3d`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantAdaptiveAvgPool3d
|
||||
:members:
|
||||
|
||||
:hidden:`Clip`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: Clip
|
||||
:members:
|
||||
|
||||
:hidden:`QuantLSTM`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantLSTM
|
||||
:members:
|
||||
|
||||
:hidden:`QuantLSTMCell`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantLSTMCell
|
||||
:members:
|
||||
@@ -0,0 +1,10 @@
|
||||
.. role:: hidden
|
||||
:class: hidden-section
|
||||
|
||||
pytorch_quantization.optim.helper
|
||||
===================================
|
||||
|
||||
.. automodule:: pytorch_quantization.optim.helper
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,31 @@
|
||||
.. role:: hidden
|
||||
:class: hidden-section
|
||||
|
||||
pytorch_quantization.tensor_quant
|
||||
===================================
|
||||
|
||||
.. automodule:: pytorch_quantization.tensor_quant
|
||||
.. currentmodule:: pytorch_quantization.tensor_quant
|
||||
|
||||
|
||||
:hidden:`QuantDescriptor`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: QuantDescriptor
|
||||
:members:
|
||||
|
||||
:hidden:`ScaledQuantDescriptor`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: ScaledQuantDescriptor
|
||||
:members:
|
||||
|
||||
:hidden:`TensorQuantFunction`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: TensorQuantFunction
|
||||
:members:
|
||||
|
||||
``tensor_quant`` is alias of ``TensorQuantFunction.apply``
|
||||
|
||||
``fake_tensor_quant`` is alias of ``FakeTensorQuantFunction.apply``
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
Creating Custom Quantized Modules
|
||||
=================================
|
||||
|
||||
There are several quantized modules provided by the quantization tool as follows:
|
||||
|
||||
- QuantConv1d, QuantConv2d, QuantConv3d, QuantConvTranspose1d, QuantConvTranspose2d, QuantConvTranspose3d
|
||||
- QuantLinear
|
||||
- QuantAvgPool1d, QuantAvgPool2d, QuantAvgPool3d, QuantMaxPool1d, QuantMaxPool2d, QuantMaxPool3d
|
||||
|
||||
To quantize a module, we need to quantize the input and weights if present. Following are 3 major use-cases:
|
||||
|
||||
#. Create quantized wrapper for modules that have only inputs
|
||||
#. Create quantized wrapper for modules that have inputs as well as weights.
|
||||
#. Directly add the ``TensorQuantizer`` module to the inputs of an operation in the model graph.
|
||||
|
||||
The first two methods are very useful if it's needed to automatically replace the original modules (nodes in the graph) with their quantized versions. The third method could be useful when it's required to manually add the quantization to the model graph at very specific places (more manual, more control).
|
||||
|
||||
Let's see each use-case with examples below.
|
||||
|
||||
Quantizing Modules With Only Inputs
|
||||
-----------------------------------
|
||||
A suitable example would be quantizing the ``pooling`` module variants.
|
||||
|
||||
Essentially, we need to provide a wrapper function that takes the original module and adds the ``TensorQuantizer`` module around it so that the input is first quantized and then fed into the original module.
|
||||
|
||||
- Create the wrapper by subclassing the original module (``pooling.MaxPool2d``) along with the utilities module (``_utils.QuantInputMixin``).
|
||||
|
||||
.. code:: python
|
||||
|
||||
class QuantMaxPool2d(pooling.MaxPool2d, _utils.QuantInputMixin):
|
||||
|
||||
- The ``__init__.py`` function would call the original module's init function and provide it with the corresponding arguments. There would be just one additional argument using ``**kwargs`` which contains the quantization configuration information. The ``QuantInputMixin`` utility contains the method ``pop_quant_desc_in_kwargs`` which extracts this configuration information from the input or returns a default if that input is ``None``. Finally the ``init_quantizer`` method is called that initializes the ``TensorQuantizer`` module which would quantize the inputs.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def __init__(self, kernel_size, stride=None, padding=0, dilation=1,
|
||||
return_indices=False, ceil_mode=False, **kwargs):
|
||||
super(QuantMaxPool2d, self).__init__(kernel_size, stride, padding, dilation,
|
||||
return_indices, ceil_mode)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
- After the initialization, the ``forward`` function needs to be defined in our wrapper module that would actually quantize the inputs using the ``_input_quantizer`` that was initialized in the ``__init__`` function forwarding the inputs to the base module using ``super`` call.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantMaxPool2d, self).forward(quant_input)
|
||||
|
||||
- Finally, we need to define a getter method for the ``_input_quantizer``. This could, for example, be used to disable the quantization for a particular module using ``module.input_quantizer.disable()`` which is helpful while experimenting with different layer quantization configuration.
|
||||
|
||||
.. code:: python
|
||||
|
||||
@property
|
||||
def input_quantizer(self):
|
||||
return self._input_quantizer
|
||||
|
||||
A complete quantized pooling module would look like following:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class QuantMaxPool2d(pooling.MaxPool2d, _utils.QuantInputMixin):
|
||||
"""Quantized 2D maxpool"""
|
||||
def __init__(self, kernel_size, stride=None, padding=0, dilation=1,
|
||||
return_indices=False, ceil_mode=False, **kwargs):
|
||||
super(QuantMaxPool2d, self).__init__(kernel_size, stride, padding, dilation,
|
||||
return_indices, ceil_mode)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantMaxPool2d, self).forward(quant_input)
|
||||
|
||||
@property
|
||||
def input_quantizer(self):
|
||||
return self._input_quantizer
|
||||
|
||||
Quantizing Modules With Weights and Inputs
|
||||
------------------------------------------
|
||||
We give an example of quantizing the ``torch.nn.Linear`` module. It follows that the only additional change from the previous example of quantizing pooling modules is that we'd need to accomodate the quantization of weights in the Linear module.
|
||||
|
||||
- We create the quantized linear module as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class QuantLinear(nn.Linear, _utils.QuantMixin):
|
||||
|
||||
- In the ``__init__`` function, we first use the ``pop_quant_desc_in_kwargs`` function to extract the quantization descriptors for both inputs and weights. Second, we initialize the ``TensorQuantizer`` modules for both inputs and weights using these quantization descriptors.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def __init__(self, in_features, out_features, bias=True, **kwargs):
|
||||
super(QuantLinear, self).__init__(in_features, out_features, bias)
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
|
||||
self.init_quantizer(quant_desc_input, quant_desc_weight)
|
||||
|
||||
- Also, override the ``forward`` function call and pass the inputs and weights through ``_input_quantizer`` and ``_weight_quantizer`` respectively before passing the quantized arguments to the actual ``F.Linear`` call. This step adds the actual input/weight ``TensorQuantizer`` to the module and eventually the model.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
quant_weight = self._weight_quantizer(self.weight)
|
||||
|
||||
output = F.linear(quant_input, quant_weight, bias=self.bias)
|
||||
|
||||
return output
|
||||
|
||||
- Also similar to the ``Linear`` module, we add the getter methods for the ``TensorQuantizer`` modules associated with inputs/weights. This could be used to, for example, disable the quantization mechanism by calling ``module_obj.weight_quantizer.disable()``
|
||||
|
||||
.. code:: python
|
||||
|
||||
@property
|
||||
def input_quantizer(self):
|
||||
return self._input_quantizer
|
||||
|
||||
@property
|
||||
def weight_quantizer(self):
|
||||
return self._weight_quantizer
|
||||
|
||||
- With all of the above changes, the quantized Linear module would look like following:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class QuantLinear(nn.Linear, _utils.QuantMixin):
|
||||
|
||||
def __init__(self, in_features, out_features, bias=True, **kwargs):
|
||||
super(QuantLinear, self).__init__(in_features, out_features, bias)
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
|
||||
self.init_quantizer(quant_desc_input, quant_desc_weight)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
quant_weight = self._weight_quantizer(self.weight)
|
||||
|
||||
output = F.linear(quant_input, quant_weight, bias=self.bias)
|
||||
|
||||
return output
|
||||
|
||||
@property
|
||||
def input_quantizer(self):
|
||||
return self._input_quantizer
|
||||
|
||||
@property
|
||||
def weight_quantizer(self):
|
||||
return self._weight_quantizer
|
||||
|
||||
|
||||
Directly Quantizing Inputs In Graph
|
||||
-----------------------------------
|
||||
It is also possible to directly quantize graph inputs without creating wrappers as explained above.
|
||||
|
||||
Here's an example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quantizer = TensorQuantizer(quant_nn.QuantLinear.default_quant_desc_input)
|
||||
|
||||
quant_input = quantizer(test_input)
|
||||
|
||||
out = F.adaptive_avg_pool2d(quant_input, 3)
|
||||
|
||||
Assume that there is a ``F.adaptive_avg_pool2d`` operation in the graph and we'd like to quantize this operation. In the example above, we use ``TensorQuantizer(quant_nn.QuantLinear.default_quant_desc_input)`` to define a quantizer that we then use to actually quantize the ``test_input`` and then feed this quantized input to the ``F.adaptive_avg_pool2d`` operation. Note that this quantizer is the same as the ones we used earlier while created quantized versions of torch's modules.
|
||||
@@ -0,0 +1,317 @@
|
||||
Quantizing Resnet50
|
||||
===================
|
||||
|
||||
Create a quantized model
|
||||
------------------------
|
||||
|
||||
Import the necessary python modules:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import torch
|
||||
import torch.utils.data
|
||||
from torch import nn
|
||||
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
from pytorch_quantization import calib
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
|
||||
from torchvision import models
|
||||
|
||||
sys.path.append("path to torchvision/references/classification/")
|
||||
from train import evaluate, train_one_epoch, load_data
|
||||
|
||||
Adding quantized modules
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The first step is to add quantizer modules to the neural network graph.
|
||||
This package provides a number of quantized layer modules, which contain quantizers for inputs and weights.
|
||||
e.g. ``quant_nn.QuantLinear``, which can be used in place of ``nn.Linear``.
|
||||
These quantized layers can be substituted automatically, via monkey-patching, or by manually modifying the model definition.
|
||||
|
||||
Automatic layer substitution is done with ``quant_modules``. This should be called before model creation.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from pytorch_quantization import quant_modules
|
||||
quant_modules.initialize()
|
||||
|
||||
This will apply to all instances of each module.
|
||||
If you do not want all modules to be quantized you should instead substitute the quantized modules manually.
|
||||
Stand-alone quantizers can also be added to the model with ``quant_nn.TensorQuantizer``.
|
||||
|
||||
Post training quantization
|
||||
--------------------------
|
||||
|
||||
For efficient inference, we want to select a fixed range for each quantizer.
|
||||
Starting with a pre-trained model, the simplest way to do this is by calibration.
|
||||
|
||||
|
||||
Calibration
|
||||
~~~~~~~~~~~
|
||||
|
||||
We will use histogram based calibration for activations and the default max calibration for weights.
|
||||
|
||||
.. code:: python
|
||||
|
||||
quant_desc_input = QuantDescriptor(calib_method='histogram')
|
||||
quant_nn.QuantConv2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_nn.QuantLinear.set_default_quant_desc_input(quant_desc_input)
|
||||
|
||||
model = models.resnet50(pretrained=True)
|
||||
model.cuda()
|
||||
|
||||
To collect activation histograms we must feed sample data in to the model.
|
||||
First, create ImageNet dataloaders as done in the training script.
|
||||
Then, enable calibration in each quantizer and feed training data in to the model.
|
||||
1024 samples (2 batches of 512) should be sufficient to estimate the distribution of activations.
|
||||
Use training data for calibration so that validation also measures generalization of the selected ranges.
|
||||
|
||||
.. code:: python
|
||||
|
||||
data_path = "PATH to imagenet"
|
||||
batch_size = 512
|
||||
|
||||
traindir = os.path.join(data_path, 'train')
|
||||
valdir = os.path.join(data_path, 'val')
|
||||
dataset, dataset_test, train_sampler, test_sampler = load_data(traindir, valdir, False, False)
|
||||
|
||||
data_loader = torch.utils.data.DataLoader(
|
||||
dataset, batch_size=batch_size,
|
||||
sampler=train_sampler, num_workers=4, pin_memory=True)
|
||||
|
||||
data_loader_test = torch.utils.data.DataLoader(
|
||||
dataset_test, batch_size=batch_size,
|
||||
sampler=test_sampler, num_workers=4, pin_memory=True)
|
||||
|
||||
.. code:: python
|
||||
|
||||
def collect_stats(model, data_loader, num_batches):
|
||||
"""Feed data to the network and collect statistic"""
|
||||
|
||||
# Enable calibrators
|
||||
for name, module in model.named_modules():
|
||||
if isinstance(module, quant_nn.TensorQuantizer):
|
||||
if module._calibrator is not None:
|
||||
module.disable_quant()
|
||||
module.enable_calib()
|
||||
else:
|
||||
module.disable()
|
||||
|
||||
for i, (image, _) in tqdm(enumerate(data_loader), total=num_batches):
|
||||
model(image.cuda())
|
||||
if i >= num_batches:
|
||||
break
|
||||
|
||||
# Disable calibrators
|
||||
for name, module in model.named_modules():
|
||||
if isinstance(module, quant_nn.TensorQuantizer):
|
||||
if module._calibrator is not None:
|
||||
module.enable_quant()
|
||||
module.disable_calib()
|
||||
else:
|
||||
module.enable()
|
||||
|
||||
def compute_amax(model, **kwargs):
|
||||
# Load calib result
|
||||
for name, module in model.named_modules():
|
||||
if isinstance(module, quant_nn.TensorQuantizer):
|
||||
if module._calibrator is not None:
|
||||
if isinstance(module._calibrator, calib.MaxCalibrator):
|
||||
module.load_calib_amax()
|
||||
else:
|
||||
module.load_calib_amax(**kwargs)
|
||||
print(F"{name:40}: {module}")
|
||||
model.cuda()
|
||||
|
||||
# It is a bit slow since we collect histograms on CPU
|
||||
with torch.no_grad():
|
||||
collect_stats(model, data_loader, num_batches=2)
|
||||
compute_amax(model, method="percentile", percentile=99.99)
|
||||
|
||||
After calibration is done, quantizers will have ``amax`` set, which represents the absolute maximum input value representable in the quantized space.
|
||||
By default, weight ranges are per channel while activation ranges are per tensor.
|
||||
We can see the condensed amaxes by printing each ``TensorQuantizer`` module.
|
||||
|
||||
::
|
||||
|
||||
conv1._input_quantizer : TensorQuantizer(8bit fake per-tensor amax=2.6400 calibrator=MaxCalibrator(track_amax=False) quant)
|
||||
conv1._weight_quantizer : TensorQuantizer(8bit fake axis=(0) amax=[0.0000, 0.7817](64) calibrator=MaxCalibrator(track_amax=False) quant)
|
||||
layer1.0.conv1._input_quantizer : TensorQuantizer(8bit fake per-tensor amax=6.8645 calibrator=MaxCalibrator(track_amax=False) quant)
|
||||
layer1.0.conv1._weight_quantizer : TensorQuantizer(8bit fake axis=(0) amax=[0.0000, 0.7266](64) calibrator=MaxCalibrator(track_amax=False) quant)
|
||||
...
|
||||
|
||||
Evaluate the calibrated model
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Next we will evaluate the classification accuracy of our post training quantized model on the ImageNet validation set.
|
||||
|
||||
.. code:: python
|
||||
|
||||
criterion = nn.CrossEntropyLoss()
|
||||
with torch.no_grad():
|
||||
evaluate(model, criterion, data_loader_test, device="cuda", print_freq=20)
|
||||
|
||||
# Save the model
|
||||
torch.save(model.state_dict(), "/tmp/quant_resnet50-calibrated.pth")
|
||||
|
||||
This should yield 76.1% top-1 accuracy, which is close to the pre-trained model accuracy of 76.2%.
|
||||
|
||||
Use different calibration
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We can try different calibrations without recollecting the histograms, and see which one gets the best accuracy.
|
||||
|
||||
.. code:: python
|
||||
|
||||
with torch.no_grad():
|
||||
compute_amax(model, method="percentile", percentile=99.9)
|
||||
evaluate(model, criterion, data_loader_test, device="cuda", print_freq=20)
|
||||
|
||||
with torch.no_grad():
|
||||
for method in ["mse", "entropy"]:
|
||||
print(F"{method} calibration")
|
||||
compute_amax(model, method=method)
|
||||
evaluate(model, criterion, data_loader_test, device="cuda", print_freq=20)
|
||||
|
||||
|
||||
MSE and entropy should both get over 76%. 99.9% clips too many values for resnet50 and will get slightly lower accuracy.
|
||||
|
||||
Quantization Aware Training
|
||||
---------------------
|
||||
|
||||
Optionally, we can fine-tune the calibrated model to improve accuracy further.
|
||||
|
||||
.. code:: python
|
||||
|
||||
criterion = nn.CrossEntropyLoss()
|
||||
optimizer = torch.optim.SGD(model.parameters(), lr=0.0001)
|
||||
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1, gamma=0.1)
|
||||
|
||||
# Training takes about one and half hour per epoch on a single V100
|
||||
train_one_epoch(model, criterion, optimizer, data_loader, "cuda", 0, 100)
|
||||
|
||||
# Save the model
|
||||
torch.save(model.state_dict(), "/tmp/quant_resnet50-finetuned.pth")
|
||||
|
||||
After one epoch of fine-tuning, we can achieve over 76.4% top-1 accuracy.
|
||||
Fine-tuning for more epochs with learning rate annealing can improve accuracy further.
|
||||
For example, fine-tuning for 15 epochs with cosine annealing starting with a learning rate of 0.001 can get over 76.7%.
|
||||
It should be noted that the same fine-tuning schedule will improve the accuracy of the unquantized model as well.
|
||||
|
||||
Further optimization
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For efficient inference on TensorRT, we need know more details about the runtime optimization.
|
||||
TensorRT supports fusion of quantizing convolution and residual add.
|
||||
The new fused operator has two inputs. Let us call them conv-input and residual-input.
|
||||
Here the fused operator’s output precision must match the residual input precision.
|
||||
When there is another quantizing node after the fused operator,
|
||||
we can insert a pair of quantizing/dequantizing nodes between the residual-input and the Elementwise-Addition node,
|
||||
so that quantizing node after the Convolution node is fused with the Convolution node, and the Convolution node is completely quantized with INT8 input and output.
|
||||
We cannot use automatic monkey-patching to apply this optimization and we need to manually insert the quantizing/dequantizing nodes.
|
||||
|
||||
First create a copy of resnet.py from https://github.com/pytorch/vision/blob/master/torchvision/models/resnet.py,
|
||||
modify the constructor, add explicit bool flag ‘quantize’
|
||||
|
||||
.. code:: python
|
||||
|
||||
def resnet50(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress, quantize, **kwargs)
|
||||
def _resnet(arch: str, block: Type[Union[BasicBlock, Bottleneck]], layers: List[int], pretrained: bool, progress: bool,
|
||||
quantize: bool, **kwargs: Any) -> ResNet:
|
||||
model = ResNet(block, layers, quantize, **kwargs)
|
||||
class ResNet(nn.Module):
|
||||
def __init__(self,
|
||||
block: Type[Union[BasicBlock, Bottleneck]],
|
||||
layers: List[int],
|
||||
quantize: bool = False,
|
||||
num_classes: int = 1000,
|
||||
zero_init_residual: bool = False,
|
||||
groups: int = 1,
|
||||
width_per_group: int = 64,
|
||||
replace_stride_with_dilation: Optional[List[bool]] = None,
|
||||
norm_layer: Optional[Callable[..., nn.Module]] = None) -> None:
|
||||
super(ResNet, self).__init__()
|
||||
self._quantize = quantize
|
||||
|
||||
When this ``self._quantize`` flag is set to ``True``, we need replace all the ``nn.Conv2d`` with ``quant_nn.QuantConv2d``.
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
def conv3x3(in_planes: int,
|
||||
out_planes: int,
|
||||
stride: int = 1,
|
||||
groups: int = 1,
|
||||
dilation: int = 1,
|
||||
quantize: bool = False) -> nn.Conv2d:
|
||||
"""3x3 convolution with padding"""
|
||||
if quantize:
|
||||
return quant_nn.QuantConv2d(in_planes,
|
||||
out_planes,
|
||||
kernel_size=3,
|
||||
stride=stride,
|
||||
padding=dilation,
|
||||
groups=groups,
|
||||
bias=False,
|
||||
dilation=dilation)
|
||||
else:
|
||||
return nn.Conv2d(in_planes,
|
||||
out_planes,
|
||||
kernel_size=3,
|
||||
stride=stride,
|
||||
padding=dilation,
|
||||
groups=groups,
|
||||
bias=False,
|
||||
dilation=dilation)
|
||||
def conv1x1(in_planes: int, out_planes: int, stride: int = 1, quantize: bool = False) -> nn.Conv2d:
|
||||
"""1x1 convolution"""
|
||||
if quantize:
|
||||
return quant_nn.QuantConv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
|
||||
else:
|
||||
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
|
||||
|
||||
|
||||
The residual conv add can be find both in both ``BasicBlock`` and ``Bottleneck``.
|
||||
We need first declare quantization node in the ``__init__`` function.
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
def __init__(self,
|
||||
inplanes: int,
|
||||
planes: int,
|
||||
stride: int = 1,
|
||||
downsample: Optional[nn.Module] = None,
|
||||
groups: int = 1,
|
||||
base_width: int = 64,
|
||||
dilation: int = 1,
|
||||
norm_layer: Optional[Callable[..., nn.Module]] = None,
|
||||
quantize: bool = False) -> None:
|
||||
# other code...
|
||||
self._quantize = quantize
|
||||
if self._quantize:
|
||||
self.residual_quantizer = quant_nn.TensorQuantizer(quant_nn.QuantConv2d.default_quant_desc_input)
|
||||
|
||||
|
||||
Finally we need patch the ``forward`` function in both ``BasicBlock`` and ``Bottleneck``,
|
||||
inserting extra quantization/dequantization nodes here.
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
# other code...
|
||||
if self._quantize:
|
||||
out += self.residual_quantizer(identity)
|
||||
else:
|
||||
out += identity
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
The final resnet code with residual quantized can be found in https://github.com/NVIDIA/TensorRT/blob/master/tools/pytorch-quantization/examples/torchvision/models/classification/resnet.py
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
Basic Functionalities
|
||||
---------------------
|
||||
|
||||
Quantization function
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``tensor_quant`` and ``fake_tensor_quant`` are 2 basic functions to
|
||||
quantize a tensor. ``fake_tensor_quant`` returns fake quantized tensor
|
||||
(float value). ``tensor_quant`` returns quantized tensor (integer value)
|
||||
and scale.
|
||||
|
||||
.. code:: python
|
||||
|
||||
tensor_quant(inputs, amax, num_bits=8, output_dtype=torch.float, unsigned=False)
|
||||
fake_tensor_quant(inputs, amax, num_bits=8, output_dtype=torch.float, unsigned=False)
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
|
||||
# Generate random input. With fixed seed 12345, x should be
|
||||
# tensor([0.9817, 0.8796, 0.9921, 0.4611, 0.0832, 0.1784, 0.3674, 0.5676, 0.3376, 0.2119])
|
||||
torch.manual_seed(12345)
|
||||
x = torch.rand(10)
|
||||
|
||||
# fake quantize tensor x. fake_quant_x will be
|
||||
# tensor([0.9843, 0.8828, 0.9921, 0.4609, 0.0859, 0.1797, 0.3672, 0.5703, 0.3359, 0.2109])
|
||||
fake_quant_x = tensor_quant.fake_tensor_quant(x, x.abs().max())
|
||||
|
||||
# quantize tensor x. quant_x will be
|
||||
# tensor([126., 113., 127., 59., 11., 23., 47., 73., 43., 27.])
|
||||
# with scale=128.0057
|
||||
quant_x, scale = tensor_quant.tensor_quant(x, x.abs().max())
|
||||
|
||||
Backward of both functions are defined as `Straight-Through Estimator (STE) <https://arxiv.org/abs/1308.3432>`_.
|
||||
|
||||
Descriptor and quantizer
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``QuantDescriptor`` defines how a tensor should be quantized. There are
|
||||
also some predefined ``QuantDescriptor``, e.g.
|
||||
``QUANT_DESC_8BIT_PER_TENSOR`` and
|
||||
``QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL``.
|
||||
|
||||
``TensorQuantizer`` is the module for quantizing tensors and defined by
|
||||
``QuantDescriptor``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
from pytorch_quantization.nn.modules.tensor_quantizer import TensorQuantizer
|
||||
|
||||
quant_desc = QuantDescriptor(num_bits=4, fake_quant=False, axis=(0), unsigned=True)
|
||||
quantizer = TensorQuantizer(quant_desc)
|
||||
|
||||
torch.manual_seed(12345)
|
||||
x = torch.rand(10, 9, 8, 7)
|
||||
|
||||
quant_x = quantizer(x)
|
||||
|
||||
If ``amax`` is given in the :func:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`, :func:`TensorQuantizer <pytorch_quantization.nn.TensorQuantizer>` will use it to quantize. Otherwise, :func:`TensorQuantizer <pytorch_quantization.nn.TensorQuantizer>` will compute amax then quantize. amax will be computed w.r.t ``axis`` specified. Note that ``axis`` of QuantDescriptor specify remaining axis as oppsed to axis of `max() <https://docs.scipy.org/doc/numpy/reference/generated/numpy.amax.html>`_.
|
||||
|
||||
Quantized module
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
There are 2 major types of module, ``Conv`` and ``Linear``. Both can
|
||||
replace ``torch.nn`` version and apply quantization on both weight and
|
||||
activation.
|
||||
|
||||
Both take ``quant_desc_input`` and ``quant_desc_weight`` in addition to
|
||||
arguments of the original module.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from torch import nn
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
import pytorch_quantization.nn as quant_nn
|
||||
|
||||
# pytorch's module
|
||||
fc1 = nn.Linear(in_features, out_features, bias=True)
|
||||
conv1 = nn.Conv2d(in_channels, out_channels, kernel_size)
|
||||
|
||||
# quantized version
|
||||
quant_fc1 = quant_nn.Linear(
|
||||
in_features, out_features, bias=True,
|
||||
quant_desc_input=tensor_quant.QUANT_DESC_8BIT_PER_TENSOR,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW)
|
||||
quant_conv1 = quant_nn.Conv2d(
|
||||
in_channels, out_channels, kernel_size,
|
||||
quant_desc_input=tensor_quant.QUANT_DESC_8BIT_PER_TENSOR,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL)
|
||||
|
||||
Post training quantization
|
||||
--------------------------
|
||||
|
||||
A model can be post training quantized by simply by calling ``quant_modules.initialize()``
|
||||
|
||||
.. code:: python
|
||||
|
||||
from pytorch_quantization import quant_modules
|
||||
model = torchvision.models.resnet50()
|
||||
|
||||
If a model is not entirely defined by module, than TensorQuantizer should be
|
||||
manually created and added to the right place in the model.
|
||||
|
||||
Calibration
|
||||
~~~~~~~~~~~
|
||||
|
||||
Calibration is the TensorRT terminology of passing data samples to the
|
||||
quantizer and deciding the best amax for activations.
|
||||
We support 3 calibration methods:
|
||||
|
||||
- ``max``: Simply use global maximum absolute value
|
||||
- ``entropy``: TensorRT's entropy calibration
|
||||
- ``percentile``: Get rid of outlier based on given percentile.
|
||||
- ``mse``: MSE(Mean Squared Error) based calibration
|
||||
|
||||
In above ResNet50 example, calibration method is set to ``mse``, it can
|
||||
be used as the following example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Find the TensorQuantizer and enable calibration
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith('_quantizer'):
|
||||
module.enable_calib()
|
||||
module.disable_quant() # Use full precision data to calibrate
|
||||
|
||||
# Feeding data samples
|
||||
model(x)
|
||||
# ...
|
||||
|
||||
# Finalize calibration
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith('_quantizer'):
|
||||
module.load_calib_amax()
|
||||
module.disable_calib()
|
||||
module.enable_quant()
|
||||
|
||||
# If running on GPU, it needs to call .cuda() again because new tensors will be created by calibration process
|
||||
model.cuda()
|
||||
|
||||
# Keep running the quantized model
|
||||
# ...
|
||||
|
||||
.. note::
|
||||
|
||||
Calibration needs to be performed before exporting the model to ONNX.
|
||||
|
||||
Quantization Aware Training
|
||||
---------------------
|
||||
|
||||
Quantization Aware Training is based on Straight Through Estimator (STE)
|
||||
derivative approximation. It is some time known as “quantization aware
|
||||
training”. We don’t use the name because it doesn’t reflect the
|
||||
underneath assumption. If anything, it makes training being “unaware” of
|
||||
quantization because of the STE approximation.
|
||||
|
||||
After calibration is done, Quantization Aware Training is simply select a
|
||||
training schedule and continue training the calibrated model. Usually,
|
||||
it doesn’t need to fine tune very long. We usually use around 10% of the
|
||||
original training schedule, starting at 1% of the initial training
|
||||
learning rate, and a cosine annealing learning rate schedule that
|
||||
follows the decreasing half of a cosine period, down to 1% of the
|
||||
initial fine tuning learning rate (0.01% of the initial training
|
||||
learning rate).
|
||||
|
||||
Some recommendations
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Quantization Aware Training (Essentially a discrete numerical optimization
|
||||
problem) is not a solved problem mathematically. Based on our
|
||||
experience, here are some recommendations:
|
||||
|
||||
- For STE approximation to work well, it is better to use small
|
||||
learning rate. Large learning rate is more likely to enlarge the
|
||||
variance introduced by STE approximation and destroy the trained
|
||||
network.
|
||||
- Do not change quantization representation (scale) during training, at
|
||||
least not too frequently. Changing scale every step, it is
|
||||
effectively like changing data format (e8m7, e5m10, e3m4, et.al)
|
||||
every step, which will easily affect convergence.
|
||||
|
||||
Export to ONNX
|
||||
--------------
|
||||
|
||||
The goal of exporting to ONNX is to deploy to TensorRT, not to
|
||||
ONNX runtime. So we only export fake quantized model into a form TensorRT will take. Fake
|
||||
quantization will be broken into a pair of
|
||||
QuantizeLinear/DequantizeLinear ONNX ops. TensorRT will take
|
||||
the generated ONNX graph, and execute it in int8 in the most optimized way to its
|
||||
capability.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, we only support exporting int8 and fp8 fake quantized modules.
|
||||
Additionally, quantized modules need to be calibrated before exporting to ONNX.
|
||||
|
||||
Fake quantized model can be exported to ONNX as any other Pytorch model.
|
||||
Please learn more about exporting a Pytorch model to ONNX at
|
||||
`torch.onnx <https://pytorch.org/docs/stable/onnx.html?highlight=onnx#module-torch.onnx>`__.
|
||||
For example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import pytorch_quantization
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
from pytorch_quantization import quant_modules
|
||||
|
||||
quant_modules.initialize()
|
||||
model = torchvision.models.resnet50()
|
||||
|
||||
# load the calibrated model
|
||||
state_dict = torch.load("quant_resnet50-entropy-1024.pth", map_location="cpu")
|
||||
model.load_state_dict(state_dict)
|
||||
model.cuda()
|
||||
|
||||
dummy_input = torch.randn(128, 3, 224, 224, device='cuda')
|
||||
|
||||
input_names = [ "actual_input_1" ]
|
||||
output_names = [ "output1" ]
|
||||
|
||||
with pytorch_quantization.enable_onnx_export():
|
||||
# enable_onnx_checker needs to be disabled. See notes below.
|
||||
torch.onnx.export(
|
||||
model, dummy_input, "quant_resnet50.onnx", verbose=True, opset_version=10, enable_onnx_checker=False
|
||||
)
|
||||
|
||||
.. Note::
|
||||
|
||||
Note that ``axis`` is added to ``QuantizeLinear`` and ``DequantizeLinear`` in opset13.
|
||||
@@ -0,0 +1,21 @@
|
||||
.. role:: hidden
|
||||
:class: hidden-section
|
||||
|
||||
pytorch_quantization.utils
|
||||
===================================
|
||||
|
||||
pytorch_quantization.utils.quant\_logging
|
||||
--------------------------------------------
|
||||
|
||||
.. automodule:: pytorch_quantization.utils.quant_logging
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pytorch_quantization.utils.reduce\_amax
|
||||
------------------------------------------
|
||||
|
||||
.. automodule:: pytorch_quantization.utils.reduce_amax
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,749 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import datetime\n",
|
||||
"import os\n",
|
||||
"import sys\n",
|
||||
"import time\n",
|
||||
"import collections\n",
|
||||
"\n",
|
||||
"import torch\n",
|
||||
"import torch.utils.data\n",
|
||||
"from torch import nn\n",
|
||||
"\n",
|
||||
"from tqdm import tqdm\n",
|
||||
"\n",
|
||||
"import torchvision\n",
|
||||
"from torchvision import transforms\n",
|
||||
"\n",
|
||||
"from pytorch_quantization import nn as quant_nn\n",
|
||||
"from pytorch_quantization import calib\n",
|
||||
"from pytorch_quantization.tensor_quant import QuantDescriptor\n",
|
||||
"\n",
|
||||
"from absl import logging\n",
|
||||
"logging.set_verbosity(logging.FATAL) # Disable logging as they are too noisy in notebook\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# For simplicity, import train and eval functions from the train script from torchvision instead of copything them here\n",
|
||||
"# Download torchvision from https://github.com/pytorch/vision\n",
|
||||
"sys.path.append(\"/raid/skyw/models/torchvision/references/classification/\")\n",
|
||||
"from train import evaluate, train_one_epoch, load_data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Set default QuantDescriptor to use histogram based calibration for activation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"quant_desc_input = QuantDescriptor(calib_method='histogram')\n",
|
||||
"quant_nn.QuantConv2d.set_default_quant_desc_input(quant_desc_input)\n",
|
||||
"quant_nn.QuantLinear.set_default_quant_desc_input(quant_desc_input)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Initialize quantized modules"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pytorch_quantization import quant_modules\n",
|
||||
"quant_modules.initialize()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Create model with pretrained weight"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"ResNet(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
|
||||
" (layer1): Sequential(\n",
|
||||
" (0): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (downsample): Sequential(\n",
|
||||
" (0): QuantConv2d(\n",
|
||||
" 64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (1): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (2): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (layer2): Sequential(\n",
|
||||
" (0): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (downsample): Sequential(\n",
|
||||
" (0): QuantConv2d(\n",
|
||||
" 256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (1): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (2): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (3): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (layer3): Sequential(\n",
|
||||
" (0): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (downsample): Sequential(\n",
|
||||
" (0): QuantConv2d(\n",
|
||||
" 512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (1): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (2): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (3): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (4): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (5): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (layer4): Sequential(\n",
|
||||
" (0): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (downsample): Sequential(\n",
|
||||
" (0): QuantConv2d(\n",
|
||||
" 1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (1): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (2): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
|
||||
" (fc): QuantLinear(\n",
|
||||
" in_features=2048, out_features=1000, bias=True\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=dynamic calibrator=HistogramCalibrator quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=dynamic calibrator=MaxCalibrator quant)\n",
|
||||
" )\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"model = torchvision.models.resnet50(pretrained=True, progress=False)\n",
|
||||
"model.cuda()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Create data loader"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Loading data\n",
|
||||
"Loading training data\n",
|
||||
"Took 3.580507755279541\n",
|
||||
"Loading validation data\n",
|
||||
"Creating data loaders\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"data_path = \"/raid/data/imagenet/imagenet_pytorch\"\n",
|
||||
"batch_size = 512\n",
|
||||
"\n",
|
||||
"traindir = os.path.join(data_path, 'train')\n",
|
||||
"valdir = os.path.join(data_path, 'val')\n",
|
||||
"_args = collections.namedtuple('mock_args', ['model', 'distributed', 'cache_dataset'])\n",
|
||||
"dataset, dataset_test, train_sampler, test_sampler = load_data(traindir, valdir, _args(model='resnet50', distributed=False, cache_dataset=False))\n",
|
||||
"\n",
|
||||
"data_loader = torch.utils.data.DataLoader(\n",
|
||||
" dataset, batch_size=batch_size,\n",
|
||||
" sampler=train_sampler, num_workers=4, pin_memory=True)\n",
|
||||
"\n",
|
||||
"data_loader_test = torch.utils.data.DataLoader(\n",
|
||||
" dataset_test, batch_size=batch_size,\n",
|
||||
" sampler=test_sampler, num_workers=4, pin_memory=True)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Calibrate the model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def collect_stats(model, data_loader, num_batches):\n",
|
||||
" \"\"\"Feed data to the network and collect statistic\"\"\"\n",
|
||||
"\n",
|
||||
" # Enable calibrators\n",
|
||||
" for name, module in model.named_modules():\n",
|
||||
" if isinstance(module, quant_nn.TensorQuantizer):\n",
|
||||
" if module._calibrator is not None:\n",
|
||||
" module.disable_quant()\n",
|
||||
" module.enable_calib()\n",
|
||||
" else:\n",
|
||||
" module.disable()\n",
|
||||
"\n",
|
||||
" for i, (image, _) in tqdm(enumerate(data_loader), total=num_batches):\n",
|
||||
" model(image.cuda())\n",
|
||||
" if i >= num_batches:\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" # Disable calibrators\n",
|
||||
" for name, module in model.named_modules():\n",
|
||||
" if isinstance(module, quant_nn.TensorQuantizer):\n",
|
||||
" if module._calibrator is not None:\n",
|
||||
" module.enable_quant()\n",
|
||||
" module.disable_calib()\n",
|
||||
" else:\n",
|
||||
" module.enable()\n",
|
||||
" \n",
|
||||
"def compute_amax(model, **kwargs):\n",
|
||||
" # Load calib result\n",
|
||||
" for name, module in model.named_modules():\n",
|
||||
" if isinstance(module, quant_nn.TensorQuantizer):\n",
|
||||
" if module._calibrator is not None:\n",
|
||||
" if isinstance(module._calibrator, calib.MaxCalibrator):\n",
|
||||
" module.load_calib_amax()\n",
|
||||
" else:\n",
|
||||
" module.load_calib_amax(**kwargs)\n",
|
||||
"# print(F\"{name:40}: {module}\")\n",
|
||||
" model.cuda()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 2/2 [04:50<00:00, 111.13s/it]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# It is a bit slow since we collect histograms on CPU\n",
|
||||
"with torch.no_grad():\n",
|
||||
" collect_stats(model, data_loader, num_batches=2)\n",
|
||||
" compute_amax(model, method=\"percentile\", percentile=99.99)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Now evaluate the calibrated model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Test: [ 0/98] eta: 0:05:53 loss: 0.5656 (0.5656) acc1: 85.7422 (85.7422) acc5: 96.0938 (96.0938) time: 3.6079 data: 2.8152 max mem: 5880\n",
|
||||
"Test: [20/98] eta: 0:01:07 loss: 0.6741 (0.6825) acc1: 82.8125 (82.4219) acc5: 95.8984 (95.7682) time: 0.7343 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [40/98] eta: 0:00:46 loss: 0.6995 (0.7157) acc1: 80.0781 (81.4024) acc5: 96.0938 (95.7412) time: 0.7226 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [60/98] eta: 0:00:29 loss: 1.1064 (0.8590) acc1: 71.4844 (78.2627) acc5: 91.0156 (94.1150) time: 0.7259 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [80/98] eta: 0:00:13 loss: 1.1220 (0.9372) acc1: 72.4609 (76.7072) acc5: 89.6484 (93.1375) time: 0.7220 data: 0.0002 max mem: 5882\n",
|
||||
"Test: Total time: 0:01:13\n",
|
||||
" * Acc@1 76.138 Acc@5 92.916\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"criterion = nn.CrossEntropyLoss()\n",
|
||||
"with torch.no_grad():\n",
|
||||
" evaluate(model, criterion, data_loader_test, device=\"cuda\", print_freq=20)\n",
|
||||
" \n",
|
||||
"# Save the model\n",
|
||||
"torch.save(model.state_dict(), \"/tmp/quant_resnet50-calibrated.pth\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## We can also try different calibrations and see which one works the best"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Test: [ 0/98] eta: 0:05:27 loss: 0.6037 (0.6037) acc1: 84.9609 (84.9609) acc5: 95.3125 (95.3125) time: 3.3411 data: 2.6190 max mem: 5882\n",
|
||||
"Test: [20/98] eta: 0:01:06 loss: 0.6760 (0.7041) acc1: 81.2500 (81.7522) acc5: 95.7031 (95.4892) time: 0.7243 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [40/98] eta: 0:00:45 loss: 0.7241 (0.7351) acc1: 79.1016 (80.7784) acc5: 95.8984 (95.4459) time: 0.7243 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [60/98] eta: 0:00:29 loss: 1.1162 (0.8793) acc1: 71.4844 (77.6383) acc5: 90.8203 (93.7948) time: 0.7204 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [80/98] eta: 0:00:13 loss: 1.1498 (0.9603) acc1: 71.4844 (76.0368) acc5: 89.4531 (92.7156) time: 0.7164 data: 0.0002 max mem: 5882\n",
|
||||
"Test: Total time: 0:01:12\n",
|
||||
" * Acc@1 75.438 Acc@5 92.486\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"with torch.no_grad():\n",
|
||||
" compute_amax(model, method=\"percentile\", percentile=99.9)\n",
|
||||
" evaluate(model, criterion, data_loader_test, device=\"cuda\", print_freq=20)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"mse calibration\n",
|
||||
"Test: [ 0/98] eta: 0:06:34 loss: 0.5700 (0.5700) acc1: 85.1562 (85.1562) acc5: 96.2891 (96.2891) time: 4.0243 data: 3.3231 max mem: 5882\n",
|
||||
"Test: [20/98] eta: 0:01:08 loss: 0.6758 (0.6838) acc1: 82.8125 (82.5707) acc5: 96.0938 (95.7868) time: 0.7204 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [40/98] eta: 0:00:46 loss: 0.7047 (0.7163) acc1: 80.2734 (81.4834) acc5: 96.2891 (95.7746) time: 0.7178 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [60/98] eta: 0:00:29 loss: 1.1127 (0.8585) acc1: 71.0938 (78.3395) acc5: 90.8203 (94.1278) time: 0.7192 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [80/98] eta: 0:00:13 loss: 1.1261 (0.9367) acc1: 72.6562 (76.7530) acc5: 89.8438 (93.1785) time: 0.7176 data: 0.0002 max mem: 5882\n",
|
||||
"Test: Total time: 0:01:13\n",
|
||||
" * Acc@1 76.186 Acc@5 92.926\n",
|
||||
"entropy calibration\n",
|
||||
"Test: [ 0/98] eta: 0:05:28 loss: 0.5648 (0.5648) acc1: 85.3516 (85.3516) acc5: 96.0938 (96.0938) time: 3.3558 data: 2.6268 max mem: 5882\n",
|
||||
"Test: [20/98] eta: 0:01:05 loss: 0.6724 (0.6815) acc1: 82.8125 (82.5428) acc5: 95.8984 (95.7589) time: 0.7196 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [40/98] eta: 0:00:45 loss: 0.7090 (0.7149) acc1: 80.6641 (81.4929) acc5: 96.0938 (95.7269) time: 0.7214 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [60/98] eta: 0:00:29 loss: 1.1077 (0.8571) acc1: 72.0703 (78.3779) acc5: 90.6250 (94.0798) time: 0.7198 data: 0.0002 max mem: 5882\n",
|
||||
"Test: [80/98] eta: 0:00:13 loss: 1.1253 (0.9356) acc1: 72.2656 (76.7626) acc5: 90.0391 (93.1231) time: 0.7192 data: 0.0002 max mem: 5882\n",
|
||||
"Test: Total time: 0:01:12\n",
|
||||
" * Acc@1 76.206 Acc@5 92.900\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"with torch.no_grad():\n",
|
||||
" for method in [\"mse\", \"entropy\"]:\n",
|
||||
" print(F\"{method} calibration\")\n",
|
||||
" compute_amax(model, method=method)\n",
|
||||
" evaluate(model, criterion, data_loader_test, device=\"cuda\", print_freq=20)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.10"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Following the calibrate_quant_resnet50 example, now we fine tune the model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"WARNING: Logging before flag parsing goes to stderr.\n",
|
||||
"W0608 21:25:39.018203 140228493526848 tensor_quant.py:96] Meaning of axis has changed since v2.0. Make sure to update.\n",
|
||||
"W0608 21:25:39.019082 140228493526848 tensor_quant.py:96] Meaning of axis has changed since v2.0. Make sure to update.\n",
|
||||
"W0608 21:25:39.019555 140228493526848 tensor_quant.py:96] Meaning of axis has changed since v2.0. Make sure to update.\n",
|
||||
"W0608 21:25:39.020030 140228493526848 tensor_quant.py:96] Meaning of axis has changed since v2.0. Make sure to update.\n",
|
||||
"W0608 21:25:39.020492 140228493526848 tensor_quant.py:96] Meaning of axis has changed since v2.0. Make sure to update.\n",
|
||||
"W0608 21:25:39.020947 140228493526848 tensor_quant.py:96] Meaning of axis has changed since v2.0. Make sure to update.\n",
|
||||
"W0608 21:25:39.021392 140228493526848 tensor_quant.py:96] Meaning of axis has changed since v2.0. Make sure to update.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import datetime\n",
|
||||
"import os\n",
|
||||
"import sys\n",
|
||||
"import time\n",
|
||||
"\n",
|
||||
"import torch\n",
|
||||
"import torch.utils.data\n",
|
||||
"from torch import nn\n",
|
||||
"\n",
|
||||
"from tqdm import tqdm\n",
|
||||
"\n",
|
||||
"import torchvision\n",
|
||||
"from torchvision import transforms\n",
|
||||
"\n",
|
||||
"from pytorch_quantization import nn as quant_nn\n",
|
||||
"from pytorch_quantization import calib\n",
|
||||
"from pytorch_quantization.tensor_quant import QuantDescriptor\n",
|
||||
"\n",
|
||||
"from absl import logging\n",
|
||||
"logging.set_verbosity(logging.FATAL) # Disable logging as they are too noisy in notebook"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# For simplicity, import train and eval functions from the train script from torchvision instead of copything them here\n",
|
||||
"sys.path.append(\"/raid/skyw/models/torchvision/references/classification/\")\n",
|
||||
"from train import evaluate, train_one_epoch, load_data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"QuantResNet(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=2.6387 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.7817](64) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
|
||||
" (layer1): Sequential(\n",
|
||||
" (0): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=2.9730 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.7266](64) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.0971 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.4679](64) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.3318 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.3936](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (downsample): Sequential(\n",
|
||||
" (0): QuantConv2d(\n",
|
||||
" 64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=2.9730 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.9879](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (1): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.4872 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.2618](64) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.0466 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0564, 0.5201](64) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.5106 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.2946](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (2): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.5250 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0533, 0.1921](64) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.9980 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0810, 0.2856](64) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.6532 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0197, 0.2752](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (layer2): Sequential(\n",
|
||||
" (0): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.5499 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0670, 0.3532](128) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.1606 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0694, 0.2993](128) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.1425 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.3917](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (downsample): Sequential(\n",
|
||||
" (0): QuantConv2d(\n",
|
||||
" 256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.5499 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.5662](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (1): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.4626 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0370, 0.2522](128) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.8304 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0142, 0.2998](128) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.1722 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.3038](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (2): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.4864 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0653, 0.2383](128) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.9450 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0646, 0.2556](128) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.8535 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0162, 0.3522](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (3): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.5229 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0648, 0.2814](128) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.9247 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0595, 0.2210](128) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.9747 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0127, 0.2956](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (layer3): Sequential(\n",
|
||||
" (0): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.5941 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0842, 0.3425](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.3565 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0544, 0.2008](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.0293 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.3212](1024) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (downsample): Sequential(\n",
|
||||
" (0): QuantConv2d(\n",
|
||||
" 512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.5941 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.3460](1024) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (1): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.3305 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0407, 0.2942](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.9844 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0432, 0.2634](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.9333 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.4969](1024) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (2): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.3388 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0469, 0.2715](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.8617 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0397, 0.2100](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.7507 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.3538](1024) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (3): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.3554 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0553, 0.2390](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.9257 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0455, 0.2792](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.8117 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0000, 0.3126](1024) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (4): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.4199 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0598, 0.2722](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.9274 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0459, 0.1919](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.8702 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0160, 0.3161](1024) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (5): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.4258 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0618, 0.3995](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.2256 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0498, 0.2236](256) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.3560 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0180, 0.3288](1024) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (layer4): Sequential(\n",
|
||||
" (0): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.3915 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0744, 0.3415](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.1571 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0432, 0.3993](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.1295 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0198, 0.3546](2048) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" (downsample): Sequential(\n",
|
||||
" (0): QuantConv2d(\n",
|
||||
" 1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.3915 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0197, 0.6413](2048) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (1): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=3.9348 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0482, 0.7003](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.1277 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0379, 0.2257](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=0.8992 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0211, 0.2427](2048) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" (2): Bottleneck(\n",
|
||||
" (conv1): QuantConv2d(\n",
|
||||
" 2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=5.2181 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0601, 0.4541](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv2): QuantConv2d(\n",
|
||||
" 512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.2051 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0338, 0.1416](512) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (conv3): QuantConv2d(\n",
|
||||
" 512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=1.1045 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.0119, 0.2798](2048) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
" (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
|
||||
" (relu): ReLU(inplace=True)\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
|
||||
" (fc): QuantLinear(\n",
|
||||
" in_features=2048, out_features=1000, bias=True\n",
|
||||
" (_input_quantizer): TensorQuantizer(8bit fake per-tensor amax=5.5345 calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" (_weight_quantizer): TensorQuantizer(8bit fake axis=0 amax=[0.1716, 0.7371](1000) calibrator=MaxCalibrator(track_amax=False) quant)\n",
|
||||
" )\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from pytorch_quantization import quant_modules\n",
|
||||
"quant_modules.initialize()\n",
|
||||
"\n",
|
||||
"# Create and load the calibrated model\n",
|
||||
"model = torchvision.models.resnet50()\n",
|
||||
"model.load_state_dict(torch.load(\"/tmp/quant_resnet50-calibrated.pth\"))\n",
|
||||
"model.cuda()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"data_path = \"/raid/data/imagenet/imagenet_pytorch\"\n",
|
||||
"\n",
|
||||
"traindir = os.path.join(data_path, 'train')\n",
|
||||
"valdir = os.path.join(data_path, 'val')\n",
|
||||
"dataset, dataset_test, train_sampler, test_sampler = load_data(traindir, valdir, False, False)\n",
|
||||
"\n",
|
||||
"data_loader = torch.utils.data.DataLoader(\n",
|
||||
" dataset, batch_size=256,\n",
|
||||
" sampler=train_sampler, num_workers=4, pin_memory=True)\n",
|
||||
"\n",
|
||||
"data_loader_test = torch.utils.data.DataLoader(\n",
|
||||
" dataset_test, batch_size=256,\n",
|
||||
" sampler=test_sampler, num_workers=4, pin_memory=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Quantized fine tuning\n",
|
||||
"Let's fine tune the model with fake quantization. We only fine tune for 1 epoch as an example."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"criterion = nn.CrossEntropyLoss()\n",
|
||||
"optimizer = torch.optim.SGD(model.parameters(), lr=0.0001)\n",
|
||||
"lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1, gamma=0.1)\n",
|
||||
"\n",
|
||||
"data_loader = torch.utils.data.DataLoader(\n",
|
||||
" dataset, batch_size=128,\n",
|
||||
" sampler=train_sampler, num_workers=16, pin_memory=True)\n",
|
||||
"\n",
|
||||
"# Training takes about one and half hour per epoch on single V100\n",
|
||||
"train_one_epoch(model, criterion, optimizer, data_loader, \"cuda\", 0, 100)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Evaluate the fine tuned model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Test: [ 0/196] eta: 0:09:36 loss: 0.4680 (0.4680) acc1: 85.9375 (85.9375) acc5: 98.0469 (98.0469) time: 2.9406 data: 2.1852 max mem: 16096\n",
|
||||
"Test: [ 10/196] eta: 0:01:58 loss: 0.6694 (0.6522) acc1: 83.2031 (82.9545) acc5: 96.0938 (96.1293) time: 0.6346 data: 0.1988 max mem: 16096\n",
|
||||
"Test: [ 20/196] eta: 0:01:30 loss: 0.6738 (0.6733) acc1: 82.0312 (82.4777) acc5: 95.7031 (95.7961) time: 0.3928 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [ 30/196] eta: 0:01:18 loss: 0.6219 (0.6322) acc1: 84.3750 (83.9718) acc5: 95.7031 (96.0181) time: 0.3859 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [ 40/196] eta: 0:01:10 loss: 0.6801 (0.6750) acc1: 81.6406 (82.5934) acc5: 95.7031 (95.9604) time: 0.3861 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [ 50/196] eta: 0:01:04 loss: 0.6937 (0.6724) acc1: 80.0781 (82.3529) acc5: 96.8750 (96.0938) time: 0.3834 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [ 60/196] eta: 0:00:58 loss: 0.7149 (0.6849) acc1: 80.0781 (81.9864) acc5: 96.4844 (96.1066) time: 0.3854 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [ 70/196] eta: 0:00:53 loss: 0.6616 (0.6716) acc1: 80.8594 (82.2843) acc5: 96.4844 (96.1983) time: 0.3859 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [ 80/196] eta: 0:00:48 loss: 0.6510 (0.6968) acc1: 81.2500 (81.7467) acc5: 95.7031 (95.9201) time: 0.3860 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [ 90/196] eta: 0:00:44 loss: 0.9469 (0.7444) acc1: 76.1719 (80.6834) acc5: 92.5781 (95.4370) time: 0.3868 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [100/196] eta: 0:00:39 loss: 1.1594 (0.7964) acc1: 70.7031 (79.5521) acc5: 90.6250 (94.8755) time: 0.3864 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [110/196] eta: 0:00:35 loss: 1.1594 (0.8214) acc1: 72.2656 (79.0365) acc5: 91.4062 (94.6298) time: 0.3836 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [120/196] eta: 0:00:31 loss: 0.9820 (0.8389) acc1: 76.1719 (78.7771) acc5: 92.1875 (94.3634) time: 0.3856 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [130/196] eta: 0:00:26 loss: 1.0825 (0.8705) acc1: 72.6562 (77.9610) acc5: 91.4062 (94.0303) time: 0.3866 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [140/196] eta: 0:00:22 loss: 1.1088 (0.8889) acc1: 72.2656 (77.6125) acc5: 91.4062 (93.8137) time: 0.3879 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [150/196] eta: 0:00:18 loss: 1.1069 (0.9059) acc1: 73.0469 (77.2998) acc5: 91.0156 (93.5586) time: 0.3914 data: 0.0002 max mem: 16096\n",
|
||||
"Test: [160/196] eta: 0:00:14 loss: 1.1360 (0.9197) acc1: 73.0469 (77.0380) acc5: 90.2344 (93.3472) time: 0.3898 data: 0.0002 max mem: 16096\n",
|
||||
"Test: [170/196] eta: 0:00:10 loss: 1.2171 (0.9371) acc1: 71.8750 (76.6265) acc5: 89.8438 (93.1721) time: 0.3845 data: 0.0002 max mem: 16096\n",
|
||||
"Test: [180/196] eta: 0:00:06 loss: 1.2493 (0.9527) acc1: 68.7500 (76.2992) acc5: 90.2344 (93.0205) time: 0.3815 data: 0.0001 max mem: 16096\n",
|
||||
"Test: [190/196] eta: 0:00:02 loss: 1.0816 (0.9511) acc1: 71.4844 (76.3089) acc5: 92.1875 (93.0465) time: 0.3736 data: 0.0001 max mem: 16096\n",
|
||||
"Test: Total time: 0:01:17\n",
|
||||
" * Acc@1 76.426 Acc@5 93.080\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"with torch.no_grad():\n",
|
||||
" evaluate(model, criterion, data_loader_test, device=\"cuda\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"After only 1 epoch of quantized fine tuning, top-1 improved from ~76.1 to 76.426. Train longer with lr anealing can improve accuracy further"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.10"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import warnings
|
||||
import collections
|
||||
|
||||
import subprocess
|
||||
|
||||
import torch
|
||||
import torch.utils.data
|
||||
|
||||
from collections import namedtuple
|
||||
from torch import nn
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
import torchvision
|
||||
from torchvision import transforms
|
||||
from torch.hub import load_state_dict_from_url
|
||||
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
from pytorch_quantization import calib
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
from pytorch_quantization import quant_modules
|
||||
|
||||
import onnxruntime
|
||||
import numpy as np
|
||||
import models.classification as models
|
||||
|
||||
from prettytable import PrettyTable
|
||||
|
||||
# The following path assumes running in nvcr.io/nvidia/pytorch:20.08-py3
|
||||
sys.path.insert(0, "/opt/pytorch/vision/references/classification/")
|
||||
|
||||
# Import functions from torchvision reference
|
||||
try:
|
||||
from train import evaluate, train_one_epoch, load_data, utils
|
||||
except Exception as e:
|
||||
raise ModuleNotFoundError(
|
||||
"Add https://github.com/pytorch/vision/blob/master/references/classification/ to PYTHONPATH")
|
||||
|
||||
|
||||
def get_parser():
|
||||
"""
|
||||
Creates an argument parser.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description='Classification quantization flow script')
|
||||
|
||||
parser.add_argument('--data-dir', '-d', type=str, help='input data folder', required=True)
|
||||
parser.add_argument('--model-name', '-m', default='resnet50', help='model name: default resnet50')
|
||||
parser.add_argument('--disable-pcq',
|
||||
'-dpcq',
|
||||
action="store_true",
|
||||
help='disable per-channel quantization for weights')
|
||||
parser.add_argument('--out-dir', '-o', default='/tmp', help='output folder: default /tmp')
|
||||
parser.add_argument('--print-freq', '-pf', type=int, default=20, help='evaluation print frequency: default 20')
|
||||
parser.add_argument('--threshold',
|
||||
'-t',
|
||||
type=float,
|
||||
default=-1.0,
|
||||
help='top1 accuracy threshold (less than 0.0 means no comparison): default -1.0')
|
||||
|
||||
parser.add_argument('--fp16', action="store_true", help="Enable FP16 model training, evaluation and export")
|
||||
|
||||
parser.add_argument('--batch-size-train', type=int, default=128, help='batch size for training: default 128')
|
||||
parser.add_argument('--batch-size-test', type=int, default=128, help='batch size for testing: default 128')
|
||||
parser.add_argument('--batch-size-onnx', type=int, default=1, help='batch size for onnx: default 1')
|
||||
|
||||
parser.add_argument('--seed', type=int, default=12345, help='random seed: default 12345')
|
||||
|
||||
checkpoint = parser.add_mutually_exclusive_group(required=True)
|
||||
checkpoint.add_argument('--ckpt-path', default='', type=str, help='path to latest checkpoint (default: none)')
|
||||
checkpoint.add_argument('--ckpt-url', default='', type=str, help='url to latest checkpoint (default: none)')
|
||||
checkpoint.add_argument('--pretrained', action="store_true")
|
||||
|
||||
parser.add_argument('--num-calib-batch',
|
||||
default=4,
|
||||
type=int,
|
||||
help='Number of batches for calibration. 0 will disable calibration. (default: 4)')
|
||||
parser.add_argument('--num-finetune-epochs',
|
||||
default=0,
|
||||
type=int,
|
||||
help='Number of epochs to fine tune. 0 will disable fine tune. (default: 0)')
|
||||
parser.add_argument('--calibrator', type=str, choices=["max", "histogram"], default="max")
|
||||
parser.add_argument('--percentile', nargs='+', type=float, default=[99.9, 99.99, 99.999, 99.9999])
|
||||
parser.add_argument('--sensitivity', action="store_true", help="Build sensitivity profile")
|
||||
parser.add_argument('--evaluate-onnx', action="store_true", help="Evaluate exported ONNX")
|
||||
parser.add_argument('--evaluate-trt', action="store_true", help="Export and evaluate TRT")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def prepare_model(model_name,
|
||||
data_dir,
|
||||
per_channel_quantization,
|
||||
batch_size_train,
|
||||
batch_size_test,
|
||||
batch_size_onnx,
|
||||
calibrator,
|
||||
pretrained=True,
|
||||
ckpt_path=None,
|
||||
ckpt_url=None,
|
||||
fp16=False):
|
||||
"""
|
||||
Prepare the model for the classification flow.
|
||||
Arguments:
|
||||
model_name: name to use when accessing torchvision model dictionary
|
||||
data_dir: directory with train and val subdirs prepared "imagenet style"
|
||||
per_channel_quantization: iff true use per channel quantization for weights
|
||||
note that this isn't currently supported in ONNX-RT/Pytorch
|
||||
batch_size_train: batch size to use when training
|
||||
batch_size_test: batch size to use when testing in Pytorch
|
||||
batch_size_onnx: batch size to use when testing with ONNX-RT
|
||||
calibrator: calibration type to use (max/histogram)
|
||||
|
||||
pretrained: if true a pretrained model will be loaded from torchvision
|
||||
ckpt_path: path to load a model checkpoint from, if not pretrained
|
||||
ckpt_url: url to download a model checkpoint from, if not pretrained and no path was given
|
||||
* at least one of {pretrained, path, url} must be valid
|
||||
|
||||
The method returns a the following list:
|
||||
[
|
||||
Model object,
|
||||
data loader for training,
|
||||
data loader for Pytorch testing,
|
||||
data loader for onnx testing
|
||||
]
|
||||
"""
|
||||
# Use 'spawn' to avoid CUDA reinitialization with forked subprocess
|
||||
torch.multiprocessing.set_start_method('spawn')
|
||||
|
||||
## Initialize quantization, model and data loaders
|
||||
if per_channel_quantization:
|
||||
quant_desc_input = QuantDescriptor(calib_method=calibrator)
|
||||
quant_nn.QuantConv2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_nn.QuantLinear.set_default_quant_desc_input(quant_desc_input)
|
||||
else:
|
||||
## Force per tensor quantization for onnx runtime
|
||||
quant_desc_input = QuantDescriptor(calib_method=calibrator, axis=None)
|
||||
quant_nn.QuantConv2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_nn.QuantConvTranspose2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_nn.QuantLinear.set_default_quant_desc_input(quant_desc_input)
|
||||
|
||||
quant_desc_weight = QuantDescriptor(calib_method=calibrator, axis=None)
|
||||
quant_nn.QuantConv2d.set_default_quant_desc_weight(quant_desc_weight)
|
||||
quant_nn.QuantConvTranspose2d.set_default_quant_desc_weight(quant_desc_weight)
|
||||
quant_nn.QuantLinear.set_default_quant_desc_weight(quant_desc_weight)
|
||||
|
||||
if model_name in models.__dict__:
|
||||
model = models.__dict__[model_name](pretrained=pretrained, quantize=True)
|
||||
else:
|
||||
quant_modules.initialize()
|
||||
model = torchvision.models.__dict__[model_name](pretrained=pretrained)
|
||||
quant_modules.deactivate()
|
||||
|
||||
if not pretrained:
|
||||
if ckpt_path:
|
||||
checkpoint = torch.load(ckpt_path)
|
||||
else:
|
||||
checkpoint = load_state_dict_from_url(ckpt_url)
|
||||
if 'state_dict' in checkpoint.keys():
|
||||
checkpoint = checkpoint['state_dict']
|
||||
elif 'model' in checkpoint.keys():
|
||||
checkpoint = checkpoint['model']
|
||||
model.load_state_dict(checkpoint)
|
||||
model.eval()
|
||||
model.cuda()
|
||||
|
||||
if fp16:
|
||||
model = model.half()
|
||||
|
||||
## Prepare the data loaders
|
||||
traindir = os.path.join(data_dir, 'train')
|
||||
valdir = os.path.join(data_dir, 'val')
|
||||
_args = collections.namedtuple("mock_args", [
|
||||
"model", "distributed", "cache_dataset", "val_resize_size", "val_crop_size", "train_crop_size", "interpolation",
|
||||
"ra_magnitude", "augmix_severity", "weights", "backend", "use_v2"
|
||||
])
|
||||
dataset, dataset_test, train_sampler, test_sampler = load_data(
|
||||
traindir, valdir,
|
||||
_args(model=model_name,
|
||||
distributed=False,
|
||||
cache_dataset=False,
|
||||
val_resize_size=256,
|
||||
val_crop_size=224,
|
||||
train_crop_size=224,
|
||||
interpolation="bilinear",
|
||||
ra_magnitude=9,
|
||||
augmix_severity=3,
|
||||
weights=None,
|
||||
backend="pil",
|
||||
use_v2=False))
|
||||
|
||||
data_loader_train = torch.utils.data.DataLoader(dataset,
|
||||
batch_size=batch_size_train,
|
||||
sampler=train_sampler,
|
||||
num_workers=4,
|
||||
pin_memory=True)
|
||||
|
||||
data_loader_test = torch.utils.data.DataLoader(dataset_test,
|
||||
batch_size=batch_size_test,
|
||||
sampler=test_sampler,
|
||||
num_workers=4,
|
||||
pin_memory=True)
|
||||
|
||||
data_loader_onnx = torch.utils.data.DataLoader(dataset_test,
|
||||
batch_size=batch_size_onnx,
|
||||
sampler=test_sampler,
|
||||
num_workers=4,
|
||||
pin_memory=True)
|
||||
|
||||
return model, data_loader_train, data_loader_test, data_loader_onnx
|
||||
|
||||
|
||||
def main(cmdline_args):
|
||||
parser = get_parser()
|
||||
args = parser.parse_args(cmdline_args)
|
||||
print(parser.description)
|
||||
print(args)
|
||||
|
||||
torch.manual_seed(args.seed)
|
||||
np.random.seed(args.seed)
|
||||
|
||||
## Prepare the pretrained model and data loaders
|
||||
model, data_loader_train, data_loader_test, data_loader_onnx = prepare_model(
|
||||
args.model_name, args.data_dir, not args.disable_pcq, args.batch_size_train, args.batch_size_test,
|
||||
args.batch_size_onnx, args.calibrator, args.pretrained, args.ckpt_path, args.ckpt_url, args.fp16)
|
||||
|
||||
## Initial accuracy evaluation
|
||||
CrossEntropy = nn.CrossEntropyLoss()
|
||||
|
||||
# nn.CrossEntropyLoss expects float inputs
|
||||
def criterion(output, target):
|
||||
return CrossEntropy(output.float(), target)
|
||||
|
||||
with torch.no_grad():
|
||||
print('Initial evaluation:')
|
||||
top1_initial = evaluate(model, criterion, data_loader_test, device="cuda", print_freq=args.print_freq)
|
||||
|
||||
## Calibrate the model
|
||||
with torch.no_grad():
|
||||
calibrate_model(model=model,
|
||||
model_name=args.model_name,
|
||||
data_loader=data_loader_train,
|
||||
num_calib_batch=args.num_calib_batch,
|
||||
calibrator=args.calibrator,
|
||||
hist_percentile=args.percentile,
|
||||
out_dir=args.out_dir)
|
||||
|
||||
## Evaluate after calibration
|
||||
if args.num_calib_batch > 0:
|
||||
with torch.no_grad():
|
||||
print('Calibration evaluation:')
|
||||
top1_calibrated = evaluate(model, criterion, data_loader_test, device="cuda", print_freq=args.print_freq)
|
||||
else:
|
||||
top1_calibrated = -1.0
|
||||
|
||||
## Build sensitivy profile
|
||||
if args.sensitivity:
|
||||
build_sensitivity_profile(model, criterion, data_loader_test)
|
||||
|
||||
## Finetune the model
|
||||
criterion = nn.CrossEntropyLoss()
|
||||
optimizer = torch.optim.SGD(model.parameters(), lr=0.0001)
|
||||
lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, args.num_finetune_epochs)
|
||||
for epoch in range(args.num_finetune_epochs):
|
||||
# Training a single epch
|
||||
if "print_freq" in inspect.signature(train_one_epoch).parameters:
|
||||
train_one_epoch(model, criterion, optimizer, data_loader_train, "cuda", 0, 100)
|
||||
else:
|
||||
_args = collections.namedtuple("mock_args",
|
||||
["print_freq", "clip_grad_norm", "model_ema_steps", "lr_warmup_epochs"])
|
||||
train_one_epoch(model, criterion, optimizer, data_loader_train, "cuda", 0,
|
||||
_args(print_freq=100, clip_grad_norm=None, model_ema_steps=32, lr_warmup_epochs=0))
|
||||
lr_scheduler.step()
|
||||
|
||||
if args.num_finetune_epochs > 0:
|
||||
## Evaluate after finetuning
|
||||
with torch.no_grad():
|
||||
print('Finetune evaluation:')
|
||||
top1_finetuned = evaluate(model, criterion, data_loader_test, device="cuda")
|
||||
else:
|
||||
top1_finetuned = -1.0
|
||||
|
||||
## Export to ONNX
|
||||
onnx_filename = args.out_dir + '/' + args.model_name + ".onnx"
|
||||
top1_onnx = -1.0
|
||||
if args.evaluate_onnx and export_onnx(model, onnx_filename, args.batch_size_onnx, not args.disable_pcq):
|
||||
## Validate ONNX and evaluate
|
||||
top1_onnx = evaluate_onnx(onnx_filename, data_loader_onnx, criterion, args.print_freq)
|
||||
|
||||
trt_filename = args.out_dir + '/' + args.model_name + ".trt"
|
||||
top1_trt = -1.0
|
||||
if args.evaluate_trt and export_trt(model, trt_filename, args.batch_size_onnx, args.fp16):
|
||||
## Validate TRT and evaluate
|
||||
top1_trt = evaluate_trt(trt_filename, data_loader_onnx, criterion, args.print_freq)
|
||||
|
||||
## Print summary
|
||||
print("Accuracy summary:")
|
||||
table = PrettyTable(['Stage', 'Top1'])
|
||||
table.align['Stage'] = "l"
|
||||
table.add_row(['Initial', "{:.2f}".format(top1_initial)])
|
||||
table.add_row(['Calibrated', "{:.2f}".format(top1_calibrated)])
|
||||
table.add_row(['Finetuned', "{:.2f}".format(top1_finetuned)])
|
||||
table.add_row(['ONNX', "{:.2f}".format(top1_onnx)])
|
||||
if args.evaluate_trt:
|
||||
table.add_row(['TRT', "{:.2f}".format(top1_trt)])
|
||||
print(table)
|
||||
|
||||
## Compare results
|
||||
if args.threshold >= 0.0:
|
||||
if args.evaluate_onnx and top1_onnx < 0.0:
|
||||
print("Failed to export/evaluate ONNX!")
|
||||
return 1
|
||||
if args.evaluate_trt and top1_trt < 0.0:
|
||||
print("Failed to export/evaluate TRT!")
|
||||
return 1
|
||||
if args.num_finetune_epochs > 0:
|
||||
if top1_finetuned >= (top1_onnx - args.threshold):
|
||||
print("Accuracy threshold was met!")
|
||||
else:
|
||||
print("Accuracy threshold was missed!")
|
||||
return 1
|
||||
|
||||
if args.evaluate_trt and top1_finetuned >= (top1_trt - args.threshold):
|
||||
print("TRT Accuracy threshold was met!")
|
||||
elif args.evaluate_trt:
|
||||
print("TRT Accuracy threshold was missed!")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def evaluate_onnx(onnx_filename, data_loader, criterion, print_freq):
|
||||
"""Evaluate accuracy on the given ONNX file using the provided data loader and criterion.
|
||||
The method returns the average top-1 accuracy on the given dataset.
|
||||
"""
|
||||
print("Loading ONNX file: ", onnx_filename)
|
||||
ort_session = onnxruntime.InferenceSession(onnx_filename, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
|
||||
with torch.no_grad():
|
||||
metric_logger = utils.MetricLogger(delimiter=" ")
|
||||
header = 'Test:'
|
||||
with torch.no_grad():
|
||||
for image, target in metric_logger.log_every(data_loader, print_freq, header):
|
||||
image = image.to("cpu", non_blocking=True)
|
||||
image_data = np.array(image)
|
||||
input_data = image_data
|
||||
|
||||
# run the data through onnx runtime instead of torch model
|
||||
input_name = ort_session.get_inputs()[0].name
|
||||
raw_result = ort_session.run([], {input_name: input_data})
|
||||
output = torch.tensor((raw_result[0])).float()
|
||||
|
||||
loss = criterion(output, target)
|
||||
acc1, acc5 = utils.accuracy(output, target, topk=(1, 5))
|
||||
batch_size = image.shape[0]
|
||||
metric_logger.update(loss=loss.item())
|
||||
metric_logger.meters['acc1'].update(acc1.item(), n=batch_size)
|
||||
metric_logger.meters['acc5'].update(acc5.item(), n=batch_size)
|
||||
# gather the stats from all processes
|
||||
metric_logger.synchronize_between_processes()
|
||||
|
||||
print(' ONNXRuntime: Acc@1 {top1.global_avg:.3f} Acc@5 {top5.global_avg:.3f}'.format(top1=metric_logger.acc1,
|
||||
top5=metric_logger.acc5))
|
||||
return metric_logger.acc1.global_avg
|
||||
|
||||
|
||||
def evaluate_trt(trt_filename, data_loader, criterion, print_freq):
|
||||
print("Loading TRT file: ", trt_filename)
|
||||
|
||||
import pycuda.driver as cuda
|
||||
try:
|
||||
import pycuda.autoprimaryctx
|
||||
except ModuleNotFoundError:
|
||||
import pycuda.autoinit
|
||||
|
||||
import tensorrt as trt
|
||||
|
||||
TRT_LOGGER = trt.Logger()
|
||||
|
||||
TRT_tensor = namedtuple('TRT_tensor', ['binding_idx', 'shape', 'dtype', 'device_memory', 'host_memory'])
|
||||
|
||||
def load_engine(engine_file_path):
|
||||
assert os.path.exists(engine_file_path)
|
||||
print("Reading engine from file {}".format(engine_file_path))
|
||||
with open(engine_file_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
|
||||
return runtime.deserialize_cuda_engine(f.read())
|
||||
|
||||
def setup_context(engine):
|
||||
return engine.create_execution_context()
|
||||
|
||||
def allocate_buffers(engine, context):
|
||||
|
||||
# Allocate host and device buffers
|
||||
bindings = []
|
||||
inputs = {}
|
||||
outputs = {}
|
||||
for binding_idx in range(engine.num_bindings):
|
||||
binding = engine.get_tensor_name(binding_idx)
|
||||
shape = tuple(context.get_tensor_shape(binding))
|
||||
|
||||
size = trt.volume(context.get_tensor_shape(binding))
|
||||
dtype = np.dtype(trt.nptype(engine.get_tensor_dtype(binding)))
|
||||
|
||||
device_memory = cuda.mem_alloc(size * dtype.itemsize)
|
||||
bindings.append(int(device_memory))
|
||||
|
||||
if engine.get_tensor_mode(binding) == trt.TensorIOMode.INPUT:
|
||||
inputs[binding] = TRT_tensor(binding_idx, shape, dtype, device_memory, None)
|
||||
else:
|
||||
host_memory = cuda.pagelocked_empty(size, dtype)
|
||||
outputs[binding] = TRT_tensor(binding_idx, shape, dtype, device_memory, host_memory)
|
||||
|
||||
stream = cuda.Stream()
|
||||
return bindings, inputs, outputs, stream
|
||||
|
||||
def infer(batch, context, bindings, inputs, outputs, stream):
|
||||
|
||||
# Transfer input data to the GPU.
|
||||
for name, trt_in_t in inputs.items():
|
||||
buffer = np.ascontiguousarray(batch[name])
|
||||
cuda.memcpy_htod_async(trt_in_t.device_memory, buffer, stream)
|
||||
|
||||
# Run inference
|
||||
context.execute_async_v2(bindings=bindings, stream_handle=stream.handle)
|
||||
|
||||
# Transfer predictions back from the GPU.
|
||||
for _, trt_out_t in outputs.items():
|
||||
cuda.memcpy_dtoh_async(trt_out_t.host_memory, trt_out_t.device_memory, stream)
|
||||
|
||||
# Synchronize the stream
|
||||
stream.synchronize()
|
||||
|
||||
return {k: torch.tensor(v.host_memory).reshape(v.shape) for k, v in outputs.items()}
|
||||
|
||||
engine = load_engine(trt_filename)
|
||||
context = setup_context(engine)
|
||||
bindings, inputs, outputs, stream = allocate_buffers(engine, context)
|
||||
|
||||
with torch.no_grad():
|
||||
metric_logger = utils.MetricLogger(delimiter=" ")
|
||||
header = 'Test:'
|
||||
with torch.no_grad():
|
||||
for image, target in metric_logger.log_every(data_loader, print_freq, header):
|
||||
image = image.to("cpu", non_blocking=True)
|
||||
image_data = np.array(image)
|
||||
|
||||
output = infer({"input": image_data}, context, bindings, inputs, outputs, stream)["output"].float()
|
||||
|
||||
loss = criterion(output, target)
|
||||
acc1, acc5 = utils.accuracy(output, target, topk=(1, 5))
|
||||
batch_size = image.shape[0]
|
||||
metric_logger.update(loss=loss.item())
|
||||
metric_logger.meters['acc1'].update(acc1.item(), n=batch_size)
|
||||
metric_logger.meters['acc5'].update(acc5.item(), n=batch_size)
|
||||
# gather the stats from all processes
|
||||
metric_logger.synchronize_between_processes()
|
||||
|
||||
print(' TRTRuntime: Acc@1 {top1.global_avg:.3f} Acc@5 {top5.global_avg:.3f}'.format(top1=metric_logger.acc1,
|
||||
top5=metric_logger.acc5))
|
||||
return metric_logger.acc1.global_avg
|
||||
|
||||
|
||||
def _export_onnx(model, dummy_input, onnx_filename, opset_version):
|
||||
try:
|
||||
if "enable_onnx_checker" in inspect.signature(torch.onnx.export).parameters:
|
||||
torch.onnx.export(model,
|
||||
dummy_input,
|
||||
onnx_filename,
|
||||
verbose=False,
|
||||
input_names=["input"],
|
||||
output_names=["output"],
|
||||
opset_version=opset_version,
|
||||
enable_onnx_checker=False,
|
||||
do_constant_folding=True)
|
||||
else:
|
||||
torch.onnx.export(model,
|
||||
dummy_input,
|
||||
onnx_filename,
|
||||
verbose=False,
|
||||
input_names=["input"],
|
||||
output_names=["output"],
|
||||
opset_version=opset_version,
|
||||
do_constant_folding=True)
|
||||
except ValueError:
|
||||
print("Failed to export to ONNX")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def export_onnx(model, onnx_filename, batch_onnx, per_channel_quantization):
|
||||
model.eval()
|
||||
|
||||
if per_channel_quantization:
|
||||
opset_version = 13
|
||||
else:
|
||||
opset_version = 12
|
||||
|
||||
# Export ONNX for multiple batch sizes
|
||||
print("Creating ONNX file: " + onnx_filename)
|
||||
dummy_input = torch.randn(batch_onnx, 3, 224, 224, device='cuda') #TODO: switch input dims by model
|
||||
return _export_onnx(model, dummy_input, onnx_filename, opset_version)
|
||||
|
||||
|
||||
def export_trt(model, trt_filename, batch_trt, fp16=False):
|
||||
model.eval()
|
||||
|
||||
# Export TRT for multiple batch sizes
|
||||
print("Creating TRT file: " + trt_filename)
|
||||
dummy_input = torch.randn(batch_trt, 3, 224, 224, device='cuda') #TODO: switch input dims by model
|
||||
|
||||
OPSET = 17
|
||||
onnx_filename = trt_filename.replace(".trt", ".onnx")
|
||||
|
||||
if not _export_onnx(model, dummy_input, onnx_filename, OPSET):
|
||||
return False
|
||||
|
||||
trt_cmd = f"trtexec --onnx={onnx_filename} --saveEngine={trt_filename} --int8"
|
||||
|
||||
if fp16:
|
||||
trt_cmd += " --fp16"
|
||||
|
||||
print(trt_cmd)
|
||||
try:
|
||||
trt_stdout = subprocess.check_output(trt_cmd, shell=True).decode("utf-8")
|
||||
except:
|
||||
print("Failed to export to TRT")
|
||||
return False
|
||||
|
||||
print(trt_stdout)
|
||||
return 'PASSED' in trt_stdout
|
||||
|
||||
|
||||
def calibrate_model(model, model_name, data_loader, num_calib_batch, calibrator, hist_percentile, out_dir):
|
||||
"""
|
||||
Feed data to the network and calibrate.
|
||||
Arguments:
|
||||
model: classification model
|
||||
model_name: name to use when creating state files
|
||||
data_loader: calibration data set
|
||||
num_calib_batch: amount of calibration passes to perform
|
||||
calibrator: type of calibration to use (max/histogram)
|
||||
hist_percentile: percentiles to be used for historgram calibration
|
||||
out_dir: dir to save state files in
|
||||
"""
|
||||
|
||||
if num_calib_batch > 0:
|
||||
print("Calibrating model")
|
||||
with torch.no_grad():
|
||||
collect_stats(model, data_loader, num_calib_batch)
|
||||
|
||||
if not calibrator == "histogram":
|
||||
compute_amax(model, method="max")
|
||||
calib_output = os.path.join(out_dir, F"{model_name}-max-{num_calib_batch*data_loader.batch_size}.pth")
|
||||
torch.save(model.state_dict(), calib_output)
|
||||
else:
|
||||
for percentile in hist_percentile:
|
||||
print(F"{percentile} percentile calibration")
|
||||
compute_amax(model, method="percentile")
|
||||
calib_output = os.path.join(
|
||||
out_dir, F"{model_name}-percentile-{percentile}-{num_calib_batch*data_loader.batch_size}.pth")
|
||||
torch.save(model.state_dict(), calib_output)
|
||||
|
||||
for method in ["mse", "entropy"]:
|
||||
print(F"{method} calibration")
|
||||
compute_amax(model, method=method)
|
||||
calib_output = os.path.join(out_dir,
|
||||
F"{model_name}-{method}-{num_calib_batch*data_loader.batch_size}.pth")
|
||||
torch.save(model.state_dict(), calib_output)
|
||||
|
||||
|
||||
def collect_stats(model, data_loader, num_batches):
|
||||
"""Feed data to the network and collect statistics"""
|
||||
# Enable calibrators
|
||||
for name, module in model.named_modules():
|
||||
if isinstance(module, quant_nn.TensorQuantizer):
|
||||
if module._calibrator is not None:
|
||||
module.disable_quant()
|
||||
module.enable_calib()
|
||||
else:
|
||||
module.disable()
|
||||
|
||||
# Feed data to the network for collecting stats
|
||||
for i, (image, _) in tqdm(enumerate(data_loader), total=num_batches):
|
||||
model(image.cuda())
|
||||
if i >= num_batches:
|
||||
break
|
||||
|
||||
# Disable calibrators
|
||||
for name, module in model.named_modules():
|
||||
if isinstance(module, quant_nn.TensorQuantizer):
|
||||
if module._calibrator is not None:
|
||||
module.enable_quant()
|
||||
module.disable_calib()
|
||||
else:
|
||||
module.enable()
|
||||
|
||||
|
||||
def compute_amax(model, **kwargs):
|
||||
# Load calib result
|
||||
for name, module in model.named_modules():
|
||||
if isinstance(module, quant_nn.TensorQuantizer):
|
||||
if module._calibrator is not None:
|
||||
if isinstance(module._calibrator, calib.MaxCalibrator):
|
||||
module.load_calib_amax()
|
||||
else:
|
||||
module.load_calib_amax(**kwargs)
|
||||
print(F"{name:40}: {module}")
|
||||
model.cuda()
|
||||
|
||||
|
||||
def build_sensitivity_profile(model, criterion, data_loader_test):
|
||||
quant_layer_names = []
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith("_quantizer"):
|
||||
module.disable()
|
||||
layer_name = name.replace("._input_quantizer", "").replace("._weight_quantizer", "")
|
||||
if layer_name not in quant_layer_names:
|
||||
quant_layer_names.append(layer_name)
|
||||
for i, quant_layer in enumerate(quant_layer_names):
|
||||
print("Enable", quant_layer)
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith("_quantizer") and quant_layer in name:
|
||||
module.enable()
|
||||
print(F"{name:40}: {module}")
|
||||
with torch.no_grad():
|
||||
evaluate(model, criterion, data_loader_test, device="cuda")
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith("_quantizer") and quant_layer in name:
|
||||
module.disable()
|
||||
print(F"{name:40}: {module}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
res = main(sys.argv[1:])
|
||||
exit(res)
|
||||
@@ -0,0 +1,18 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
from . import classification
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
from .resnet import *
|
||||
|
||||
@@ -0,0 +1,495 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# BSD 3-Clause License
|
||||
#
|
||||
# Copyright (c) Soumith Chintala 2016,
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# * Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import torch
|
||||
from torch import Tensor
|
||||
import torch.nn as nn
|
||||
from torch.hub import load_state_dict_from_url
|
||||
from typing import Type, Any, Callable, Union, List, Optional
|
||||
from pytorch_quantization import quant_modules
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
|
||||
__all__ = [
|
||||
'ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101', 'resnet152', 'resnext50_32x4d', 'resnext101_32x8d',
|
||||
'wide_resnet50_2', 'wide_resnet101_2'
|
||||
]
|
||||
|
||||
model_urls = {
|
||||
'resnet18': 'https://download.pytorch.org/models/resnet18-f37072fd.pth',
|
||||
'resnet34': 'https://download.pytorch.org/models/resnet34-b627a593.pth',
|
||||
'resnet50': 'https://download.pytorch.org/models/resnet50-0676ba61.pth',
|
||||
'resnet101': 'https://download.pytorch.org/models/resnet101-63fe2227.pth',
|
||||
'resnet152': 'https://download.pytorch.org/models/resnet152-394f9c45.pth',
|
||||
'resnext50_32x4d': 'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth',
|
||||
'resnext101_32x8d': 'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth',
|
||||
'wide_resnet50_2': 'https://download.pytorch.org/models/wide_resnet50_2-95faca4d.pth',
|
||||
'wide_resnet101_2': 'https://download.pytorch.org/models/wide_resnet101_2-32ee1156.pth',
|
||||
}
|
||||
|
||||
|
||||
def conv3x3(in_planes: int,
|
||||
out_planes: int,
|
||||
stride: int = 1,
|
||||
groups: int = 1,
|
||||
dilation: int = 1,
|
||||
quantize: bool = False) -> nn.Conv2d:
|
||||
"""3x3 convolution with padding"""
|
||||
if quantize:
|
||||
return quant_nn.QuantConv2d(in_planes,
|
||||
out_planes,
|
||||
kernel_size=3,
|
||||
stride=stride,
|
||||
padding=dilation,
|
||||
groups=groups,
|
||||
bias=False,
|
||||
dilation=dilation)
|
||||
else:
|
||||
return nn.Conv2d(in_planes,
|
||||
out_planes,
|
||||
kernel_size=3,
|
||||
stride=stride,
|
||||
padding=dilation,
|
||||
groups=groups,
|
||||
bias=False,
|
||||
dilation=dilation)
|
||||
|
||||
|
||||
def conv1x1(in_planes: int, out_planes: int, stride: int = 1, quantize: bool = False) -> nn.Conv2d:
|
||||
"""1x1 convolution"""
|
||||
if quantize:
|
||||
return quant_nn.QuantConv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
|
||||
else:
|
||||
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
|
||||
|
||||
|
||||
class BasicBlock(nn.Module):
|
||||
expansion: int = 1
|
||||
|
||||
def __init__(self,
|
||||
inplanes: int,
|
||||
planes: int,
|
||||
stride: int = 1,
|
||||
downsample: Optional[nn.Module] = None,
|
||||
groups: int = 1,
|
||||
base_width: int = 64,
|
||||
dilation: int = 1,
|
||||
norm_layer: Optional[Callable[..., nn.Module]] = None,
|
||||
quantize: bool = False) -> None:
|
||||
super(BasicBlock, self).__init__()
|
||||
if norm_layer is None:
|
||||
norm_layer = nn.BatchNorm2d
|
||||
if groups != 1 or base_width != 64:
|
||||
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
|
||||
if dilation > 1:
|
||||
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
|
||||
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
|
||||
self.conv1 = conv3x3(inplanes, planes, stride, quantize=quantize)
|
||||
self.bn1 = norm_layer(planes)
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
self.conv2 = conv3x3(planes, planes, quantize=quantize)
|
||||
self.bn2 = norm_layer(planes)
|
||||
self.downsample = downsample
|
||||
self.stride = stride
|
||||
self._quantize = quantize
|
||||
if self._quantize:
|
||||
self.residual_quantizer = quant_nn.TensorQuantizer(quant_nn.QuantConv2d.default_quant_desc_input)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
identity = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
|
||||
out = self.conv2(out)
|
||||
out = self.bn2(out)
|
||||
|
||||
if self.downsample is not None:
|
||||
identity = self.downsample(x)
|
||||
|
||||
if self._quantize:
|
||||
out += self.residual_quantizer(identity)
|
||||
else:
|
||||
out += identity
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class Bottleneck(nn.Module):
|
||||
# Bottleneck in torchvision places the stride for downsampling at 3x3 convolution(self.conv2)
|
||||
# while original implementation places the stride at the first 1x1 convolution(self.conv1)
|
||||
# according to "Deep residual learning for image recognition"https://arxiv.org/abs/1512.03385.
|
||||
# This variant is also known as ResNet V1.5 and improves accuracy according to
|
||||
# https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch.
|
||||
|
||||
expansion: int = 4
|
||||
|
||||
def __init__(self,
|
||||
inplanes: int,
|
||||
planes: int,
|
||||
stride: int = 1,
|
||||
downsample: Optional[nn.Module] = None,
|
||||
groups: int = 1,
|
||||
base_width: int = 64,
|
||||
dilation: int = 1,
|
||||
norm_layer: Optional[Callable[..., nn.Module]] = None,
|
||||
quantize: bool = False) -> None:
|
||||
super(Bottleneck, self).__init__()
|
||||
if norm_layer is None:
|
||||
norm_layer = nn.BatchNorm2d
|
||||
width = int(planes * (base_width / 64.)) * groups
|
||||
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
|
||||
self.conv1 = conv1x1(inplanes, width, quantize=quantize)
|
||||
self.bn1 = norm_layer(width)
|
||||
self.conv2 = conv3x3(width, width, stride, groups, dilation, quantize=quantize)
|
||||
self.bn2 = norm_layer(width)
|
||||
self.conv3 = conv1x1(width, planes * self.expansion, quantize=quantize)
|
||||
self.bn3 = norm_layer(planes * self.expansion)
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
self.downsample = downsample
|
||||
self.stride = stride
|
||||
self._quantize = quantize
|
||||
if self._quantize:
|
||||
self.residual_quantizer = quant_nn.TensorQuantizer(quant_nn.QuantConv2d.default_quant_desc_input)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
identity = x
|
||||
|
||||
out = self.conv1(x)
|
||||
out = self.bn1(out)
|
||||
out = self.relu(out)
|
||||
|
||||
out = self.conv2(out)
|
||||
out = self.bn2(out)
|
||||
out = self.relu(out)
|
||||
|
||||
out = self.conv3(out)
|
||||
out = self.bn3(out)
|
||||
|
||||
if self.downsample is not None:
|
||||
identity = self.downsample(x)
|
||||
|
||||
if self._quantize:
|
||||
out += self.residual_quantizer(identity)
|
||||
else:
|
||||
out += identity
|
||||
out = self.relu(out)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class ResNet(nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
block: Type[Union[BasicBlock, Bottleneck]],
|
||||
layers: List[int],
|
||||
quantize: bool = False,
|
||||
num_classes: int = 1000,
|
||||
zero_init_residual: bool = False,
|
||||
groups: int = 1,
|
||||
width_per_group: int = 64,
|
||||
replace_stride_with_dilation: Optional[List[bool]] = None,
|
||||
norm_layer: Optional[Callable[..., nn.Module]] = None) -> None:
|
||||
super(ResNet, self).__init__()
|
||||
self._quantize = quantize
|
||||
|
||||
if norm_layer is None:
|
||||
norm_layer = nn.BatchNorm2d
|
||||
self._norm_layer = norm_layer
|
||||
|
||||
self.inplanes = 64
|
||||
self.dilation = 1
|
||||
if replace_stride_with_dilation is None:
|
||||
# each element in the tuple indicates if we should replace
|
||||
# the 2x2 stride with a dilated convolution instead
|
||||
replace_stride_with_dilation = [False, False, False]
|
||||
if len(replace_stride_with_dilation) != 3:
|
||||
raise ValueError("replace_stride_with_dilation should be None "
|
||||
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
|
||||
self.groups = groups
|
||||
self.base_width = width_per_group
|
||||
|
||||
if quantize:
|
||||
self.conv1 = quant_nn.QuantConv2d(3,
|
||||
self.inplanes,
|
||||
kernel_size=7,
|
||||
stride=2,
|
||||
padding=3,
|
||||
bias=False)
|
||||
else:
|
||||
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)
|
||||
|
||||
self.bn1 = norm_layer(self.inplanes)
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
|
||||
self.layer1 = self._make_layer(block, 64, layers[0], quantize=quantize)
|
||||
self.layer2 = self._make_layer(block,
|
||||
128,
|
||||
layers[1],
|
||||
stride=2,
|
||||
dilate=replace_stride_with_dilation[0],
|
||||
quantize=quantize)
|
||||
self.layer3 = self._make_layer(block,
|
||||
256,
|
||||
layers[2],
|
||||
stride=2,
|
||||
dilate=replace_stride_with_dilation[1],
|
||||
quantize=quantize)
|
||||
self.layer4 = self._make_layer(block,
|
||||
512,
|
||||
layers[3],
|
||||
stride=2,
|
||||
dilate=replace_stride_with_dilation[2],
|
||||
quantize=quantize)
|
||||
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
|
||||
|
||||
if quantize:
|
||||
self.fc = quant_nn.QuantLinear(512 * block.expansion, num_classes)
|
||||
else:
|
||||
self.fc = nn.Linear(512 * block.expansion, num_classes)
|
||||
|
||||
for m in self.modules():
|
||||
if isinstance(m, nn.Conv2d):
|
||||
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
||||
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
|
||||
nn.init.constant_(m.weight, 1)
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
# Zero-initialize the last BN in each residual branch,
|
||||
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
|
||||
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
|
||||
if zero_init_residual:
|
||||
for m in self.modules():
|
||||
if isinstance(m, Bottleneck):
|
||||
nn.init.constant_(m.bn3.weight, 0) # type: ignore[arg-type]
|
||||
elif isinstance(m, BasicBlock):
|
||||
nn.init.constant_(m.bn2.weight, 0) # type: ignore[arg-type]
|
||||
|
||||
def _make_layer(self,
|
||||
block: Type[Union[BasicBlock, Bottleneck]],
|
||||
planes: int,
|
||||
blocks: int,
|
||||
stride: int = 1,
|
||||
dilate: bool = False,
|
||||
quantize: bool = False) -> nn.Sequential:
|
||||
norm_layer = self._norm_layer
|
||||
downsample = None
|
||||
previous_dilation = self.dilation
|
||||
if dilate:
|
||||
self.dilation *= stride
|
||||
stride = 1
|
||||
if stride != 1 or self.inplanes != planes * block.expansion:
|
||||
downsample = nn.Sequential(
|
||||
conv1x1(self.inplanes, planes * block.expansion, stride, quantize=quantize),
|
||||
norm_layer(planes * block.expansion),
|
||||
)
|
||||
|
||||
layers = []
|
||||
layers.append(
|
||||
block(self.inplanes, planes, stride, downsample, self.groups, self.base_width, previous_dilation,
|
||||
norm_layer, self._quantize))
|
||||
self.inplanes = planes * block.expansion
|
||||
for _ in range(1, blocks):
|
||||
layers.append(
|
||||
block(self.inplanes,
|
||||
planes,
|
||||
groups=self.groups,
|
||||
base_width=self.base_width,
|
||||
dilation=self.dilation,
|
||||
norm_layer=norm_layer,
|
||||
quantize=quantize))
|
||||
|
||||
return nn.Sequential(*layers)
|
||||
|
||||
def _get_dtype(self):
|
||||
return self.conv1.weight.dtype
|
||||
|
||||
def _forward_impl(self, x: Tensor) -> Tensor:
|
||||
# See note [TorchScript super()]
|
||||
x = self.conv1(x)
|
||||
x = self.bn1(x)
|
||||
x = self.relu(x)
|
||||
x = self.maxpool(x)
|
||||
|
||||
x = self.layer1(x)
|
||||
x = self.layer2(x)
|
||||
x = self.layer3(x)
|
||||
x = self.layer4(x)
|
||||
|
||||
x = self.avgpool(x)
|
||||
x = torch.flatten(x, 1)
|
||||
x = self.fc(x)
|
||||
|
||||
return x
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
return self._forward_impl(x.to(self._get_dtype()))
|
||||
|
||||
|
||||
def _resnet(arch: str, block: Type[Union[BasicBlock, Bottleneck]], layers: List[int], pretrained: bool, progress: bool,
|
||||
quantize: bool, **kwargs: Any) -> ResNet:
|
||||
model = ResNet(block, layers, quantize, **kwargs)
|
||||
if pretrained:
|
||||
state_dict = load_state_dict_from_url(model_urls[arch], progress=progress)
|
||||
model.load_state_dict(state_dict)
|
||||
return model
|
||||
|
||||
|
||||
def resnet18(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""ResNet-18 model from
|
||||
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress, quantize, **kwargs)
|
||||
|
||||
|
||||
def resnet34(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""ResNet-34 model from
|
||||
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress, quantize, **kwargs)
|
||||
|
||||
|
||||
def resnet50(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""ResNet-50 model from
|
||||
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress, quantize, **kwargs)
|
||||
|
||||
|
||||
def resnet101(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""ResNet-101 model from
|
||||
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained, progress, quantize, **kwargs)
|
||||
|
||||
|
||||
def resnet152(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""ResNet-152 model from
|
||||
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained, progress, quantize, **kwargs)
|
||||
|
||||
|
||||
def resnext50_32x4d(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""ResNeXt-50 32x4d model from
|
||||
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
kwargs['groups'] = 32
|
||||
kwargs['width_per_group'] = 4
|
||||
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3], pretrained, progress, quantize, **kwargs)
|
||||
|
||||
|
||||
def resnext101_32x8d(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""ResNeXt-101 32x8d model from
|
||||
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
kwargs['groups'] = 32
|
||||
kwargs['width_per_group'] = 8
|
||||
return _resnet('resnext101_32x8d', Bottleneck, [3, 4, 23, 3], pretrained, progress, quantize, **kwargs)
|
||||
|
||||
|
||||
def wide_resnet50_2(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""Wide ResNet-50-2 model from
|
||||
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_.
|
||||
|
||||
The model is the same as ResNet except for the bottleneck number of channels
|
||||
which is twice larger in every block. The number of channels in outer 1x1
|
||||
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
|
||||
channels, and in Wide ResNet-50-2 has 2048-1024-2048.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
kwargs['width_per_group'] = 64 * 2
|
||||
return _resnet('wide_resnet50_2', Bottleneck, [3, 4, 6, 3], pretrained, progress, quantize, **kwargs)
|
||||
|
||||
|
||||
def wide_resnet101_2(pretrained: bool = False, progress: bool = True, quantize: bool = False, **kwargs: Any) -> ResNet:
|
||||
r"""Wide ResNet-101-2 model from
|
||||
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_.
|
||||
|
||||
The model is the same as ResNet except for the bottleneck number of channels
|
||||
which is twice larger in every block. The number of channels in outer 1x1
|
||||
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
|
||||
channels, and in Wide ResNet-50-2 has 2048-1024-2048.
|
||||
|
||||
Args:
|
||||
pretrained (bool): If True, returns a model pre-trained on ImageNet
|
||||
progress (bool): If True, displays a progress bar of the download to stderr
|
||||
"""
|
||||
kwargs['width_per_group'] = 64 * 2
|
||||
return _resnet('wide_resnet101_2', Bottleneck, [3, 4, 23, 3], pretrained, progress, quantize, **kwargs)
|
||||
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from absl import logging
|
||||
from .version import __version__
|
||||
from .quant_modules import *
|
||||
|
||||
logging.use_absl_handler()
|
||||
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""``pytorch_quantization.calib`` provides Calibrator classes that
|
||||
collect data statistics and determine pytorch_quantization parameters.
|
||||
"""
|
||||
|
||||
from .max import MaxCalibrator
|
||||
from .histogram import *
|
||||
@@ -0,0 +1,61 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Abstract base class for calibrators"""
|
||||
|
||||
|
||||
class _Calibrator():
|
||||
"""Abstract base class of calibrators
|
||||
Args:
|
||||
num_bits: An integer. Number of bits of quantization.
|
||||
axis: A tuple. see QuantDescriptor.
|
||||
unsigned: A boolean. using unsigned quantization.
|
||||
|
||||
Readonly Properties:
|
||||
axis:
|
||||
"""
|
||||
def __init__(self, num_bits, axis, unsigned):
|
||||
self._num_bits = num_bits
|
||||
self._axis = axis
|
||||
self._unsigned = unsigned
|
||||
|
||||
def collect(self, x):
|
||||
"""Abstract method: collect tensor statistics used to compute amax
|
||||
|
||||
Args:
|
||||
x: A tensor
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def reset(self):
|
||||
"""Abstract method: reset calibrator to initial state"""
|
||||
raise NotImplementedError
|
||||
|
||||
def compute_amax(self, *args, **kwargs):
|
||||
"""Abstract method: compute the amax from the collected data
|
||||
|
||||
Returns:
|
||||
amax: a tensor
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
s = "num_bits={_num_bits}"
|
||||
s += " axis={_axis}"
|
||||
s += " unsigned={_unsigned}"
|
||||
return s.format(**self.__dict__)
|
||||
@@ -0,0 +1,386 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
"""Histogram based calibrators"""
|
||||
from collections import Counter
|
||||
import numpy as np
|
||||
from scipy.stats import entropy
|
||||
|
||||
from absl import logging
|
||||
|
||||
import torch
|
||||
|
||||
from pytorch_quantization.calib.calibrator import _Calibrator
|
||||
from pytorch_quantization.tensor_quant import fake_tensor_quant, scaled_e4m3
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
|
||||
__all__ = ["HistogramCalibrator", "calibrate_weights"]
|
||||
|
||||
|
||||
class HistogramCalibrator(_Calibrator):
|
||||
"""Unified histogram calibrator
|
||||
|
||||
Histogram will be only collected once. compute_amax() performs entropy, percentile, or mse
|
||||
calibration based on arguments
|
||||
|
||||
Args:
|
||||
num_bits: An integer. Number of bits of quantization.
|
||||
axis: A tuple. see QuantDescriptor.
|
||||
unsigned: A boolean. using unsigned quantization.
|
||||
num_bins: An integer. Number of histograms bins. Default 2048.
|
||||
grow_method: A string. DEPRECATED. default None.
|
||||
skip_zeros: A boolean. If True, skips zeros when collecting data for histogram. Default False.
|
||||
torch_hist: A boolean. If True, collect histogram by torch.histc instead of np.histogram. If input tensor
|
||||
is on GPU, histc will also be running on GPU. Default True.
|
||||
"""
|
||||
|
||||
def __init__(self, num_bits, axis, unsigned, num_bins=2048, grow_method=None, skip_zeros=False, torch_hist=True):
|
||||
super(HistogramCalibrator, self).__init__(num_bits, axis, unsigned)
|
||||
self._num_bins = num_bins
|
||||
self._skip_zeros = skip_zeros
|
||||
|
||||
self._calib_bin_edges = None
|
||||
self._calib_hist = None
|
||||
|
||||
self._torch_hist = torch_hist
|
||||
|
||||
if axis is not None:
|
||||
raise NotImplementedError("Calibrator histogram collection only supports per tensor scaling")
|
||||
|
||||
if grow_method is not None:
|
||||
logging.warning("grow_method is deprecated. Got %s, ingored!", grow_method)
|
||||
|
||||
def collect(self, x):
|
||||
"""Collect histogram"""
|
||||
if torch.min(x) < 0.:
|
||||
logging.log_first_n(logging.INFO,
|
||||
("Calibrator encountered negative values. It shouldn't happen after ReLU. "
|
||||
"Make sure this is the right tensor to calibrate."), 1)
|
||||
x = x.abs()
|
||||
|
||||
x = x.float()
|
||||
|
||||
if not self._torch_hist:
|
||||
x_np = x.cpu().detach().numpy()
|
||||
|
||||
if self._skip_zeros:
|
||||
x_np = x_np[np.where(x_np != 0)]
|
||||
|
||||
if self._calib_bin_edges is None and self._calib_hist is None:
|
||||
# first time it uses num_bins to compute histogram.
|
||||
self._calib_hist, self._calib_bin_edges = np.histogram(x_np, bins=self._num_bins)
|
||||
else:
|
||||
temp_amax = np.max(x_np)
|
||||
if temp_amax > self._calib_bin_edges[-1]:
|
||||
# increase the number of bins
|
||||
width = self._calib_bin_edges[1] - self._calib_bin_edges[0]
|
||||
# NOTE: np.arange may create an extra bin after the one containing temp_amax
|
||||
new_bin_edges = np.arange(self._calib_bin_edges[-1] + width, temp_amax + width, width)
|
||||
self._calib_bin_edges = np.hstack((self._calib_bin_edges, new_bin_edges))
|
||||
hist, self._calib_bin_edges = np.histogram(x_np, bins=self._calib_bin_edges)
|
||||
hist[:len(self._calib_hist)] += self._calib_hist
|
||||
self._calib_hist = hist
|
||||
else:
|
||||
# This branch of code is designed to match numpy version as close as possible
|
||||
with torch.no_grad():
|
||||
if self._skip_zeros:
|
||||
x = x[torch.where(x != 0)]
|
||||
|
||||
# Because we collect histogram on absolute value, setting min=0 simplifying the rare case where
|
||||
# minimum value is not exactly 0 and first batch collected has larger min value than later batches
|
||||
x_max = x.max()
|
||||
if self._calib_bin_edges is None and self._calib_hist is None:
|
||||
self._calib_hist = torch.histc(x, bins=self._num_bins, min=0, max=x_max)
|
||||
self._calib_bin_edges = torch.linspace(0, x_max, self._num_bins + 1)
|
||||
else:
|
||||
if x_max > self._calib_bin_edges[-1]:
|
||||
width = self._calib_bin_edges[1] - self._calib_bin_edges[0]
|
||||
self._num_bins = int((x_max / width).ceil().item())
|
||||
self._calib_bin_edges = torch.arange(0, x_max + width, width, device=x.device)
|
||||
|
||||
hist = torch.histc(x, bins=self._num_bins, min=0, max=self._calib_bin_edges[-1])
|
||||
hist[:self._calib_hist.numel()] += self._calib_hist
|
||||
self._calib_hist = hist
|
||||
|
||||
def reset(self):
|
||||
"""Reset the collected histogram"""
|
||||
self._calib_bin_edges = None
|
||||
self._calib_hist = None
|
||||
|
||||
def compute_amax(self, method: str, *, stride: int = 1, start_bin: int = 128, percentile: float = 99.99):
|
||||
"""Compute the amax from the collected histogram
|
||||
|
||||
Args:
|
||||
method: A string. One of ['entropy', 'mse', 'percentile']
|
||||
|
||||
Keyword Arguments:
|
||||
stride: An integer. Default 1
|
||||
start_bin: An integer. Default 128
|
||||
percentils: A float number between [0, 100]. Default 99.99.
|
||||
|
||||
Returns:
|
||||
amax: a tensor
|
||||
"""
|
||||
if isinstance(self._calib_hist, torch.Tensor):
|
||||
calib_hist = self._calib_hist.to(torch.int64).cpu().numpy()
|
||||
calib_bin_edges = self._calib_bin_edges.cpu().numpy()
|
||||
else:
|
||||
calib_hist = self._calib_hist
|
||||
calib_bin_edges = self._calib_bin_edges
|
||||
|
||||
if method == 'entropy':
|
||||
calib_amax = _compute_amax_entropy(calib_hist, calib_bin_edges, self._num_bits, self._unsigned, stride,
|
||||
start_bin)
|
||||
elif method == 'mse':
|
||||
calib_amax = _compute_amax_mse(calib_hist, calib_bin_edges, self._num_bits, self._unsigned, stride,
|
||||
start_bin)
|
||||
elif method == 'percentile':
|
||||
calib_amax = _compute_amax_percentile(calib_hist, calib_bin_edges, percentile)
|
||||
else:
|
||||
raise TypeError("Unknown calibration method {}".format(method))
|
||||
|
||||
return calib_amax
|
||||
|
||||
# pylint:disable=missing-docstring
|
||||
def __str__(self):
|
||||
s = "HistogramCalibrator("
|
||||
if self._calib_bin_edges is None:
|
||||
bin_edge_str = "None"
|
||||
else:
|
||||
bin_edge_str = "[{:.3f}, ..., {:.3f}]({})".format(self._calib_bin_edges[0], self._calib_bin_edges[-1],
|
||||
len(self._calib_bin_edges))
|
||||
s += "calib_bin_edges={})".format(bin_edge_str)
|
||||
return s
|
||||
|
||||
def __repr__(self):
|
||||
s = "HistogramCalibrator("
|
||||
s += super(HistogramCalibrator, self).__repr__()
|
||||
s += " calib_bin_edges={_calib_bin_edges}"
|
||||
s += " calib_hist={_calib_hist})"
|
||||
return s.format(**self.__dict__)
|
||||
|
||||
# pylint:enable=missing-docstring
|
||||
|
||||
|
||||
# Ideally, we want to decouple collector (collect histogram) and calibrator (compute amax) as opposed to
|
||||
# the current calibrator design. The following compute amax functions are broken out from the calibrator
|
||||
# as first step towards there.
|
||||
def _compute_amax_entropy(calib_hist, calib_bin_edges, num_bits, unsigned, stride=1, start_bin=128):
|
||||
"""Returns amax that minimizes KL-Divergence of the collected histogram"""
|
||||
|
||||
# If calibrator hasn't collected any data, return none
|
||||
if calib_bin_edges is None and calib_hist is None:
|
||||
return None
|
||||
|
||||
def _normalize_distr(distr):
|
||||
summ = np.sum(distr)
|
||||
if summ != 0:
|
||||
distr = distr / summ
|
||||
|
||||
bins = calib_hist[:]
|
||||
bins[0] = bins[1]
|
||||
|
||||
total_data = np.sum(bins)
|
||||
|
||||
divergences = []
|
||||
arguments = []
|
||||
|
||||
# we are quantizing to 128 values + sign if num_bits=8
|
||||
nbins = 1 << (num_bits - 1 + int(unsigned))
|
||||
|
||||
starting = start_bin
|
||||
stop = len(bins)
|
||||
|
||||
new_density_counts = np.zeros(nbins, dtype=np.float64)
|
||||
|
||||
for i in range(starting, stop + 1, stride):
|
||||
new_density_counts.fill(0)
|
||||
space = np.linspace(0, i, num=nbins + 1)
|
||||
digitized_space = np.digitize(range(i), space) - 1
|
||||
|
||||
digitized_space[bins[:i] == 0] = -1
|
||||
|
||||
for idx, digitized in enumerate(digitized_space):
|
||||
if digitized != -1:
|
||||
new_density_counts[digitized] += bins[idx]
|
||||
|
||||
counter = Counter(digitized_space)
|
||||
for key, val in counter.items():
|
||||
if key != -1:
|
||||
new_density_counts[key] = new_density_counts[key] / val
|
||||
|
||||
new_density = np.zeros(i, dtype=np.float64)
|
||||
for idx, digitized in enumerate(digitized_space):
|
||||
if digitized != -1:
|
||||
new_density[idx] = new_density_counts[digitized]
|
||||
|
||||
total_counts_new = np.sum(new_density) + np.sum(bins[i:])
|
||||
_normalize_distr(new_density)
|
||||
|
||||
reference_density = np.array(bins[:len(digitized_space)])
|
||||
reference_density[-1] += np.sum(bins[i:])
|
||||
|
||||
total_counts_old = np.sum(reference_density)
|
||||
if round(total_counts_new) != total_data or round(total_counts_old) != total_data:
|
||||
raise RuntimeError("Count mismatch! total_counts_new={}, total_counts_old={}, total_data={}".format(
|
||||
total_counts_new, total_counts_old, total_data))
|
||||
|
||||
_normalize_distr(reference_density)
|
||||
|
||||
ent = entropy(reference_density, new_density)
|
||||
divergences.append(ent)
|
||||
arguments.append(i)
|
||||
|
||||
divergences = np.array(divergences)
|
||||
logging.debug("divergences={}".format(divergences))
|
||||
last_argmin = len(divergences) - 1 - np.argmin(divergences[::-1])
|
||||
calib_amax = calib_bin_edges[last_argmin * stride + starting]
|
||||
calib_amax = torch.tensor(calib_amax.item()) #pylint: disable=not-callable
|
||||
|
||||
return calib_amax
|
||||
|
||||
|
||||
def _compute_amax_mse(calib_hist, calib_bin_edges, num_bits, unsigned, stride=1, start_bin=128):
|
||||
"""Returns amax that minimizes MSE of the collected histogram"""
|
||||
|
||||
# If calibrator hasn't collected any data, return none
|
||||
if calib_bin_edges is None and calib_hist is None:
|
||||
return None
|
||||
|
||||
counts = torch.from_numpy(calib_hist[:]).float().cuda()
|
||||
edges = torch.from_numpy(calib_bin_edges[:]).float().cuda()
|
||||
centers = (edges[1:] + edges[:-1]) / 2
|
||||
|
||||
mses = []
|
||||
arguments = []
|
||||
|
||||
for i in range(start_bin, len(centers), stride):
|
||||
|
||||
amax = centers[i]
|
||||
if isinstance(num_bits, int) and num_bits >= 0:
|
||||
if num_bits == 0:
|
||||
logging.error("num_bits is 0. This will result in the tensor being quantized to all zeros."
|
||||
" This mode should only be used for debugging purposes.")
|
||||
quant_centers = fake_tensor_quant(centers, amax, num_bits, unsigned)
|
||||
elif num_bits == (4, 3):
|
||||
quant_centers = scaled_e4m3(centers, amax, num_bits[0], num_bits[1])
|
||||
else:
|
||||
raise TypeError("Invalid num_bits. num_bits must be a postivie integer or tuple (4,3).")
|
||||
|
||||
mse = ((quant_centers - centers)**2 * counts).mean()
|
||||
|
||||
mses.append(mse.cpu())
|
||||
arguments.append(i)
|
||||
|
||||
logging.debug("mses={}".format(mses))
|
||||
argmin = np.argmin(mses)
|
||||
calib_amax = centers[arguments[argmin]]
|
||||
|
||||
return calib_amax
|
||||
|
||||
|
||||
def _compute_amax_percentile(calib_hist, calib_bin_edges, percentile):
|
||||
"""Returns amax that clips the percentile fraction of collected data"""
|
||||
|
||||
if percentile < 0 or percentile > 100:
|
||||
raise ValueError("Invalid percentile. Must be in range 0 <= percentile <= 100.")
|
||||
|
||||
# If calibrator hasn't collected any data, return none
|
||||
if calib_bin_edges is None and calib_hist is None:
|
||||
return None
|
||||
|
||||
total = calib_hist.sum()
|
||||
cdf = np.cumsum(calib_hist / total)
|
||||
idx = np.searchsorted(cdf, percentile / 100)
|
||||
calib_amax = calib_bin_edges[idx]
|
||||
calib_amax = torch.tensor(calib_amax.item()) #pylint: disable=not-callable
|
||||
|
||||
return calib_amax
|
||||
|
||||
|
||||
def calibrate_weights(model, method="percentile", perchannel=True, percentile=99.99, num_bins=2048):
|
||||
"""Calibrate weights of all child quantized modules
|
||||
|
||||
Ideally, we would split calibration functionality to histogram collector and calibrator which
|
||||
takes histogram and compute amax. But since we haven't decoupled collector and calibrator, it
|
||||
is easier to create a separate function to calibrate weight.
|
||||
|
||||
.. note::
|
||||
This function uses `method` specified by the argument to decide which method to use, NOT the one
|
||||
specified by the calibrator embedded in weight_quantizer.
|
||||
We haven't moved calibration to GPU, so everything is transfered to CPU
|
||||
|
||||
Args:
|
||||
model: A torch.nn.Module.
|
||||
method: A string of calibration method. Supports "mse" and "percentile". Default "percentile"
|
||||
perchannel: A bool. Set channel/neuron axis if True. Default True.
|
||||
percentile: A float. Default 99.99
|
||||
num_bins: A integer. Number of bins of histogram. Default 2048.
|
||||
|
||||
"""
|
||||
for name, module in model.named_modules():
|
||||
if hasattr(module, "weight") and hasattr(module, "weight_quantizer"):
|
||||
logging.info("Calibrate weight of %s", name)
|
||||
num_bits = module.weight_quantizer.num_bits
|
||||
unsigned = module.weight_quantizer.unsigned
|
||||
channel_second_modules = (quant_nn.QuantConvTranspose1d, quant_nn.QuantConvTranspose2d,
|
||||
quant_nn.QuantConvTranspose3d)
|
||||
if perchannel:
|
||||
axis = 1 if isinstance(module, channel_second_modules) else 0
|
||||
else:
|
||||
axis = None
|
||||
axis_size = module.weight.shape[axis] if axis is not None else 1
|
||||
|
||||
# Histogram is always collected even if method is "max". Although "max" is supported here
|
||||
# but it is not the primary usage of this function
|
||||
if axis is None:
|
||||
input_weights = module.weight.abs().cpu().detach().numpy()
|
||||
calib_hist, calib_bin_edges = np.histogram(input_weights, bins=2048, range=(0, input_weights.max()))
|
||||
calib_hist = [calib_hist]
|
||||
calib_bin_edges = [calib_bin_edges]
|
||||
else:
|
||||
calib_hist = []
|
||||
calib_bin_edges = []
|
||||
for i in range(axis_size):
|
||||
input_weights = module.weight.index_select(axis, torch.tensor(
|
||||
i, device=module.weight.device)).abs().cpu().detach().numpy()
|
||||
hist, bin_edges = np.histogram(input_weights, bins=num_bins, range=(0, input_weights.max()))
|
||||
calib_hist.append(hist)
|
||||
calib_bin_edges.append(bin_edges)
|
||||
|
||||
calib_amax = []
|
||||
if method == "max":
|
||||
reduce_axis = list(range(module.weight.dim()))
|
||||
reduce_axis.remove(axis)
|
||||
calib_amax.append(quant_utils.reduce_amax(module.weight, axis=reduce_axis))
|
||||
elif method == 'mse':
|
||||
for i in range(axis_size):
|
||||
calib_amax.append(_compute_amax_mse(calib_hist[i], calib_bin_edges[i], num_bits, unsigned))
|
||||
elif method == 'percentile':
|
||||
for i in range(axis_size):
|
||||
calib_amax.append(_compute_amax_percentile(calib_hist[i], calib_bin_edges[i], percentile))
|
||||
else:
|
||||
raise TypeError("Unsupported calibration method {}".format(method))
|
||||
|
||||
if axis is None:
|
||||
calib_amax = calib_amax[0]
|
||||
else:
|
||||
calib_amax_shape = [1] * module.weight.dim()
|
||||
calib_amax_shape[axis] = module.weight.shape[axis]
|
||||
calib_amax = torch.stack(calib_amax).reshape(calib_amax_shape)
|
||||
module.weight_quantizer.amax = calib_amax.detach().cpu().numpy()
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Calibrator that returns the absolute max of all collected tensors"""
|
||||
from absl import logging
|
||||
import torch
|
||||
|
||||
from pytorch_quantization.calib.calibrator import _Calibrator
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
|
||||
class MaxCalibrator(_Calibrator):
|
||||
"""Max calibrator, tracks the maximum value globally
|
||||
|
||||
Args:
|
||||
calib_desc: A MaxCalibDescriptor.
|
||||
num_bits: An integer. Number of bits of quantization.
|
||||
axis: A tuple. see QuantDescriptor.
|
||||
unsigned: A boolean. using unsigned quantization.
|
||||
|
||||
Readonly Properties:
|
||||
amaxs: A list of amax. Numpy array is saved as it is likely to be used for some plot.
|
||||
"""
|
||||
def __init__(self, num_bits, axis, unsigned, track_amax=False):
|
||||
super(MaxCalibrator, self).__init__(num_bits, axis, unsigned)
|
||||
self._track_amax = track_amax
|
||||
if self._track_amax:
|
||||
self._amaxs = [] # shall we have a better name?
|
||||
self._calib_amax = None
|
||||
|
||||
# pylint:disable=missing-docstring
|
||||
@property
|
||||
def amaxs(self):
|
||||
return self._amaxs
|
||||
# pylint:enable=missing-docstring
|
||||
|
||||
def collect(self, x):
|
||||
"""Tracks the absolute max of all tensors
|
||||
|
||||
Args:
|
||||
x: A tensor
|
||||
|
||||
Raises:
|
||||
RuntimeError: If amax shape changes
|
||||
"""
|
||||
if torch.min(x) < 0.:
|
||||
logging.log_first_n(
|
||||
logging.INFO,
|
||||
("Calibrator encountered negative values. It shouldn't happen after ReLU. "
|
||||
"Make sure this is the right tensor to calibrate."),
|
||||
1)
|
||||
x = x.abs()
|
||||
|
||||
# Swap axis to reduce.
|
||||
axis = self._axis if isinstance(self._axis, (list, tuple)) else [self._axis]
|
||||
# Handle negative axis.
|
||||
axis = [x.dim() + i if isinstance(i, int) and i < 0 else i for i in axis]
|
||||
reduce_axis = []
|
||||
for i in range(x.dim()):
|
||||
if not i in axis:
|
||||
reduce_axis.append(i)
|
||||
local_amax = quant_utils.reduce_amax(x, axis=reduce_axis).detach()
|
||||
if self._calib_amax is None:
|
||||
self._calib_amax = local_amax
|
||||
else:
|
||||
if local_amax.shape != self._calib_amax.shape:
|
||||
raise RuntimeError("amax shape changed!")
|
||||
self._calib_amax.copy_(torch.max(self._calib_amax, local_amax).data)
|
||||
|
||||
if self._track_amax:
|
||||
self._amaxs.append(local_amax.cpu().numpy())
|
||||
|
||||
def reset(self):
|
||||
"""Reset the collected absolute max"""
|
||||
self._calib_amax = None
|
||||
|
||||
def compute_amax(self):
|
||||
"""Return the absolute max of all tensors collected"""
|
||||
return self._calib_amax
|
||||
|
||||
# pylint:disable=missing-docstring
|
||||
def __str__(self):
|
||||
s = "MaxCalibrator("
|
||||
s += "track_amax={_track_amax}"
|
||||
s += ")"
|
||||
return s.format(**self.__dict__)
|
||||
|
||||
def __repr__(self):
|
||||
s = "MaxCalibrator("
|
||||
s += super(MaxCalibrator, self).__repr__()
|
||||
s += " calib_amax={_calib_amax}"
|
||||
s += " track_amax={_track_amax}"
|
||||
if self._track_amax:
|
||||
s += " amaxs={_amaxs}"
|
||||
s += ")"
|
||||
return s.format(**self.__dict__)
|
||||
# pylint:enable=missing-docstring
|
||||
@@ -0,0 +1,25 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
from pytorch_quantization.nn.modules.tensor_quantizer import *
|
||||
from pytorch_quantization.nn.modules.quant_conv import *
|
||||
from pytorch_quantization.nn.modules.quant_linear import *
|
||||
from pytorch_quantization.nn.modules.quant_pooling import *
|
||||
from pytorch_quantization.nn.modules.clip import *
|
||||
from pytorch_quantization.nn.modules.quant_rnn import *
|
||||
from pytorch_quantization.nn.modules.quant_instancenorm import *
|
||||
@@ -0,0 +1,286 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""RNN implementation in python
|
||||
Originally copied from https://github.com/pytorch/pytorch/blob/v0.4.1/torch/nn/_functions/rnn.py
|
||||
with following modification
|
||||
fusedBackend is removed
|
||||
CudnnRNN is removed
|
||||
Hack for ONNX in RNN() is removed
|
||||
Only LSTM is quantized. Other paths are excluded in __all__
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from torch.autograd import NestedIOFunction
|
||||
from torch.nn import functional as F
|
||||
import torch
|
||||
import itertools
|
||||
from functools import partial
|
||||
|
||||
__all__ = ["LSTMCell", "RNN"]
|
||||
|
||||
def RNNReLUCell(input, hidden, w_ih, w_hh, b_ih=None, b_hh=None):
|
||||
hy = F.relu(F.linear(input, w_ih, b_ih) + F.linear(hidden, w_hh, b_hh))
|
||||
return hy
|
||||
|
||||
|
||||
def RNNTanhCell(input, hidden, w_ih, w_hh, b_ih=None, b_hh=None):
|
||||
hy = torch.tanh(F.linear(input, w_ih, b_ih) + F.linear(hidden, w_hh, b_hh))
|
||||
return hy
|
||||
|
||||
|
||||
def LSTMCell(input, hidden, w_ih, w_hh, b_ih=None, b_hh=None, input_quantizer=None, weight_quantizer=None):
|
||||
"""Quantized LSTM Cell
|
||||
|
||||
The assumption is at inference time, only one fused gemm will be launched for one time step Weights of 4 gates
|
||||
are fused together, and activation from layer and recurrent paths are fused togather. ``input_quantizer`` will be
|
||||
applied on the fused activation tensor. And ``weight_quantizer`` will be applied on the fused weight tensor.
|
||||
"""
|
||||
|
||||
hx, cx = hidden
|
||||
if input_quantizer is not None:
|
||||
input, hx = input_quantizer(torch.cat([input, hx], 1)).split([input.size()[1], hx.size()[1]], 1)
|
||||
if weight_quantizer is not None:
|
||||
w_ih, w_hh = weight_quantizer(torch.cat([w_ih, w_hh], 1)).split([w_ih.size()[1], w_hh.size()[1]], 1)
|
||||
gates = F.linear(input, w_ih, b_ih) + F.linear(hx, w_hh, b_hh)
|
||||
|
||||
ingate, forgetgate, cellgate, outgate = gates.chunk(4, 1)
|
||||
|
||||
ingate = torch.sigmoid(ingate)
|
||||
forgetgate = torch.sigmoid(forgetgate)
|
||||
cellgate = torch.tanh(cellgate)
|
||||
outgate = torch.sigmoid(outgate)
|
||||
|
||||
cy = (forgetgate * cx) + (ingate * cellgate)
|
||||
hy = outgate * torch.tanh(cy)
|
||||
|
||||
return hy, cy
|
||||
|
||||
|
||||
def GRUCell(input, hidden, w_ih, w_hh, b_ih=None, b_hh=None):
|
||||
gi = F.linear(input, w_ih, b_ih)
|
||||
gh = F.linear(hidden, w_hh, b_hh)
|
||||
i_r, i_i, i_n = gi.chunk(3, 1)
|
||||
h_r, h_i, h_n = gh.chunk(3, 1)
|
||||
|
||||
resetgate = torch.sigmoid(i_r + h_r)
|
||||
inputgate = torch.sigmoid(i_i + h_i)
|
||||
newgate = torch.tanh(i_n + resetgate * h_n)
|
||||
hy = newgate + inputgate * (hidden - newgate)
|
||||
|
||||
return hy
|
||||
|
||||
|
||||
def StackedRNN(inners, num_layers, lstm=False, dropout=0, train=True):
|
||||
|
||||
num_directions = len(inners)
|
||||
total_layers = num_layers * num_directions
|
||||
|
||||
def forward(input, hidden, weight, batch_sizes, input_quantizers, weight_quantizers):
|
||||
assert(len(weight) == total_layers)
|
||||
next_hidden = []
|
||||
|
||||
if lstm:
|
||||
hidden = list(zip(*hidden))
|
||||
|
||||
for i in range(num_layers):
|
||||
all_output = []
|
||||
for j, inner in enumerate(inners):
|
||||
l = i * num_directions + j
|
||||
|
||||
hy, output = inner(input, hidden[l], weight[l], batch_sizes,
|
||||
input_quantizer=input_quantizers[l], weight_quantizer=weight_quantizers[l])
|
||||
next_hidden.append(hy)
|
||||
all_output.append(output)
|
||||
|
||||
input = torch.cat(all_output, input.dim() - 1)
|
||||
|
||||
if dropout != 0 and i < num_layers - 1:
|
||||
input = F.dropout(input, p=dropout, training=train, inplace=False)
|
||||
|
||||
if lstm:
|
||||
next_h, next_c = zip(*next_hidden)
|
||||
next_hidden = (
|
||||
torch.cat(next_h, 0).view(total_layers, *next_h[0].size()),
|
||||
torch.cat(next_c, 0).view(total_layers, *next_c[0].size())
|
||||
)
|
||||
else:
|
||||
next_hidden = torch.cat(next_hidden, 0).view(
|
||||
total_layers, *next_hidden[0].size())
|
||||
|
||||
return next_hidden, input
|
||||
|
||||
return forward
|
||||
|
||||
|
||||
def Recurrent(inner, reverse=False):
|
||||
def forward(input, hidden, weight, batch_sizes, input_quantizer, weight_quantizer):
|
||||
output = []
|
||||
steps = range(input.size(0) - 1, -1, -1) if reverse else range(input.size(0))
|
||||
for i in steps:
|
||||
hidden = inner(input[i], hidden, *weight,
|
||||
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer)
|
||||
# hack to handle LSTM
|
||||
output.append(hidden[0] if isinstance(hidden, tuple) else hidden)
|
||||
|
||||
if reverse:
|
||||
output.reverse()
|
||||
output = torch.cat(output, 0).view(input.size(0), *output[0].size())
|
||||
|
||||
return hidden, output
|
||||
|
||||
return forward
|
||||
|
||||
|
||||
def variable_recurrent_factory(inner, reverse=False):
|
||||
if reverse:
|
||||
return VariableRecurrentReverse(inner)
|
||||
else:
|
||||
return VariableRecurrent(inner)
|
||||
|
||||
|
||||
def VariableRecurrent(inner):
|
||||
def forward(input, hidden, weight, batch_sizes, input_quantizer, weight_quantizer):
|
||||
|
||||
output = []
|
||||
input_offset = 0
|
||||
last_batch_size = batch_sizes[0]
|
||||
hiddens = []
|
||||
flat_hidden = not isinstance(hidden, tuple)
|
||||
if flat_hidden:
|
||||
hidden = (hidden,)
|
||||
for batch_size in batch_sizes:
|
||||
step_input = input[input_offset:input_offset + batch_size]
|
||||
input_offset += batch_size
|
||||
|
||||
dec = last_batch_size - batch_size
|
||||
if dec > 0:
|
||||
hiddens.append(tuple(h[-dec:] for h in hidden))
|
||||
hidden = tuple(h[:-dec] for h in hidden)
|
||||
last_batch_size = batch_size
|
||||
|
||||
if flat_hidden:
|
||||
hidden = (inner(step_input, hidden[0], *weight,
|
||||
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer),)
|
||||
else:
|
||||
hidden = inner(step_input, hidden, *weight,
|
||||
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer)
|
||||
|
||||
output.append(hidden[0])
|
||||
hiddens.append(hidden)
|
||||
hiddens.reverse()
|
||||
|
||||
hidden = tuple(torch.cat(h, 0) for h in zip(*hiddens))
|
||||
assert hidden[0].size(0) == batch_sizes[0]
|
||||
if flat_hidden:
|
||||
hidden = hidden[0]
|
||||
output = torch.cat(output, 0)
|
||||
|
||||
return hidden, output
|
||||
|
||||
return forward
|
||||
|
||||
|
||||
def VariableRecurrentReverse(inner):
|
||||
def forward(input, hidden, weight, batch_sizes, input_quantizer, weight_quantizer):
|
||||
output = []
|
||||
input_offset = input.size(0)
|
||||
last_batch_size = batch_sizes[-1]
|
||||
initial_hidden = hidden
|
||||
flat_hidden = not isinstance(hidden, tuple)
|
||||
if flat_hidden:
|
||||
hidden = (hidden,)
|
||||
initial_hidden = (initial_hidden,)
|
||||
hidden = tuple(h[:batch_sizes[-1]] for h in hidden)
|
||||
for i in reversed(range(len(batch_sizes))):
|
||||
batch_size = batch_sizes[i]
|
||||
inc = batch_size - last_batch_size
|
||||
if inc > 0:
|
||||
hidden = tuple(torch.cat((h, ih[last_batch_size:batch_size]), 0)
|
||||
for h, ih in zip(hidden, initial_hidden))
|
||||
last_batch_size = batch_size
|
||||
step_input = input[input_offset - batch_size:input_offset]
|
||||
input_offset -= batch_size
|
||||
|
||||
if flat_hidden:
|
||||
hidden = (inner(step_input, hidden[0], *weight,
|
||||
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer),)
|
||||
else:
|
||||
hidden = inner(step_input, hidden, *weight,
|
||||
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer)
|
||||
output.append(hidden[0])
|
||||
|
||||
output.reverse()
|
||||
output = torch.cat(output, 0)
|
||||
if flat_hidden:
|
||||
hidden = hidden[0]
|
||||
return hidden, output
|
||||
|
||||
return forward
|
||||
|
||||
|
||||
def AutogradRNN(mode, input_size, hidden_size, num_layers=1, batch_first=False,
|
||||
dropout=0, train=True, bidirectional=False, variable_length=False,
|
||||
dropout_state=None, flat_weight=None,
|
||||
input_quantizers=None, weight_quantizers=None):
|
||||
|
||||
if mode == 'RNN_RELU':
|
||||
cell = RNNReLUCell
|
||||
elif mode == 'RNN_TANH':
|
||||
cell = RNNTanhCell
|
||||
elif mode == 'LSTM':
|
||||
cell = LSTMCell
|
||||
elif mode == 'GRU':
|
||||
cell = GRUCell
|
||||
else:
|
||||
raise Exception('Unknown mode: {}'.format(mode))
|
||||
|
||||
rec_factory = variable_recurrent_factory if variable_length else Recurrent
|
||||
|
||||
if bidirectional:
|
||||
layer = (rec_factory(cell), rec_factory(cell, reverse=True))
|
||||
else:
|
||||
layer = (rec_factory(cell),)
|
||||
|
||||
func = StackedRNN(layer,
|
||||
num_layers,
|
||||
(mode == 'LSTM'),
|
||||
dropout=dropout,
|
||||
train=train)
|
||||
|
||||
def forward(input, weight, hidden, batch_sizes, input_quantizers, weight_quantizers):
|
||||
if batch_first and not variable_length:
|
||||
input = input.transpose(0, 1)
|
||||
|
||||
nexth, output = func(input, hidden, weight, batch_sizes, input_quantizers, weight_quantizers)
|
||||
|
||||
if batch_first and not variable_length:
|
||||
output = output.transpose(0, 1)
|
||||
|
||||
return output, nexth
|
||||
|
||||
return forward
|
||||
|
||||
|
||||
def RNN(*args, **kwargs):
|
||||
|
||||
def forward(input, *fargs, **fkwargs):
|
||||
func = AutogradRNN(*args, **kwargs)
|
||||
return func(input, *fargs, **fkwargs)
|
||||
|
||||
return forward
|
||||
@@ -0,0 +1,61 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Some supportive functions"""
|
||||
from absl import logging
|
||||
|
||||
import torch
|
||||
from torch.autograd import Function
|
||||
|
||||
|
||||
class ClipFunction(Function):
|
||||
"""An universal tensor clip function
|
||||
|
||||
Pytorch's clamp() only supports scalar range and doesn't support broadcast. This implementation uses min/max which
|
||||
is more genaral. The gradient is defined according to IBM's PACT paper https://arxiv.org/abs/1805.06085, which is
|
||||
also the behavior of Tensorflow's clip_by_value()
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, input, clip_value_min, clip_value_max):
|
||||
output = torch.min(input, clip_value_max)
|
||||
output = torch.max(output, clip_value_min)
|
||||
ctx.save_for_backward(input, clip_value_min, clip_value_max)
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
input, clip_value_min, clip_value_max = ctx.saved_tensors
|
||||
min_mask = (input > clip_value_min).to(grad_output.dtype)
|
||||
max_mask = (input < clip_value_max).to(grad_output.dtype)
|
||||
grad_input = grad_output * min_mask * max_mask
|
||||
|
||||
if clip_value_min.requires_grad or clip_value_max.requires_grad:
|
||||
logging.log_first_n(logging.WARNING, "Learning clip min/max is experimental, use at your own risk :).", 1)
|
||||
if clip_value_min.numel() != 1 or clip_value_max.numel() != 1:
|
||||
raise ValueError("Learnable min/max can only be scalar, got size %s and %s." % (clip_value_min.size(),
|
||||
clip_value_max.size()))
|
||||
|
||||
# Ensure the dtypes of min/max grads matches the input dtype
|
||||
# This might be necessary if running w/ AMP which will cast to fp32 before `sum()`
|
||||
grad_clip_value_min = (grad_output * (1. - min_mask)).sum().to(clip_value_min.dtype) if clip_value_min.requires_grad else None
|
||||
grad_clip_value_max = (grad_output * (1. - max_mask)).sum().to(clip_value_min.dtype) if clip_value_max.requires_grad else None
|
||||
|
||||
return grad_input, grad_clip_value_min, grad_clip_value_max
|
||||
|
||||
|
||||
clip = ClipFunction.apply
|
||||
@@ -0,0 +1,165 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Some helper functions for implementing quantized modules"""
|
||||
import copy
|
||||
import inspect
|
||||
|
||||
from absl import logging
|
||||
|
||||
from torch import nn
|
||||
|
||||
from pytorch_quantization.nn import TensorQuantizer
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor, QUANT_DESC_8BIT_PER_TENSOR
|
||||
|
||||
|
||||
class QuantMixin():
|
||||
"""Mixin class for adding basic quantization logic to quantized modules"""
|
||||
|
||||
default_quant_desc_input = QUANT_DESC_8BIT_PER_TENSOR
|
||||
default_quant_desc_weight = QUANT_DESC_8BIT_PER_TENSOR
|
||||
|
||||
@classmethod
|
||||
def set_default_quant_desc_input(cls, value):
|
||||
"""
|
||||
Args:
|
||||
value: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
|
||||
"""
|
||||
if not isinstance(value, QuantDescriptor):
|
||||
raise ValueError("{} is not an instance of QuantDescriptor!")
|
||||
cls.default_quant_desc_input = copy.deepcopy(value)
|
||||
|
||||
@classmethod
|
||||
def set_default_quant_desc_weight(cls, value):
|
||||
"""
|
||||
Args:
|
||||
value: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
|
||||
"""
|
||||
if not isinstance(value, QuantDescriptor):
|
||||
raise ValueError("{} is not an instance of QuantDescriptor!")
|
||||
cls.default_quant_desc_weight = copy.deepcopy(value)
|
||||
|
||||
def init_quantizer(self, quant_desc_input, quant_desc_weight, num_layers=None):
|
||||
"""Helper function for __init__ of quantized module
|
||||
|
||||
Create input and weight quantizer based on quant_desc passed by kwargs, or default of the class.
|
||||
|
||||
Args:
|
||||
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
|
||||
quant_desc_weight: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
|
||||
num_layers: An integer. Default None. If not None, create a list of quantizers.
|
||||
"""
|
||||
if not inspect.stack()[1].function == "__init__":
|
||||
raise TypeError("{} should be only called by __init__ of quantized module.".format(__name__))
|
||||
self._fake_quant = True
|
||||
if (not quant_desc_input.fake_quant) or (not quant_desc_weight.fake_quant):
|
||||
raise ValueError("Only fake quantization is supported!")
|
||||
|
||||
logging.info("Input is %squantized to %d bits in %s with axis %s!", ""
|
||||
if not quant_desc_input.fake_quant else "fake ",
|
||||
quant_desc_input.num_bits, self.__class__.__name__, quant_desc_input.axis)
|
||||
logging.info("Weight is %squantized to %d bits in %s with axis %s!", ""
|
||||
if not quant_desc_weight.fake_quant else "fake ",
|
||||
quant_desc_weight.num_bits, self.__class__.__name__, quant_desc_weight.axis)
|
||||
|
||||
if num_layers is None:
|
||||
self._input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
self._weight_quantizer = TensorQuantizer(quant_desc_weight)
|
||||
else:
|
||||
self._input_quantizers = nn.ModuleList([TensorQuantizer(quant_desc_input) for _ in range(num_layers)])
|
||||
self._weight_quantizers = nn.ModuleList([TensorQuantizer(quant_desc_weight) for _ in range(num_layers)])
|
||||
|
||||
# pylint:disable=missing-docstring
|
||||
@property
|
||||
def input_quantizer(self):
|
||||
return self._input_quantizer
|
||||
|
||||
@property
|
||||
def weight_quantizer(self):
|
||||
return self._weight_quantizer
|
||||
# pylint:enable=missing-docstring
|
||||
|
||||
|
||||
class QuantInputMixin():
|
||||
"""Mixin class for adding basic quantization logic to quantized modules"""
|
||||
|
||||
default_quant_desc_input = QUANT_DESC_8BIT_PER_TENSOR
|
||||
|
||||
@classmethod
|
||||
def set_default_quant_desc_input(cls, value):
|
||||
"""
|
||||
Args:
|
||||
value: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
|
||||
"""
|
||||
if not isinstance(value, QuantDescriptor):
|
||||
raise ValueError("{} is not an instance of QuantDescriptor!")
|
||||
cls.default_quant_desc_input = copy.deepcopy(value)
|
||||
|
||||
def init_quantizer(self, quant_desc_input):
|
||||
"""Helper function for __init__ of simple quantized module
|
||||
|
||||
Create input quantizer based on quant_desc passed by kwargs, or default of the class.
|
||||
|
||||
Args:
|
||||
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
|
||||
"""
|
||||
if not inspect.stack()[1].function == "__init__":
|
||||
raise TypeError("{} should be only called by __init__ of quantized module.".format(__name__))
|
||||
self._fake_quant = True
|
||||
if not quant_desc_input.fake_quant:
|
||||
raise ValueError("Only fake quantization is supported!")
|
||||
|
||||
logging.info("Input is %squantized to %d bits in %s with axis %s!", ""
|
||||
if not quant_desc_input.fake_quant else "fake ",
|
||||
quant_desc_input.num_bits, self.__class__.__name__, quant_desc_input.axis)
|
||||
|
||||
self._input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
|
||||
# pylint:disable=missing-docstring
|
||||
@property
|
||||
def input_quantizer(self):
|
||||
return self._input_quantizer
|
||||
# pylint:enable=missing-docstring
|
||||
|
||||
|
||||
def pop_quant_desc_in_kwargs(quant_cls, input_only=False, **kwargs):
|
||||
"""Pop quant descriptors in kwargs
|
||||
|
||||
If there is no descriptor in kwargs, the default one in quant_cls will be used
|
||||
|
||||
Arguments:
|
||||
quant_cls: A class that has default quantization descriptors
|
||||
input_only: A boolean. If True, pop quant_desc_input only, not quant_desc_weight. Default false.
|
||||
|
||||
Keyword Arguments:
|
||||
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
Quantization descriptor of input.
|
||||
quant_desc_weight: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
Quantization descriptor of weight.
|
||||
"""
|
||||
quant_desc_input = kwargs.pop('quant_desc_input', quant_cls.default_quant_desc_input)
|
||||
if not input_only:
|
||||
quant_desc_weight = kwargs.pop('quant_desc_weight', quant_cls.default_quant_desc_weight)
|
||||
|
||||
# Check if anything is left in **kwargs
|
||||
if kwargs:
|
||||
raise TypeError("Unused keys: {}".format(kwargs.keys()))
|
||||
|
||||
if input_only:
|
||||
return quant_desc_input
|
||||
return quant_desc_input, quant_desc_weight
|
||||
@@ -0,0 +1,59 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Implement a clip module as pytorch only has a simple clamp function """
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn.parameter import Parameter
|
||||
|
||||
from pytorch_quantization.nn import functional as QF
|
||||
|
||||
__all__ = ['Clip']
|
||||
|
||||
class Clip(nn.Module):
|
||||
"""Clip tensor
|
||||
|
||||
Args:
|
||||
clip_value_min: A number or tensor of lower bound to clip
|
||||
clip_value_max: A number of tensor of upper bound to clip
|
||||
learn_min: A boolean. If True, learn min. clip_value_min will be used to initialize. Default False
|
||||
learn_max: A boolean. Similar as learn_min but for max.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
"""
|
||||
|
||||
def __init__(self, clip_value_min, clip_value_max, learn_min=False, learn_max=False):
|
||||
super(Clip, self).__init__()
|
||||
if learn_min:
|
||||
if not isinstance(clip_value_min, float) and clip_value_min.size != 1:
|
||||
raise ValueError("clip_value_min/clip_value_max must be scalar for initilizing learnable range.")
|
||||
self.clip_value_min = Parameter(torch.tensor(clip_value_min)) # pylint: disable=not-callable
|
||||
else:
|
||||
self.clip_value_min = clip_value_min
|
||||
|
||||
if learn_max:
|
||||
if not isinstance(clip_value_max, float) and clip_value_max.size != 1:
|
||||
raise ValueError("clip_value_min/clip_value_max must be scalar for initilizing learnable range.")
|
||||
self.clip_value_max = Parameter(torch.tensor(clip_value_max)) # pylint: disable=not-callable
|
||||
else:
|
||||
self.clip_value_max = clip_value_max
|
||||
|
||||
def forward(self, inputs):
|
||||
outputs = QF.clip(inputs, self.clip_value_min, self.clip_value_max)
|
||||
return outputs
|
||||
@@ -0,0 +1,419 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Quantized convolution
|
||||
Base code is from nn.Conv, details of Module and original argument can be found there.
|
||||
Module names are intentionally kept same as unquantized version so that they can be dropped into preexisting model
|
||||
easily, and load pretrained weight. Aliases with Quant prefix are defined and are encouraged to be used explicitly
|
||||
when start scratch.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import torch
|
||||
import torch.nn
|
||||
import torch.nn.functional as F
|
||||
from torch.nn.modules.utils import _single, _pair, _triple
|
||||
from torch.nn.modules.conv import _ConvTransposeNd
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
|
||||
from . import _utils
|
||||
|
||||
__all__ = [
|
||||
"Conv2d", "QuantConv2d", "Conv3d", "QuantConv3d", "Conv1d", "QuantConv1d", "ConvTranspose1d", "ConvTranspose2d",
|
||||
"ConvTranspose3d", "QuantConvTranspose1d", "QuantConvTranspose2d", "QuantConvTranspose3d"
|
||||
]
|
||||
|
||||
|
||||
class _QuantConvNd(torch.nn.modules.conv._ConvNd, _utils.QuantMixin):
|
||||
"""base class of quantized Conv inherited from _ConvNd
|
||||
|
||||
Comments of original arguments can be found in torch.nn.modules.conv
|
||||
|
||||
Arguments:
|
||||
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
Quantization descriptor of input.
|
||||
quant_desc_weight: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
Quantization descriptor of weight.
|
||||
|
||||
Raises:
|
||||
ValueError: If unsupported arguments are passed in.
|
||||
|
||||
Readonly properties:
|
||||
- input_quantizer:
|
||||
- weight_quantizer:
|
||||
|
||||
Static methods:
|
||||
- set_default_quant_desc_input: Set default_quant_desc_input
|
||||
- set_default_quant_desc_weight: Set default_quant_desc_weight
|
||||
"""
|
||||
|
||||
default_quant_desc_input = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
|
||||
def __init__(self, in_channels, out_channels, kernel_size, stride, padding, dilation, transposed, output_padding,
|
||||
groups, bias, padding_mode, quant_desc_input, quant_desc_weight):
|
||||
super(_QuantConvNd, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation,
|
||||
transposed, output_padding, groups, bias, padding_mode)
|
||||
self.init_quantizer(quant_desc_input, quant_desc_weight)
|
||||
|
||||
def _quant(self, input):
|
||||
"""Apply quantization on input and weight
|
||||
|
||||
Function called by the classes lower in the hierarchy, which actually performs the quantization before forward
|
||||
in the derivate class the particular Function.
|
||||
|
||||
Arguments:
|
||||
input: in_features to quantize
|
||||
Returns:
|
||||
A tuple: (quant_in_feature, quant_weight)
|
||||
"""
|
||||
quant_input = self._input_quantizer(input)
|
||||
quant_weight = self._weight_quantizer(self.weight)
|
||||
|
||||
return (quant_input, quant_weight)
|
||||
|
||||
|
||||
class QuantConv2d(_QuantConvNd):
|
||||
"""Quantized 2D conv"""
|
||||
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL
|
||||
|
||||
def __init__(self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=0,
|
||||
dilation=1,
|
||||
groups=1,
|
||||
bias=True,
|
||||
padding_mode='zeros',
|
||||
**kwargs):
|
||||
|
||||
kernel_size = _pair(kernel_size)
|
||||
stride = _pair(stride)
|
||||
padding = _pair(padding)
|
||||
dilation = _pair(dilation)
|
||||
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
super(QuantConv2d, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation, False,
|
||||
_pair(0), groups, bias, padding_mode,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
|
||||
def forward(self, input):
|
||||
# the actual quantization happens in the next level of the class hierarchy
|
||||
quant_input, quant_weight = self._quant(input)
|
||||
|
||||
if self.padding_mode == 'circular':
|
||||
expanded_padding = ((self.padding[1] + 1) // 2, self.padding[1] // 2,
|
||||
(self.padding[0] + 1) // 2, self.padding[0] // 2)
|
||||
output = F.conv2d(F.pad(quant_input, expanded_padding, mode='circular'),
|
||||
quant_weight, self.bias, self.stride,
|
||||
_pair(0), self.dilation, self.groups)
|
||||
else:
|
||||
output = F.conv2d(quant_input, quant_weight, self.bias, self.stride, self.padding, self.dilation,
|
||||
self.groups)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class QuantConv3d(_QuantConvNd):
|
||||
"""Quantized 3D Conv"""
|
||||
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONV3D_WEIGHT_PER_CHANNEL
|
||||
|
||||
def __init__(self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=0,
|
||||
dilation=1,
|
||||
groups=1,
|
||||
bias=True,
|
||||
padding_mode='zeros',
|
||||
**kwargs):
|
||||
|
||||
kernel_size = _triple(kernel_size)
|
||||
stride = _triple(stride)
|
||||
padding = _triple(padding)
|
||||
dilation = _triple(dilation)
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
super(QuantConv3d, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation, False,
|
||||
_triple(0), groups, bias, padding_mode,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
|
||||
def forward(self, input):
|
||||
# the actual quantization happens in the next level of the class hierarchy
|
||||
quant_input, quant_weight = self._quant(input)
|
||||
|
||||
if self.padding_mode == 'circular':
|
||||
expanded_padding = ((self.padding[2] + 1) // 2, self.padding[2] // 2,
|
||||
(self.padding[1] + 1) // 2, self.padding[1] // 2,
|
||||
(self.padding[0] + 1) // 2, self.padding[0] // 2)
|
||||
output = F.conv3d(F.pad(quant_input, expanded_padding, mode='circular'),
|
||||
quant_weight, self.bias, self.stride, _triple(0),
|
||||
self.dilation, self.groups)
|
||||
else:
|
||||
output = F.conv3d(quant_input, quant_weight, self.bias, self.stride, self.padding, self.dilation,
|
||||
self.groups)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class QuantConv1d(_QuantConvNd):
|
||||
"""Quantized 1D Conv"""
|
||||
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONV1D_WEIGHT_PER_CHANNEL
|
||||
|
||||
def __init__(self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=0,
|
||||
dilation=1,
|
||||
groups=1,
|
||||
bias=True,
|
||||
padding_mode='zeros',
|
||||
**kwargs):
|
||||
|
||||
kernel_size = _single(kernel_size)
|
||||
stride = _single(stride)
|
||||
padding = _single(padding)
|
||||
dilation = _single(dilation)
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
super(QuantConv1d, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation, False,
|
||||
_single(0), groups, bias, padding_mode,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
|
||||
def forward(self, input):
|
||||
# the actual quantization happens in the next level of the class hierarchy
|
||||
quant_input, quant_weight = self._quant(input)
|
||||
|
||||
if self.padding_mode == 'circular':
|
||||
expanded_padding = ((self.padding[0] + 1) // 2, self.padding[0] // 2)
|
||||
output = F.conv1d(F.pad(quant_input, expanded_padding, mode='circular'),
|
||||
quant_weight, self.bias, self.stride,
|
||||
_single(0), self.dilation, self.groups)
|
||||
else:
|
||||
output = F.conv1d(quant_input, quant_weight, self.bias, self.stride,
|
||||
self.padding, self.dilation, self.groups)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class _QuantConvTransposeNd(torch.nn.modules.conv._ConvTransposeNd, _utils.QuantMixin):
|
||||
"""base class of quantized Transposed Conv inherited from _ConvTransposeNd
|
||||
|
||||
Comments of original arguments can be found in torch.nn.modules.conv
|
||||
|
||||
Arguments:
|
||||
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
Quantization descriptor of input.
|
||||
quant_desc_weight: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
Quantization descriptor of weight.
|
||||
|
||||
Raises:
|
||||
ValueError: If unsupported arguments are passed in.
|
||||
|
||||
Readonly properties:
|
||||
- input_quantizer:
|
||||
- weight_quantizer:
|
||||
|
||||
Static methods:
|
||||
- set_default_quant_desc_input: Set default_quant_desc_input
|
||||
- set_default_quant_desc_weight: Set default_quant_desc_weight
|
||||
"""
|
||||
|
||||
default_quant_desc_input = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
def __init__(self, in_channels, out_channels, kernel_size, stride,
|
||||
padding, dilation, transposed, output_padding,
|
||||
groups, bias, padding_mode, quant_desc_input, quant_desc_weight):
|
||||
super(_QuantConvTransposeNd, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation,
|
||||
transposed, output_padding, groups, bias, padding_mode)
|
||||
self.init_quantizer(quant_desc_input, quant_desc_weight)
|
||||
|
||||
def _quant(self, input):
|
||||
"""Apply quantization on input and weight
|
||||
|
||||
Function called by the classes lower in the hierarchy, which actually performs the quantization before forward
|
||||
in the derivate class the particular Function.
|
||||
|
||||
Arguments:
|
||||
input: in_features to quantize
|
||||
Returns:
|
||||
A tuple: (quant_in_feature, quant_weight)
|
||||
"""
|
||||
quant_input = self._input_quantizer(input)
|
||||
quant_weight = self._weight_quantizer(self.weight)
|
||||
|
||||
return (quant_input, quant_weight)
|
||||
|
||||
def _output_padding_nd(self,
|
||||
input,
|
||||
output_size,
|
||||
stride,
|
||||
padding,
|
||||
kernel_size,
|
||||
num_spatial_dims,
|
||||
dilation=None):
|
||||
if "num_spatial_dims" in inspect.signature(self._output_padding).parameters:
|
||||
return self._output_padding(input, output_size, stride, padding, kernel_size, num_spatial_dims)
|
||||
else:
|
||||
return self._output_padding(input, output_size, stride, padding, kernel_size)
|
||||
|
||||
|
||||
class QuantConvTranspose1d(_QuantConvTransposeNd):
|
||||
"""Quantized ConvTranspose1d"""
|
||||
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE1D_WEIGHT_PER_CHANNEL
|
||||
|
||||
def __init__(self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=0,
|
||||
output_padding=0,
|
||||
groups=1,
|
||||
bias=True,
|
||||
dilation=1,
|
||||
padding_mode='zeros',
|
||||
**kwargs):
|
||||
kernel_size = _single(kernel_size)
|
||||
stride = _single(stride)
|
||||
padding = _single(padding)
|
||||
dilation = _single(dilation)
|
||||
output_padding = _single(output_padding)
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
super(QuantConvTranspose1d, self).__init__(
|
||||
in_channels, out_channels, kernel_size, stride, padding, dilation,
|
||||
True, output_padding, groups, bias, padding_mode,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
|
||||
def forward(self, input, output_size=None):
|
||||
if self.padding_mode != 'zeros':
|
||||
raise ValueError('Only `zeros` padding mode is supported for QuantConvTranspose1d')
|
||||
|
||||
num_spatial_dims = 1
|
||||
output_padding = self._output_padding_nd(input, output_size, self.stride, self.padding, self.kernel_size,
|
||||
num_spatial_dims)
|
||||
|
||||
quant_input, quant_weight = self._quant(input)
|
||||
output = F.conv_transpose1d(quant_input, quant_weight, self.bias, self.stride, self.padding, output_padding,
|
||||
self.groups, self.dilation)
|
||||
return output
|
||||
|
||||
|
||||
class QuantConvTranspose2d(_QuantConvTransposeNd):
|
||||
"""Quantized ConvTranspose2d"""
|
||||
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE2D_WEIGHT_PER_CHANNEL
|
||||
|
||||
def __init__(self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=0,
|
||||
output_padding=0,
|
||||
groups=1,
|
||||
bias=True,
|
||||
dilation=1,
|
||||
padding_mode='zeros',
|
||||
**kwargs):
|
||||
kernel_size = _pair(kernel_size)
|
||||
stride = _pair(stride)
|
||||
padding = _pair(padding)
|
||||
dilation = _pair(dilation)
|
||||
output_padding = _pair(output_padding)
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
super(QuantConvTranspose2d, self).__init__(
|
||||
in_channels, out_channels, kernel_size, stride, padding, dilation,
|
||||
True, output_padding, groups, bias, padding_mode,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
|
||||
def forward(self, input, output_size=None):
|
||||
if self.padding_mode != 'zeros':
|
||||
raise ValueError('Only `zeros` padding mode is supported for QuantConvTranspose2d')
|
||||
|
||||
num_spatial_dims = 2
|
||||
output_padding = self._output_padding_nd(input, output_size, self.stride, self.padding, self.kernel_size,
|
||||
num_spatial_dims)
|
||||
|
||||
quant_input, quant_weight = self._quant(input)
|
||||
output = F.conv_transpose2d(quant_input, quant_weight, self.bias, self.stride, self.padding, output_padding,
|
||||
self.groups, self.dilation)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class QuantConvTranspose3d(_QuantConvTransposeNd):
|
||||
"""Quantized ConvTranspose3d"""
|
||||
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE3D_WEIGHT_PER_CHANNEL
|
||||
|
||||
def __init__(self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=0,
|
||||
output_padding=0,
|
||||
groups=1,
|
||||
bias=True,
|
||||
dilation=1,
|
||||
padding_mode='zeros',
|
||||
**kwargs):
|
||||
kernel_size = _triple(kernel_size)
|
||||
stride = _triple(stride)
|
||||
padding = _triple(padding)
|
||||
dilation = _triple(dilation)
|
||||
output_padding = _triple(output_padding)
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
super(QuantConvTranspose3d, self).__init__(
|
||||
in_channels, out_channels, kernel_size, stride, padding, dilation,
|
||||
True, output_padding, groups, bias, padding_mode,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
|
||||
def forward(self, input, output_size=None):
|
||||
if self.padding_mode != 'zeros':
|
||||
raise ValueError('Only `zeros` padding mode is supported for QuantConvTranspose3d')
|
||||
|
||||
num_spatial_dims = 3
|
||||
output_padding = self._output_padding_nd(input, output_size, self.stride, self.padding, self.kernel_size,
|
||||
num_spatial_dims)
|
||||
|
||||
quant_input, quant_weight = self._quant(input)
|
||||
output = F.conv_transpose3d(quant_input, quant_weight, self.bias, self.stride, self.padding, output_padding,
|
||||
self.groups, self.dilation)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
# Define alias with Quant prefix
|
||||
_ConvNd = _QuantConvNd
|
||||
Conv1d = QuantConv1d
|
||||
Conv2d = QuantConv2d
|
||||
Conv3d = QuantConv3d
|
||||
ConvTranspose1d = QuantConvTranspose1d
|
||||
ConvTranspose2d = QuantConvTranspose2d
|
||||
ConvTranspose3d = QuantConvTranspose3d
|
||||
@@ -0,0 +1,79 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Quantized instance normalization module
|
||||
Base code is from nn.InstanceNorm, details of the module can be found from the offical repo.
|
||||
"""
|
||||
|
||||
from torch.nn.modules.batchnorm import _NormBase
|
||||
import torch.nn.functional as F
|
||||
from torch.nn.modules import instancenorm
|
||||
|
||||
from pytorch_quantization.nn import TensorQuantizer
|
||||
from pytorch_quantization import tensor_quant
|
||||
from . import _utils
|
||||
|
||||
__all__ = [
|
||||
"QuantInstanceNorm1d", "QuantInstanceNorm2d", "QuantInstanceNorm3d"
|
||||
]
|
||||
|
||||
class QuantInstanceNorm1d(instancenorm.InstanceNorm1d, _utils.QuantInputMixin):
|
||||
r"""Applies Quantized Instance Normalization over a 3D input
|
||||
"""
|
||||
def __init__(
|
||||
self, num_features: int, eps: float = 1e-5, momentum: float = 0.1, affine: bool = False,
|
||||
track_running_stats: bool = False, **kwargs):
|
||||
super(QuantInstanceNorm1d, self).__init__(
|
||||
num_features, eps, momentum, affine, track_running_stats)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantInstanceNorm1d, self).forward(quant_input)
|
||||
|
||||
|
||||
class QuantInstanceNorm2d(instancenorm.InstanceNorm2d, _utils.QuantInputMixin):
|
||||
r"""Applies Quantized Instance Normalization over a 4D input
|
||||
"""
|
||||
def __init__(
|
||||
self, num_features: int, eps: float = 1e-5, momentum: float = 0.1, affine: bool = False,
|
||||
track_running_stats: bool = False, **kwargs):
|
||||
super(QuantInstanceNorm2d, self).__init__(
|
||||
num_features, eps, momentum, affine, track_running_stats)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantInstanceNorm2d, self).forward(quant_input)
|
||||
|
||||
|
||||
class QuantInstanceNorm3d(instancenorm.InstanceNorm3d, _utils.QuantInputMixin):
|
||||
r"""Applies Quantized Instance Normalization over a 5D input
|
||||
"""
|
||||
def __init__(
|
||||
self, num_features: int, eps: float = 1e-5, momentum: float = 0.1, affine: bool = False,
|
||||
track_running_stats: bool = False, **kwargs):
|
||||
super(QuantInstanceNorm3d, self).__init__(
|
||||
num_features, eps, momentum, affine, track_running_stats)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantInstanceNorm3d, self).forward(quant_input)
|
||||
@@ -0,0 +1,78 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Quantized Linear"""
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
|
||||
from . import _utils
|
||||
|
||||
__all__ = ["Linear", "QuantLinear"]
|
||||
|
||||
class QuantLinear(nn.Linear, _utils.QuantMixin):
|
||||
"""Quantized version of nn.Linear
|
||||
|
||||
Apply quantized linear to the incoming data, y = dequant(quant(x)quant(A)^T + b).
|
||||
|
||||
Keep Module name "Linear" instead of "QuantLinear" so that it can be easily dropped into preexisting model and load
|
||||
pretrained weights. An alias "QuantLinear" is defined below. The base code is a copy of nn.Linear, see detailed
|
||||
comment of original arguments there.
|
||||
|
||||
Quantization descriptors are passed in in kwargs. If not presents, default_quant_desc_input and
|
||||
default_quant_desc_weight are used.
|
||||
|
||||
Keyword Arguments:
|
||||
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
Quantization descriptor of input.
|
||||
quant_desc_wegiht: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
Quantization descriptor of weight.
|
||||
|
||||
Raises:
|
||||
ValueError: If unsupported arguments are passed in.
|
||||
KeyError: If unsupported kwargs are passed in.
|
||||
|
||||
Readonly properties:
|
||||
- input_quantizer:
|
||||
- weight_quantizer:
|
||||
|
||||
Static methods:
|
||||
- set_default_quant_desc_input: Set default_quant_desc_input
|
||||
- set_default_quant_desc_weight: Set default_quant_desc_weight
|
||||
"""
|
||||
|
||||
default_quant_desc_input = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW
|
||||
|
||||
def __init__(self, in_features, out_features, bias=True, **kwargs):
|
||||
super(QuantLinear, self).__init__(in_features, out_features, bias)
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
|
||||
self.init_quantizer(quant_desc_input, quant_desc_weight)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
quant_weight = self._weight_quantizer(self.weight)
|
||||
|
||||
output = F.linear(quant_input, quant_weight, bias=self.bias)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
Linear = QuantLinear
|
||||
@@ -0,0 +1,163 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Quantized Pooling
|
||||
Base code is from nn.pooling, details of Module and original argument can be found there.
|
||||
Module names are intentionally kept same as unquantized version so that they can be dropped into preexisting model
|
||||
easily, and load pretrained weight. Aliases with Quant prefix are defined and are encouraged to be used explicitly
|
||||
when start scratch.
|
||||
"""
|
||||
|
||||
from torch.nn.modules import pooling
|
||||
|
||||
from . import _utils
|
||||
|
||||
__all__ = [
|
||||
"MaxPool1d", "QuantMaxPool1d", "MaxPool2d", "QuantMaxPool2d", "MaxPool3d", "QuantMaxPool3d",
|
||||
"AvgPool1d", "QuantAvgPool1d", "AvgPool2d", "QuantAvgPool2d", "AvgPool3d", "QuantAvgPool3d",
|
||||
"AdaptiveAvgPool1d", "QuantAdaptiveAvgPool1d", "AdaptiveAvgPool2d", "QuantAdaptiveAvgPool2d",
|
||||
"AdaptiveAvgPool3d", "QuantAdaptiveAvgPool3d"
|
||||
]
|
||||
|
||||
class QuantMaxPool1d(pooling.MaxPool1d, _utils.QuantInputMixin):
|
||||
"""Quantized 1D maxpool"""
|
||||
def __init__(self, kernel_size, stride=None, padding=0, dilation=1,
|
||||
return_indices=False, ceil_mode=False, **kwargs):
|
||||
super(QuantMaxPool1d, self).__init__(kernel_size, stride, padding, dilation,
|
||||
return_indices, ceil_mode)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantMaxPool1d, self).forward(quant_input)
|
||||
|
||||
class QuantMaxPool2d(pooling.MaxPool2d, _utils.QuantInputMixin):
|
||||
"""Quantized 2D maxpool"""
|
||||
def __init__(self, kernel_size, stride=None, padding=0, dilation=1,
|
||||
return_indices=False, ceil_mode=False, **kwargs):
|
||||
super(QuantMaxPool2d, self).__init__(kernel_size, stride, padding, dilation,
|
||||
return_indices, ceil_mode)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantMaxPool2d, self).forward(quant_input)
|
||||
|
||||
class QuantMaxPool3d(pooling.MaxPool3d, _utils.QuantInputMixin):
|
||||
"""Quantized 3D maxpool"""
|
||||
def __init__(self, kernel_size, stride=None, padding=0, dilation=1,
|
||||
return_indices=False, ceil_mode=False, **kwargs):
|
||||
super(QuantMaxPool3d, self).__init__(kernel_size, stride, padding, dilation,
|
||||
return_indices, ceil_mode)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantMaxPool3d, self).forward(quant_input)
|
||||
|
||||
|
||||
class QuantAvgPool1d(pooling.AvgPool1d, _utils.QuantInputMixin):
|
||||
"""Quantized 1D average pool"""
|
||||
def __init__(self, kernel_size, stride=None, padding=0, ceil_mode=False,
|
||||
count_include_pad=True, **kwargs):
|
||||
super(QuantAvgPool1d, self).__init__(kernel_size, stride, padding, ceil_mode,
|
||||
count_include_pad)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantAvgPool1d, self).forward(quant_input)
|
||||
|
||||
class QuantAvgPool2d(pooling.AvgPool2d, _utils.QuantInputMixin):
|
||||
"""Quantized 2D average pool"""
|
||||
def __init__(self, kernel_size, stride=None, padding=0, ceil_mode=False,
|
||||
count_include_pad=True, divisor_override=None, **kwargs):
|
||||
super(QuantAvgPool2d, self).__init__(kernel_size, stride, padding, ceil_mode,
|
||||
count_include_pad, divisor_override)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantAvgPool2d, self).forward(quant_input)
|
||||
|
||||
|
||||
class QuantAvgPool3d(pooling.AvgPool3d, _utils.QuantInputMixin):
|
||||
"""Quantized 3D average pool"""
|
||||
def __init__(self, kernel_size, stride=None, padding=0, ceil_mode=False,
|
||||
count_include_pad=True, divisor_override=None, **kwargs):
|
||||
super(QuantAvgPool3d, self).__init__(kernel_size, stride, padding, ceil_mode,
|
||||
count_include_pad, divisor_override)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantAvgPool3d, self).forward(quant_input)
|
||||
|
||||
|
||||
class QuantAdaptiveAvgPool1d(pooling.AdaptiveAvgPool1d, _utils.QuantInputMixin):
|
||||
"""Quantized 1D adaptive average pool"""
|
||||
def __init__(self, output_size, **kwargs):
|
||||
super(QuantAdaptiveAvgPool1d, self).__init__(output_size)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantAdaptiveAvgPool1d, self).forward(quant_input)
|
||||
|
||||
|
||||
class QuantAdaptiveAvgPool2d(pooling.AdaptiveAvgPool2d, _utils.QuantInputMixin):
|
||||
"""Quantized 2D adaptive average pool"""
|
||||
def __init__(self, output_size, **kwargs):
|
||||
super(QuantAdaptiveAvgPool2d, self).__init__(output_size)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantAdaptiveAvgPool2d, self).forward(quant_input)
|
||||
|
||||
|
||||
class QuantAdaptiveAvgPool3d(pooling.AdaptiveAvgPool3d, _utils.QuantInputMixin):
|
||||
"""Quantized 3D adaptive average pool"""
|
||||
def __init__(self, output_size, **kwargs):
|
||||
super(QuantAdaptiveAvgPool3d, self).__init__(output_size)
|
||||
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
|
||||
self.init_quantizer(quant_desc_input)
|
||||
|
||||
def forward(self, input):
|
||||
quant_input = self._input_quantizer(input)
|
||||
return super(QuantAdaptiveAvgPool3d, self).forward(quant_input)
|
||||
|
||||
|
||||
# Define alias with Quant prefix
|
||||
MaxPool1d = QuantMaxPool1d
|
||||
MaxPool2d = QuantMaxPool2d
|
||||
MaxPool3d = QuantMaxPool3d
|
||||
AvgPool1d = QuantAvgPool1d
|
||||
AvgPool2d = QuantAvgPool2d
|
||||
AvgPool3d = QuantAvgPool3d
|
||||
AdaptiveAvgPool1d = QuantAdaptiveAvgPool1d
|
||||
AdaptiveAvgPool2d = QuantAdaptiveAvgPool2d
|
||||
AdaptiveAvgPool3d = QuantAdaptiveAvgPool3d
|
||||
@@ -0,0 +1,467 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""RNN implementation in python
|
||||
originally copied from https://github.com/pytorch/pytorch/blob/v0.4.1/torch/nn/modules/rnn.py
|
||||
backend is changed to _functions/rnn.py
|
||||
"""
|
||||
import math
|
||||
import torch
|
||||
import warnings
|
||||
import itertools
|
||||
import numbers
|
||||
|
||||
from torch import nn
|
||||
from torch.nn import Parameter
|
||||
from torch.nn.utils.rnn import PackedSequence
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization.nn._functions import quant_rnn
|
||||
|
||||
from . import _utils
|
||||
|
||||
__all__ = ["QuantLSTM", "QuantLSTMCell", "LSTM", "LSTMCell"]
|
||||
|
||||
class QuantRNNBase(nn.Module, _utils.QuantMixin):
|
||||
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW
|
||||
|
||||
def __init__(self, mode, input_size, hidden_size,
|
||||
num_layers=1, bias=True, batch_first=False,
|
||||
dropout=0, bidirectional=False, proj_size=0, **kwargs):
|
||||
super(QuantRNNBase, self).__init__()
|
||||
self.mode = mode
|
||||
self.input_size = input_size
|
||||
self.hidden_size = hidden_size
|
||||
self.num_layers = num_layers
|
||||
self.bias = bias
|
||||
self.batch_first = batch_first
|
||||
self.dropout = dropout
|
||||
self.dropout_state = {}
|
||||
self.bidirectional = bidirectional
|
||||
self.proj_size = proj_size
|
||||
num_directions = 2 if bidirectional else 1
|
||||
|
||||
if not isinstance(dropout, numbers.Number) or not 0 <= dropout <= 1 or \
|
||||
isinstance(dropout, bool):
|
||||
raise ValueError("dropout should be a number in range [0, 1] "
|
||||
"representing the probability of an element being "
|
||||
"zeroed")
|
||||
if dropout > 0 and num_layers == 1:
|
||||
warnings.warn("dropout option adds dropout after all but last "
|
||||
"recurrent layer, so non-zero dropout expects "
|
||||
"num_layers greater than 1, but got dropout={} and "
|
||||
"num_layers={}".format(dropout, num_layers))
|
||||
|
||||
if proj_size < 0:
|
||||
raise ValueError("proj_size should be a positive integer or zero to disable projections")
|
||||
if proj_size > 0:
|
||||
raise ValueError("proj_size is not supported in pytorch-quantization yet")
|
||||
|
||||
if mode == 'LSTM':
|
||||
gate_size = 4 * hidden_size
|
||||
elif mode == 'GRU':
|
||||
gate_size = 3 * hidden_size
|
||||
else:
|
||||
gate_size = hidden_size
|
||||
|
||||
self._all_weights = []
|
||||
for layer in range(num_layers):
|
||||
for direction in range(num_directions):
|
||||
layer_input_size = input_size if layer == 0 else hidden_size * num_directions
|
||||
|
||||
w_ih = Parameter(torch.Tensor(gate_size, layer_input_size))
|
||||
w_hh = Parameter(torch.Tensor(gate_size, hidden_size))
|
||||
b_ih = Parameter(torch.Tensor(gate_size))
|
||||
b_hh = Parameter(torch.Tensor(gate_size))
|
||||
layer_params = (w_ih, w_hh, b_ih, b_hh)
|
||||
|
||||
suffix = '_reverse' if direction == 1 else ''
|
||||
param_names = ['weight_ih_l{}{}', 'weight_hh_l{}{}']
|
||||
if bias:
|
||||
param_names += ['bias_ih_l{}{}', 'bias_hh_l{}{}']
|
||||
param_names = [x.format(layer, suffix) for x in param_names]
|
||||
|
||||
for name, param in zip(param_names, layer_params):
|
||||
setattr(self, name, param)
|
||||
self._all_weights.append(param_names)
|
||||
|
||||
self.flatten_parameters()
|
||||
self.reset_parameters()
|
||||
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
self.init_quantizer(quant_desc_input, quant_desc_weight, num_layers=num_layers * (1 + bidirectional))
|
||||
|
||||
def flatten_parameters(self):
|
||||
"""Resets parameter data pointer so that they can use faster code paths.
|
||||
|
||||
Right now, this works only if the module is on the GPU and cuDNN is enabled.
|
||||
Otherwise, it's a no-op.
|
||||
"""
|
||||
any_param = next(self.parameters()).data
|
||||
if not any_param.is_cuda or not torch.backends.cudnn.is_acceptable(any_param):
|
||||
self._data_ptrs = []
|
||||
return
|
||||
|
||||
# If any parameters alias, we fall back to the slower, copying code path. This is
|
||||
# a sufficient check, because overlapping parameter buffers that don't completely
|
||||
# alias would break the assumptions of the uniqueness check in
|
||||
# Module.named_parameters().
|
||||
unique_data_ptrs = set(p.data_ptr() for l in self.all_weights for p in l)
|
||||
if len(unique_data_ptrs) != sum(len(l) for l in self.all_weights):
|
||||
self._data_ptrs = []
|
||||
return
|
||||
|
||||
with torch.cuda.device_of(any_param):
|
||||
import torch.backends.cudnn.rnn as rnn
|
||||
|
||||
weight_arr = list(itertools.chain.from_iterable(self.all_weights))
|
||||
weight_stride0 = len(self.all_weights[0])
|
||||
|
||||
# NB: This is a temporary hack while we still don't have Tensor
|
||||
# bindings for ATen functions
|
||||
with torch.no_grad():
|
||||
# NB: this is an INPLACE function on weight_arr, that's why the
|
||||
# no_grad() is necessary.
|
||||
weight_buf = torch._cudnn_rnn_flatten_weight(weight_arr, weight_stride0, self.input_size,
|
||||
rnn.get_cudnn_mode(self.mode), self.hidden_size,
|
||||
self.proj_size, self.num_layers, self.batch_first,
|
||||
bool(self.bidirectional))
|
||||
|
||||
self._param_buf_size = weight_buf.size(0)
|
||||
self._data_ptrs = list(p.data.data_ptr() for p in self.parameters())
|
||||
|
||||
def _apply(self, fn):
|
||||
ret = super(QuantRNNBase, self)._apply(fn)
|
||||
self.flatten_parameters()
|
||||
return ret
|
||||
|
||||
def reset_parameters(self):
|
||||
stdv = 1.0 / math.sqrt(self.hidden_size)
|
||||
for weight in self.parameters():
|
||||
weight.data.uniform_(-stdv, stdv)
|
||||
|
||||
def check_forward_args(self, input, hidden, batch_sizes):
|
||||
is_input_packed = batch_sizes is not None
|
||||
expected_input_dim = 2 if is_input_packed else 3
|
||||
if input.dim() != expected_input_dim:
|
||||
raise RuntimeError(
|
||||
'input must have {} dimensions, got {}'.format(
|
||||
expected_input_dim, input.dim()))
|
||||
if self.input_size != input.size(-1):
|
||||
raise RuntimeError(
|
||||
'input.size(-1) must be equal to input_size. Expected {}, got {}'.format(
|
||||
self.input_size, input.size(-1)))
|
||||
|
||||
if is_input_packed:
|
||||
mini_batch = int(batch_sizes[0])
|
||||
else:
|
||||
mini_batch = input.size(0) if self.batch_first else input.size(1)
|
||||
|
||||
num_directions = 2 if self.bidirectional else 1
|
||||
expected_hidden_size = (self.num_layers * num_directions,
|
||||
mini_batch, self.hidden_size)
|
||||
|
||||
def check_hidden_size(hx, expected_hidden_size, msg='Expected hidden size {}, got {}'):
|
||||
if tuple(hx.size()) != expected_hidden_size:
|
||||
raise RuntimeError(msg.format(expected_hidden_size, tuple(hx.size())))
|
||||
|
||||
if self.mode == 'LSTM':
|
||||
check_hidden_size(hidden[0], expected_hidden_size,
|
||||
'Expected hidden[0] size {}, got {}')
|
||||
check_hidden_size(hidden[1], expected_hidden_size,
|
||||
'Expected hidden[1] size {}, got {}')
|
||||
else:
|
||||
check_hidden_size(hidden, expected_hidden_size)
|
||||
|
||||
def forward(self, input, hx=None):
|
||||
is_packed = isinstance(input, PackedSequence)
|
||||
if is_packed:
|
||||
input, batch_sizes, sorted_indices, unsorted_indices = input
|
||||
max_batch_size = batch_sizes[0]
|
||||
max_batch_size = int(max_batch_size)
|
||||
else:
|
||||
batch_sizes = None
|
||||
max_batch_size = input.size(0) if self.batch_first else input.size(1)
|
||||
|
||||
if hx is None:
|
||||
num_directions = 2 if self.bidirectional else 1
|
||||
hx = input.new_zeros(self.num_layers * num_directions,
|
||||
max_batch_size, self.hidden_size,
|
||||
requires_grad=False)
|
||||
if self.mode == 'LSTM':
|
||||
hx = (hx, hx)
|
||||
|
||||
has_flat_weights = list(p.data.data_ptr() for p in self.parameters()) == self._data_ptrs
|
||||
if has_flat_weights:
|
||||
first_data = next(self.parameters()).data
|
||||
assert first_data.storage().size() == self._param_buf_size
|
||||
flat_weight = first_data.new().set_(first_data.storage(), 0, torch.Size([self._param_buf_size]))
|
||||
else:
|
||||
flat_weight = None
|
||||
|
||||
self.check_forward_args(input, hx, batch_sizes)
|
||||
func = quant_rnn.RNN(
|
||||
self.mode,
|
||||
self.input_size,
|
||||
self.hidden_size,
|
||||
num_layers=self.num_layers,
|
||||
batch_first=self.batch_first,
|
||||
dropout=self.dropout,
|
||||
train=self.training,
|
||||
bidirectional=self.bidirectional,
|
||||
dropout_state=self.dropout_state,
|
||||
variable_length=is_packed,
|
||||
flat_weight=flat_weight
|
||||
)
|
||||
output, hidden = func(input, self.all_weights, hx, batch_sizes, self._input_quantizers, self._weight_quantizers)
|
||||
if is_packed:
|
||||
output = PackedSequence(output, batch_sizes)
|
||||
return output, hidden
|
||||
|
||||
def extra_repr(self):
|
||||
s = '{input_size}, {hidden_size}'
|
||||
if self.num_layers != 1:
|
||||
s += ', num_layers={num_layers}'
|
||||
if self.bias is not True:
|
||||
s += ', bias={bias}'
|
||||
if self.batch_first is not False:
|
||||
s += ', batch_first={batch_first}'
|
||||
if self.dropout != 0:
|
||||
s += ', dropout={dropout}'
|
||||
if self.bidirectional is not False:
|
||||
s += ', bidirectional={bidirectional}'
|
||||
return s.format(**self.__dict__)
|
||||
|
||||
def __setstate__(self, d):
|
||||
super(QuantRNNBase, self).__setstate__(d)
|
||||
self.__dict__.setdefault('_data_ptrs', [])
|
||||
if 'all_weights' in d:
|
||||
self._all_weights = d['all_weights']
|
||||
if isinstance(self._all_weights[0][0], str):
|
||||
return
|
||||
num_layers = self.num_layers
|
||||
num_directions = 2 if self.bidirectional else 1
|
||||
self._all_weights = []
|
||||
for layer in range(num_layers):
|
||||
for direction in range(num_directions):
|
||||
suffix = '_reverse' if direction == 1 else ''
|
||||
weights = ['weight_ih_l{}{}', 'weight_hh_l{}{}', 'bias_ih_l{}{}', 'bias_hh_l{}{}']
|
||||
weights = [x.format(layer, suffix) for x in weights]
|
||||
if self.bias:
|
||||
self._all_weights += [weights]
|
||||
else:
|
||||
self._all_weights += [weights[:2]]
|
||||
|
||||
@property
|
||||
def all_weights(self):
|
||||
return [[getattr(self, weight) for weight in weights] for weights in self._all_weights]
|
||||
|
||||
|
||||
class QuantRNN(QuantRNNBase):
|
||||
r"""Applies a multi-layer Elman RNN with `tanh` or `ReLU` non-linearity to an
|
||||
input sequence.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'proj_size' in kwargs:
|
||||
raise ValueError("proj_size argument is only supported for LSTM, not RNN or GRU")
|
||||
if 'nonlinearity' in kwargs:
|
||||
if kwargs['nonlinearity'] == 'tanh':
|
||||
mode = 'RNN_TANH'
|
||||
elif kwargs['nonlinearity'] == 'relu':
|
||||
mode = 'RNN_RELU'
|
||||
else:
|
||||
raise ValueError("Unknown nonlinearity '{}'".format(
|
||||
kwargs['nonlinearity']))
|
||||
del kwargs['nonlinearity']
|
||||
else:
|
||||
mode = 'RNN_TANH'
|
||||
|
||||
super(QuantRNN, self).__init__(mode, *args, **kwargs)
|
||||
|
||||
|
||||
class QuantLSTM(QuantRNNBase):
|
||||
r"""Applies a multi-layer long short-term memory (LSTM) RNN to an input
|
||||
sequence.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(QuantLSTM, self).__init__('LSTM', *args, **kwargs)
|
||||
|
||||
|
||||
class GRU(QuantRNNBase):
|
||||
r"""Applies a multi-layer gated recurrent unit (GRU) RNN to an input sequence.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(GRU, self).__init__('GRU', *args, **kwargs)
|
||||
|
||||
class QuantRNNCellBase(nn.Module, _utils.QuantMixin):
|
||||
|
||||
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW
|
||||
|
||||
def extra_repr(self):
|
||||
s = '{input_size}, {hidden_size}'
|
||||
if 'bias' in self.__dict__ and self.bias is not True:
|
||||
s += ', bias={bias}'
|
||||
if 'nonlinearity' in self.__dict__ and self.nonlinearity != "tanh":
|
||||
s += ', nonlinearity={nonlinearity}'
|
||||
return s.format(**self.__dict__)
|
||||
|
||||
def check_forward_input(self, input):
|
||||
if input.size(1) != self.input_size:
|
||||
raise RuntimeError(
|
||||
"input has inconsistent input_size: got {}, expected {}".format(
|
||||
input.size(1), self.input_size))
|
||||
|
||||
def check_forward_hidden(self, input, hx, hidden_label=''):
|
||||
if input.size(0) != hx.size(0):
|
||||
raise RuntimeError(
|
||||
"Input batch size {} doesn't match hidden{} batch size {}".format(
|
||||
input.size(0), hidden_label, hx.size(0)))
|
||||
|
||||
if hx.size(1) != self.hidden_size:
|
||||
raise RuntimeError(
|
||||
"hidden{} has inconsistent hidden_size: got {}, expected {}".format(
|
||||
hidden_label, hx.size(1), self.hidden_size))
|
||||
|
||||
|
||||
class QuantRNNCell(QuantRNNCellBase):
|
||||
r"""An Elman RNN cell with tanh or ReLU non-linearity.
|
||||
"""
|
||||
|
||||
def __init__(self, input_size, hidden_size, bias=True, nonlinearity="tanh"):
|
||||
super(QuantRNNCell, self).__init__()
|
||||
self.input_size = input_size
|
||||
self.hidden_size = hidden_size
|
||||
self.bias = bias
|
||||
self.nonlinearity = nonlinearity
|
||||
self.weight_ih = Parameter(torch.Tensor(hidden_size, input_size))
|
||||
self.weight_hh = Parameter(torch.Tensor(hidden_size, hidden_size))
|
||||
if bias:
|
||||
self.bias_ih = Parameter(torch.Tensor(hidden_size))
|
||||
self.bias_hh = Parameter(torch.Tensor(hidden_size))
|
||||
else:
|
||||
self.register_parameter('bias_ih', None)
|
||||
self.register_parameter('bias_hh', None)
|
||||
self.reset_parameters()
|
||||
|
||||
def reset_parameters(self):
|
||||
stdv = 1.0 / math.sqrt(self.hidden_size)
|
||||
for weight in self.parameters():
|
||||
weight.data.uniform_(-stdv, stdv)
|
||||
|
||||
def forward(self, input, hx=None):
|
||||
self.check_forward_input(input)
|
||||
if hx is None:
|
||||
hx = input.new_zeros(input.size(0), self.hidden_size, requires_grad=False)
|
||||
self.check_forward_hidden(input, hx)
|
||||
if self.nonlinearity == "tanh":
|
||||
func = quant_rnn.RNNTanhCell
|
||||
elif self.nonlinearity == "relu":
|
||||
func = quant_rnn.RNNReLUCell
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Unknown nonlinearity: {}".format(self.nonlinearity))
|
||||
|
||||
return func(
|
||||
input, hx,
|
||||
self.weight_ih, self.weight_hh,
|
||||
self.bias_ih, self.bias_hh,
|
||||
)
|
||||
|
||||
|
||||
class QuantLSTMCell(QuantRNNCellBase):
|
||||
r"""A long short-term memory (LSTM) cell.
|
||||
"""
|
||||
|
||||
def __init__(self, input_size, hidden_size, bias=True, **kwargs):
|
||||
super(QuantLSTMCell, self).__init__()
|
||||
self.input_size = input_size
|
||||
self.hidden_size = hidden_size
|
||||
self.bias = bias
|
||||
self.weight_ih = Parameter(torch.Tensor(4 * hidden_size, input_size))
|
||||
self.weight_hh = Parameter(torch.Tensor(4 * hidden_size, hidden_size))
|
||||
if bias:
|
||||
self.bias_ih = Parameter(torch.Tensor(4 * hidden_size))
|
||||
self.bias_hh = Parameter(torch.Tensor(4 * hidden_size))
|
||||
else:
|
||||
self.register_parameter('bias_ih', None)
|
||||
self.register_parameter('bias_hh', None)
|
||||
self.reset_parameters()
|
||||
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
|
||||
self.init_quantizer(quant_desc_input, quant_desc_weight)
|
||||
|
||||
def reset_parameters(self):
|
||||
stdv = 1.0 / math.sqrt(self.hidden_size)
|
||||
for weight in self.parameters():
|
||||
weight.data.uniform_(-stdv, stdv)
|
||||
|
||||
def forward(self, input, hx=None):
|
||||
self.check_forward_input(input)
|
||||
if hx is None:
|
||||
hx = input.new_zeros(input.size(0), self.hidden_size, requires_grad=False)
|
||||
hx = (hx, hx)
|
||||
self.check_forward_hidden(input, hx[0], '[0]')
|
||||
self.check_forward_hidden(input, hx[1], '[1]')
|
||||
return quant_rnn.LSTMCell(
|
||||
input, hx,
|
||||
self.weight_ih, self.weight_hh,
|
||||
self.bias_ih, self.bias_hh,
|
||||
self._input_quantizer, self._weight_quantizer
|
||||
)
|
||||
|
||||
|
||||
class GRUCell(QuantRNNCellBase):
|
||||
r"""A gated recurrent unit (GRU) cell
|
||||
"""
|
||||
|
||||
def __init__(self, input_size, hidden_size, bias=True):
|
||||
super(GRUCell, self).__init__()
|
||||
self.input_size = input_size
|
||||
self.hidden_size = hidden_size
|
||||
self.bias = bias
|
||||
self.weight_ih = Parameter(torch.Tensor(3 * hidden_size, input_size))
|
||||
self.weight_hh = Parameter(torch.Tensor(3 * hidden_size, hidden_size))
|
||||
if bias:
|
||||
self.bias_ih = Parameter(torch.Tensor(3 * hidden_size))
|
||||
self.bias_hh = Parameter(torch.Tensor(3 * hidden_size))
|
||||
else:
|
||||
self.register_parameter('bias_ih', None)
|
||||
self.register_parameter('bias_hh', None)
|
||||
self.reset_parameters()
|
||||
|
||||
def reset_parameters(self):
|
||||
stdv = 1.0 / math.sqrt(self.hidden_size)
|
||||
for weight in self.parameters():
|
||||
weight.data.uniform_(-stdv, stdv)
|
||||
|
||||
def forward(self, input, hx=None):
|
||||
self.check_forward_input(input)
|
||||
if hx is None:
|
||||
hx = input.new_zeros(input.size(0), self.hidden_size, requires_grad=False)
|
||||
self.check_forward_hidden(input, hx)
|
||||
return quant_rnn.GRUCell(
|
||||
input, hx,
|
||||
self.weight_ih, self.weight_hh,
|
||||
self.bias_ih, self.bias_hh,
|
||||
)
|
||||
|
||||
LSTM = QuantLSTM
|
||||
LSTMCell = QuantLSTMCell
|
||||
@@ -0,0 +1,456 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
"""TensorQuantizer Module"""
|
||||
import math
|
||||
from absl import logging
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor, tensor_quant, fake_tensor_quant, scaled_e4m3
|
||||
from pytorch_quantization.nn.modules.clip import Clip
|
||||
|
||||
from pytorch_quantization import calib
|
||||
|
||||
import pytorch_quantization.utils as quant_utils
|
||||
|
||||
__all__ = ['TensorQuantizer']
|
||||
|
||||
|
||||
class TensorQuantizer(nn.Module):
|
||||
"""Tensor quantizer module
|
||||
|
||||
This module uses tensor_quant or fake_tensor_quant function to quantize a tensor. And wrappers variable, moving
|
||||
statistics we'd want when training a quantized network.
|
||||
|
||||
Experimental features:
|
||||
``clip`` stage learns range before enabling quantization.
|
||||
``calib`` stage runs calibration
|
||||
|
||||
Args:
|
||||
quant_desc: An instance of :func:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
disabled: A boolean. If True, by pass the whole module returns input. Default False.
|
||||
if_quant: A boolean. If True, run main quantization body. Default True.
|
||||
if_clip: A boolean. If True, clip before quantization and learn amax. Default False.
|
||||
if_calib: A boolean. If True, run calibration. Not implemented yet. Settings of calibration will probably
|
||||
go to :func:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
|
||||
|
||||
Raises:
|
||||
|
||||
Readonly Properties:
|
||||
- axis:
|
||||
- fake_quant:
|
||||
- scale:
|
||||
- step_size:
|
||||
|
||||
Mutable Properties:
|
||||
- num_bits:
|
||||
- unsigned:
|
||||
- amax:
|
||||
"""
|
||||
|
||||
_enable_onnx_export = False
|
||||
|
||||
def __init__(self, quant_desc=QuantDescriptor(), disabled=False, if_quant=True, if_clip=False, if_calib=False):
|
||||
"""Initialize quantizer and set up required variables"""
|
||||
super(TensorQuantizer, self).__init__()
|
||||
# Expand quant_desc. Use quant_desc.dict would be eaiser, but adding one-by-one explicitly gives more control
|
||||
self._num_bits = quant_desc.num_bits
|
||||
self._fake_quant = quant_desc.fake_quant
|
||||
self._axis = quant_desc.axis
|
||||
self._scale_amax = quant_desc.scale_amax
|
||||
self._learn_amax = quant_desc.learn_amax
|
||||
self._unsigned = quant_desc.unsigned
|
||||
self._narrow_range = quant_desc.narrow_range
|
||||
|
||||
self._scale = None if not quant_desc.fake_quant else 1.
|
||||
self._disabled = disabled
|
||||
self._if_quant = if_quant
|
||||
self._if_clip = False
|
||||
self._if_calib = if_calib
|
||||
|
||||
if quant_desc.amax is not None:
|
||||
self.register_buffer('_amax', torch.tensor(quant_desc.amax))
|
||||
|
||||
# Clip module consumes a lot of memory, so only create it if learn_amax is True
|
||||
if self._learn_amax:
|
||||
init_amax = quant_desc.amax if quant_desc.amax is not None else 1.
|
||||
self.clip = Clip(-init_amax, init_amax, learn_min=True, learn_max=True)
|
||||
# It makes more sense to enable clip stage (which learns amax) if learn_amax is true
|
||||
self.enable_clip()
|
||||
if if_clip:
|
||||
self.enable_clip()
|
||||
|
||||
if quant_desc.calib_method == "histogram":
|
||||
logging.info("Creating histogram calibrator")
|
||||
self._calibrator = calib.HistogramCalibrator(num_bits=self._num_bits,
|
||||
axis=self._axis,
|
||||
unsigned=self._unsigned)
|
||||
elif quant_desc.calib_method == "max":
|
||||
logging.info("Creating Max calibrator")
|
||||
self._calibrator = calib.MaxCalibrator(num_bits=self._num_bits, axis=self._axis, unsigned=self._unsigned)
|
||||
|
||||
# pylint:disable=missing-docstring
|
||||
@property
|
||||
def num_bits(self):
|
||||
return self._num_bits
|
||||
|
||||
@property
|
||||
def maxbound(self):
|
||||
if self._num_bits == (4, 3):
|
||||
return 448.0
|
||||
return (1 << (self._num_bits - 1 + int(self._unsigned))) - 1
|
||||
|
||||
@property
|
||||
def unsigned(self):
|
||||
return self._unsigned
|
||||
|
||||
@property
|
||||
def scale(self):
|
||||
if self._fake_quant:
|
||||
logging.error("Fake quantize mode doesn't use scale explicitly!")
|
||||
if self._scale is None:
|
||||
logging.critical("Accessing scale before quantizing any tensor!")
|
||||
return self._scale
|
||||
|
||||
@property
|
||||
def pre_quant_scale(self):
|
||||
if not hasattr(self, "_pre_quant_scale"):
|
||||
return None
|
||||
return self._pre_quant_scale
|
||||
|
||||
@property
|
||||
def amax(self):
|
||||
if not hasattr(self, "_amax"):
|
||||
return None
|
||||
return self._amax
|
||||
|
||||
@property
|
||||
def step_size(self):
|
||||
if not hasattr(self, "_amax"):
|
||||
logging.error("step_size is undefined under dynamic amax mode!")
|
||||
return None
|
||||
return self._amax / (2.0**(self._num_bits - 1 + int(self._unsigned)) - 1.0)
|
||||
|
||||
@property
|
||||
def axis(self):
|
||||
return self._axis
|
||||
|
||||
@property
|
||||
def fake_quant(self):
|
||||
return self._fake_quant
|
||||
|
||||
@property
|
||||
def narrow_range(self):
|
||||
return self._narrow_range
|
||||
|
||||
def disable(self):
|
||||
"""Bypass the module"""
|
||||
self._disabled = True
|
||||
|
||||
def enable(self):
|
||||
self._disabled = False
|
||||
|
||||
def disable_clip(self):
|
||||
"""Disable clip stage"""
|
||||
self._if_clip = False
|
||||
self.clip.clip_value_min.requires_grad = False
|
||||
self.clip.clip_value_max.requires_grad = False
|
||||
|
||||
def enable_clip(self):
|
||||
"""Enable clip stage"""
|
||||
logging.warning("Enable `clip` stage for amax learning.")
|
||||
if not self._learn_amax:
|
||||
raise ValueError("learn_amax is False. Cannot enable clip.")
|
||||
self.clip.clip_value_min.requires_grad = True
|
||||
self.clip.clip_value_max.requires_grad = True
|
||||
self._if_clip = True
|
||||
|
||||
def disable_calib(self):
|
||||
logging.warning("Disable {}".format(self._calibrator.__class__.__name__))
|
||||
self._if_calib = False
|
||||
|
||||
def enable_calib(self):
|
||||
if self._calibrator is None:
|
||||
raise ValueError("Calibrator was not created, cannot enable calibration.")
|
||||
logging.info("Enable {}".format(self._calibrator.__class__.__name__))
|
||||
self._if_calib = True
|
||||
|
||||
def disable_quant(self):
|
||||
logging.info("Disable `quant` stage.")
|
||||
self._if_quant = False
|
||||
|
||||
def enable_quant(self):
|
||||
logging.info("Enable `quant` stage.")
|
||||
self._if_quant = True
|
||||
|
||||
@amax.setter
|
||||
def amax(self, value):
|
||||
if value is None:
|
||||
logging.error("Setting amax no None is meaningless.")
|
||||
else:
|
||||
if isinstance(value, torch.Tensor):
|
||||
logging.warning("amax setter is not designed to take tensor.")
|
||||
if not hasattr(self, "_amax"):
|
||||
self.register_buffer('_amax', torch.tensor(value))
|
||||
else:
|
||||
value = torch.tensor(value, device=self._amax.device)
|
||||
if self._amax.shape != value.shape:
|
||||
raise RuntimeError("Changing shape when setting amax is not allowed.")
|
||||
self._amax.data.copy_(value.data)
|
||||
|
||||
@pre_quant_scale.setter
|
||||
def pre_quant_scale(self, value):
|
||||
if value is None:
|
||||
logging.error("Setting pre_quant_scale no None is meaningless.")
|
||||
else:
|
||||
if not hasattr(self, "_pre_quant_scale"):
|
||||
self.register_buffer('_pre_quant_scale', torch.tensor(value))
|
||||
else:
|
||||
value = torch.tensor(value, device=self._pre_quant_scale.device)
|
||||
if self._pre_quant_scale.shape != value.shape:
|
||||
raise RuntimeError("Changing shape when setting pre_quant_scale is not allowed.")
|
||||
self._pre_quant_scale.data.copy_(value.data)
|
||||
|
||||
@num_bits.setter
|
||||
def num_bits(self, value):
|
||||
self._num_bits = value
|
||||
|
||||
@unsigned.setter
|
||||
def unsigned(self, value):
|
||||
self._unsigned = value
|
||||
|
||||
@narrow_range.setter
|
||||
def narrow_range(self, value):
|
||||
self._narrow_range = value
|
||||
|
||||
# pylint:enable=missing-docstring
|
||||
def load_calib_amax(self, *args, **kwargs):
|
||||
"""Load amax from calibrator.
|
||||
|
||||
Updates the amax buffer with value computed by the calibrator, creating it if necessary.
|
||||
*args and **kwargs are directly passed to compute_amax, except "strict" in kwargs. Refer to
|
||||
compute_amax for more details.
|
||||
"""
|
||||
strict = kwargs.pop("strict", True)
|
||||
if getattr(self, '_calibrator', None) is None:
|
||||
raise RuntimeError("Calibrator not created.")
|
||||
calib_amax = self._calibrator.compute_amax(*args, **kwargs)
|
||||
if calib_amax is None:
|
||||
err_msg = "Calibrator returned None. This usually happens when calibrator hasn't seen any tensor."
|
||||
if not strict:
|
||||
logging.warning(err_msg)
|
||||
logging.warning("Set amax to NaN!")
|
||||
calib_amax = torch.tensor(math.nan)
|
||||
else:
|
||||
raise RuntimeError(err_msg + " Passing 'strict=False' to `load_calib_amax()` will ignore the error.")
|
||||
logging.warning("Load calibrated amax, shape={}.".format(calib_amax.shape))
|
||||
logging.log_first_n(logging.WARNING, "Call .cuda() if running on GPU after loading calibrated amax.", 1)
|
||||
if not hasattr(self, '_amax'):
|
||||
self.register_buffer("_amax", calib_amax.data)
|
||||
else:
|
||||
self._amax.copy_(calib_amax)
|
||||
|
||||
def init_learn_amax(self):
|
||||
"""Initialize learned amax from fixed amax"""
|
||||
if self._learn_amax is False:
|
||||
raise RuntimeError("Called init_learn_amax with learn_amax=False.")
|
||||
logging.warning("Load amax as initial value for amax learning!")
|
||||
if self._amax.numel() != 1:
|
||||
logging.warning("Per channel learned amax not supported. Initializing with max(amax).")
|
||||
init_amax = torch.max(self._amax)
|
||||
else:
|
||||
init_amax = self._amax
|
||||
self.clip.clip_value_min.data.copy_(-init_amax.data)
|
||||
self.clip.clip_value_max.data.copy_(init_amax.data)
|
||||
|
||||
def _get_amax(self, inputs):
|
||||
"""get amax from buffer or compute it dynamically."""
|
||||
if hasattr(self, '_amax'):
|
||||
amax = self._amax
|
||||
else:
|
||||
if self._axis is None:
|
||||
reduce_axis = None
|
||||
else:
|
||||
reduce_axis = []
|
||||
# Swap axis to reduce
|
||||
axis = self._axis if isinstance(self._axis, (list, tuple)) else [self._axis]
|
||||
for i in range(inputs.dim()):
|
||||
if not i in axis:
|
||||
reduce_axis.append(i)
|
||||
amax = quant_utils.reduce_amax(inputs, axis=reduce_axis, keepdims=True).detach()
|
||||
if self._scale_amax is not None:
|
||||
amax = amax.detach() * self._scale_amax
|
||||
|
||||
amax = amax.data
|
||||
|
||||
# cast amax to float32 if it is in a lower precision dtype
|
||||
if amax.dtype not in (torch.double, torch.float):
|
||||
amax = amax.float()
|
||||
|
||||
return amax
|
||||
|
||||
def _quant_forward(self, inputs):
|
||||
"""Quantized forward pass."""
|
||||
if self._learn_amax:
|
||||
inputs = self.clip(inputs)
|
||||
amax = torch.max(-self.clip.clip_value_min, self.clip.clip_value_max).detach()
|
||||
else:
|
||||
amax = self._get_amax(inputs)
|
||||
|
||||
if self._fake_quant:
|
||||
outputs = fake_tensor_quant(inputs, amax, self._num_bits, self._unsigned, self._narrow_range)
|
||||
else:
|
||||
outputs, self._scale = tensor_quant(inputs, amax, self._num_bits, self._unsigned)
|
||||
|
||||
return outputs
|
||||
|
||||
def _check_onnx_readiness(self, inputs):
|
||||
"""Check if quantizer is ready for ONNX export."""
|
||||
|
||||
assert hasattr(
|
||||
self, '_amax'), ("Quantizer has not been calibrated. ONNX export requires the quantizer to be calibrated."
|
||||
"Calibrate and load amax before exporting to ONNX.")
|
||||
|
||||
if self._if_calib:
|
||||
logging.warning("Quantizer is in calibration mode. "
|
||||
"Please complete calibration before exporting to ONNX for correct results.")
|
||||
|
||||
amax = self._get_amax(inputs)
|
||||
|
||||
# We only support scalar amax for E4M3 ONNX export
|
||||
if isinstance(self.num_bits, tuple):
|
||||
assert amax.numel() == 1, ("E4M3 supports ONNX export only for per-tensor quantization."
|
||||
" Per-tensor quantization requires scalar amax. "
|
||||
f"Received non-scalar amax of shape: {amax.shape}")
|
||||
|
||||
def forward(self, inputs):
|
||||
"""Apply tensor_quant function to inputs
|
||||
|
||||
Args:
|
||||
inputs: A Tensor of type float32.
|
||||
|
||||
Returns:
|
||||
outputs: A Tensor of type output_dtype
|
||||
"""
|
||||
|
||||
if self._enable_onnx_export:
|
||||
self._check_onnx_readiness(inputs)
|
||||
|
||||
# Activation scaling for smoothquant
|
||||
if self.pre_quant_scale is not None:
|
||||
inputs = inputs * self.pre_quant_scale
|
||||
|
||||
if self._disabled:
|
||||
return inputs
|
||||
|
||||
outputs = inputs
|
||||
|
||||
if self._if_calib:
|
||||
if self._calibrator is None:
|
||||
raise RuntimeError("Calibrator was not created.")
|
||||
# Shape is only known when it sees the first tensor
|
||||
self._calibrator.collect(inputs)
|
||||
|
||||
if self._if_clip:
|
||||
if not self._learn_amax:
|
||||
raise RuntimeError("Clip without learning amax is not implemented.")
|
||||
outputs = self.clip(inputs)
|
||||
|
||||
if self._if_quant:
|
||||
if not isinstance(self._num_bits, tuple):
|
||||
outputs = self._quant_forward(inputs)
|
||||
else:
|
||||
E, M = self._num_bits
|
||||
outputs = scaled_e4m3(inputs, self._get_amax(inputs), E, M)
|
||||
|
||||
return outputs
|
||||
|
||||
def _short_amax(self, fmt='.4f'):
|
||||
"""Short description of amax
|
||||
|
||||
Returns:
|
||||
'dynamic': if _amax is not registered
|
||||
'amax': if _amax is per-tensor
|
||||
'[min, max](size)': if _amax is per-channel
|
||||
"""
|
||||
if not hasattr(self, '_amax'):
|
||||
return 'dynamic'
|
||||
if self._amax is None:
|
||||
return "None"
|
||||
if self._amax.numel() == 1:
|
||||
return '{:{fmt}}'.format(self._amax.item(), fmt=fmt)
|
||||
return '[{:{fmt}}, {:{fmt}}]({})'.format(self._amax.min().item(),
|
||||
self._amax.max().item(),
|
||||
self._amax.numel(),
|
||||
fmt=fmt)
|
||||
|
||||
def extra_repr(self):
|
||||
if self._disabled:
|
||||
return "disabled"
|
||||
s = "{}{}bit".format("unsigned " if self._unsigned else "", self._num_bits)
|
||||
s += " narrow" if (self._narrow_range) else ""
|
||||
s += " fake" if (self._fake_quant) else ""
|
||||
s += " axis={}".format(self._axis) if self._axis is not None else " per-tensor"
|
||||
s += " amax={}".format(self._short_amax())
|
||||
s += " *{}".format(self._scale_amax) if self._scale_amax else ""
|
||||
s += " pre_quant_scale" if self.pre_quant_scale is not None else ""
|
||||
s += " learned" if (self._learn_amax) else ""
|
||||
s += " calibrator={}".format(self._calibrator.__class__.__name__) if (self._calibrator is not None) else ""
|
||||
s += " scale={}".format(self._scale) if self._scale is not None else ""
|
||||
s += " quant" if (self._if_quant) else ""
|
||||
s += " clip" if (self._if_clip) else ""
|
||||
s += " calib" if (self._if_calib) else ""
|
||||
return s
|
||||
|
||||
def _load_from_state_dict(self, state_dict, prefix, *args, **kwargs):
|
||||
"""Overloaded module function
|
||||
|
||||
Adds warnings during state_dict loading.
|
||||
A workaround is implemented for loading amax from checkpoint and only supports CUDA.
|
||||
|
||||
Args:
|
||||
state_dict: A dict containing the state of the top level module
|
||||
prefix: A string that prefixes all of this modules state in state_dict, e.g. 'model.conv1.'
|
||||
"""
|
||||
dst_has_amax = '_amax' in self._buffers
|
||||
src_has_amax = prefix + '_amax' in state_dict
|
||||
|
||||
if not src_has_amax and dst_has_amax:
|
||||
logging.error("{}: No amax in state_dict.".format(prefix[:-1]))
|
||||
elif src_has_amax and not dst_has_amax:
|
||||
logging.debug(("{}: No '_amax' buffer to load amax into."
|
||||
" '_amax` will be created as WAR for now. "
|
||||
"This behavior will change in future.").format(prefix[:-1]))
|
||||
self.register_buffer("_amax", state_dict[prefix + '_amax'].data.cuda())
|
||||
elif src_has_amax and dst_has_amax:
|
||||
logging.warning("{}: Overwriting amax.".format(prefix[:-1]))
|
||||
|
||||
dst_has_pre_quant_scale = '_pre_quant_scale' in self._buffers
|
||||
src_has_pre_quant_scale = prefix + '_pre_quant_scale' in state_dict
|
||||
|
||||
if not src_has_pre_quant_scale and dst_has_pre_quant_scale:
|
||||
logging.error("{}: No pre_quant_scale in state_dict.".format(prefix[:-1]))
|
||||
elif src_has_pre_quant_scale and not dst_has_pre_quant_scale:
|
||||
logging.debug(("{}: No '_pre_quant_scale' buffer to load pre_quant_scale into."
|
||||
" '_pre_quant_scale` will be created as WAR for now. "
|
||||
"This behavior will change in future.").format(prefix[:-1]))
|
||||
self.register_buffer("_pre_quant_scale", state_dict[prefix + '_pre_quant_scale'].data.cuda())
|
||||
elif src_has_pre_quant_scale and dst_has_pre_quant_scale:
|
||||
logging.warning("{}: Overwriting pre_quant_scale.".format(prefix[:-1]))
|
||||
|
||||
super(TensorQuantizer, self)._load_from_state_dict(state_dict, prefix, *args, **kwargs)
|
||||
@@ -0,0 +1,131 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Helper functions for quant optimizer/trainer"""
|
||||
|
||||
import re
|
||||
|
||||
from absl import logging
|
||||
|
||||
def match_parameters(model, patterns):
|
||||
"""Returns an generator over module parameters if name matches key
|
||||
|
||||
It is useful to group parameters, and apply different functions to different group. This function provides an easy
|
||||
way to group them.
|
||||
|
||||
Args:
|
||||
model: A Module
|
||||
patterns: A list of strings that will be used to match parameter names. If parameter name contains any pattern,
|
||||
it will be yield
|
||||
|
||||
Yields:
|
||||
param: Module parameters
|
||||
"""
|
||||
for name, param in model.named_parameters():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, name):
|
||||
yield param
|
||||
|
||||
def group_parameters(model, patterns_list, lrs=None, momentums=None, weight_decays=None):
|
||||
"""Group parameters for using per-parameters option in optimizer
|
||||
|
||||
Returns a list of dict that matches Pytorch optimizer fashion, see
|
||||
https://pytorch.org/docs/stable/optim.html#per-parameter-options for more details.
|
||||
|
||||
Example:
|
||||
>>> [
|
||||
>>> {'params': model.base.parameters()},
|
||||
>>> {'params': model.classifier.parameters(), 'lr': 1e-3}
|
||||
>>> ]
|
||||
Parameters will be grouped w.r.t first level of the keys_list. e.g. `keys_list=[['conv1', 'conv2'], ['conv3']]` will
|
||||
return 2 groups, one with `conv1` and `conv2` in name, and the other with `conv3` in name.
|
||||
|
||||
If lr, momentum or weight_decay are supplied, they will be added to the group as well.
|
||||
|
||||
|
||||
Args:
|
||||
model: A module
|
||||
patterns_list: A list of list of strings. WARNING: patters must be EXCLUSIVE, the function doesn't
|
||||
perform exclusive check.
|
||||
lrs: A list of float with same length as keys_list or None.
|
||||
momentums: A list of float with same length as keys_list or None.
|
||||
weight_decays: A list of float with same length as keys_list or None.
|
||||
|
||||
Returns:
|
||||
param_group: A list of dict
|
||||
|
||||
"""
|
||||
param_groups = []
|
||||
for pattern in patterns_list:
|
||||
if not isinstance(pattern, list):
|
||||
raise TypeError("patterns_list must be list of list of patterns")
|
||||
param_groups.append({'params': match_parameters(model, pattern)})
|
||||
|
||||
if lrs is not None:
|
||||
if len(lrs) != len(patterns_list):
|
||||
raise TypeError("len(lrs) must match len(patterns_list)")
|
||||
for i, lr in enumerate(lrs):
|
||||
param_groups[i]['lr'] = lr
|
||||
|
||||
if momentums is not None:
|
||||
if len(momentums) != len(patterns_list):
|
||||
raise TypeError("len(momentums) must match len(patterns_list)")
|
||||
for i, momentum in enumerate(momentums):
|
||||
param_groups[i]['momentum'] = momentum
|
||||
|
||||
if weight_decays is not None:
|
||||
if len(weight_decays) != len(patterns_list):
|
||||
raise TypeError("len(weight_decays) must match len(patterns_list)")
|
||||
for i, weight_decay in enumerate(weight_decays):
|
||||
param_groups[i]['weight_decay'] = weight_decay
|
||||
|
||||
return param_groups
|
||||
|
||||
def freeze_parameters(model, patterns):
|
||||
"""Set requires_grad to False if patterns match name
|
||||
|
||||
Args:
|
||||
model: A Module
|
||||
patterns: A list of strings that will be used to match parameter names. If parameter name contains any pattern,
|
||||
it will be frozen.
|
||||
"""
|
||||
for name, param in model.named_parameters():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, name):
|
||||
logging.warning("Freeze %s.", name)
|
||||
param.requires_grad = False
|
||||
|
||||
def quant_weight_inplace(model):
|
||||
"""Make quantization inplace
|
||||
|
||||
Search for quantized modules including QuantConvNd and QuantLinear, make weight quantization in place using
|
||||
weight_quantizer.
|
||||
|
||||
Most publications of quantization aware training uses STE by default, which is really an approximation of
|
||||
derivative of the nondifferentiable quantization function, which works to some extended but by no means the F=ma of
|
||||
the problem.
|
||||
Inplace quantization can be used to implement relax-and-round, which is a common method in Discrete Optimization's
|
||||
or Integer Programming.
|
||||
"""
|
||||
for name, module in model.named_modules():
|
||||
if hasattr(module, '_weight_quantizer') and module.weight_quantizer is not None:
|
||||
if not module.weight_quantizer.fake_quant:
|
||||
logging.warning(("In-place real quantization is VERY dangerous and should be used for inference only. "
|
||||
"Make sure that is the desired behavior."))
|
||||
logging.warning("In-place quantize weight of %s", name)
|
||||
module.weight.data.copy_(module.weight_quantizer(module.weight))
|
||||
@@ -0,0 +1,182 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
"""Dynamically replace the modules with quantized versions."""
|
||||
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
|
||||
import torch
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
|
||||
__all__ = ['initialize', 'deactivate', 'enable_onnx_export']
|
||||
|
||||
# Definition of the named tuple that is used to store mapping of the quantized modules
|
||||
_quant_entry = namedtuple('quant_entry', 'orig_mod mod_name replace_mod')
|
||||
|
||||
# Global member of the file that contains the mapping of quantized modules
|
||||
_DEFAULT_QUANT_MAP = [
|
||||
_quant_entry(torch.nn, "Conv1d", quant_nn.QuantConv1d),
|
||||
_quant_entry(torch.nn, "Conv2d", quant_nn.QuantConv2d),
|
||||
_quant_entry(torch.nn, "Conv3d", quant_nn.QuantConv3d),
|
||||
_quant_entry(torch.nn, "ConvTranspose1d", quant_nn.QuantConvTranspose1d),
|
||||
_quant_entry(torch.nn, "ConvTranspose2d", quant_nn.QuantConvTranspose2d),
|
||||
_quant_entry(torch.nn, "ConvTranspose3d", quant_nn.QuantConvTranspose3d),
|
||||
_quant_entry(torch.nn, "Linear", quant_nn.QuantLinear),
|
||||
_quant_entry(torch.nn, "LSTM", quant_nn.QuantLSTM),
|
||||
_quant_entry(torch.nn, "LSTMCell", quant_nn.QuantLSTMCell),
|
||||
_quant_entry(torch.nn, "AvgPool1d", quant_nn.QuantAvgPool1d),
|
||||
_quant_entry(torch.nn, "AvgPool2d", quant_nn.QuantAvgPool2d),
|
||||
_quant_entry(torch.nn, "AvgPool3d", quant_nn.QuantAvgPool3d),
|
||||
_quant_entry(torch.nn, "AdaptiveAvgPool1d", quant_nn.QuantAdaptiveAvgPool1d),
|
||||
_quant_entry(torch.nn, "AdaptiveAvgPool2d", quant_nn.QuantAdaptiveAvgPool2d),
|
||||
_quant_entry(torch.nn, "AdaptiveAvgPool3d", quant_nn.QuantAdaptiveAvgPool3d),
|
||||
]
|
||||
|
||||
|
||||
class QuantModuleReplacementHelper():
|
||||
"""To help replace torch.nn modules with quantized versions.
|
||||
|
||||
This module is used to replace (by monkey patching) the torch.nn modules with their
|
||||
quantized versions as provided by either tool's internal implementation or any other
|
||||
user provided custom module.
|
||||
|
||||
Attributes:
|
||||
orginal_func_map: A dict. Maintains the original torch.nn module mapping.
|
||||
quant_support_list: A list. Contains the names of modules for which a quantized
|
||||
version is provided by the tool.
|
||||
quant_map: A dict. Contains the map of the module name and its quantized versions.
|
||||
quant_switch_opt: A dict. A map to indicate which modules to be left unreplaced with
|
||||
their quantized versions. This dict is updated by a list provided from the user
|
||||
which indicates the modules to leave out in monkey patching.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Will hold the original modules to be replaced back
|
||||
self.orginal_func_map = set()
|
||||
|
||||
# Maintains the list of supported quantized modules by the tool as default
|
||||
self.default_quant_map = _DEFAULT_QUANT_MAP
|
||||
|
||||
# Will hold the final quantized modules after checking if user supplied any
|
||||
# custom quantized functions.
|
||||
self.quant_map = set()
|
||||
|
||||
def prepare_state(self, float_module_list=None, custom_map=None):
|
||||
"""
|
||||
Prepare the internal variables that would used in the monkey patching mechanism later.
|
||||
1. Set up the list of quantized modules that are supported by the tool for torch.nn.
|
||||
2. Set up the custom mapping for modules other than torch.nn.
|
||||
3. Use the float_module_list to switch off the monkey patching replacement for user indicated modules
|
||||
"""
|
||||
|
||||
# For the default quantized modules supported, generate the quant_map
|
||||
for item in self.default_quant_map:
|
||||
if float_module_list is not None and item.mod_name in float_module_list:
|
||||
# Skip this module if this is present in the float_module_list
|
||||
continue
|
||||
else:
|
||||
# append the modules into the variable that will be used in monkey patching
|
||||
self.quant_map.add(item)
|
||||
# also store the original module to be used in reverse monkey patching
|
||||
self.orginal_func_map.add(
|
||||
_quant_entry(item.orig_mod, item.mod_name, getattr(item.orig_mod, item.mod_name)))
|
||||
|
||||
# Add custom modules to the quant_map
|
||||
if custom_map is not None:
|
||||
for item in custom_map:
|
||||
# append the custom modules to the list that will be used in monkey patching
|
||||
# Note that we convert a tuple to a named tuple here
|
||||
self.quant_map.add(_quant_entry(item[0], item[1], item[2]))
|
||||
# also store the original module in another list which will be used to reverse monkey patching
|
||||
self.orginal_func_map.add(_quant_entry(item[0], item[1], getattr(item[0], item[1])))
|
||||
|
||||
def apply_quant_modules(self):
|
||||
"""
|
||||
For the modules registered in the quant_map, simply monkey patch them and also store the
|
||||
original modules so that they could be later replaced back.
|
||||
"""
|
||||
for entry in self.quant_map:
|
||||
setattr(entry.orig_mod, entry.mod_name, entry.replace_mod)
|
||||
|
||||
def restore_float_modules(self):
|
||||
"""
|
||||
Reverse the effect of monkey patch by using the orginal_func_map to replace back the
|
||||
original modules.
|
||||
"""
|
||||
for entry in self.orginal_func_map:
|
||||
setattr(entry.orig_mod, entry.mod_name, entry.replace_mod)
|
||||
|
||||
|
||||
def initialize(float_module_list=None, custom_quant_modules=None):
|
||||
"""Dynamic module replacement using monkey patching.
|
||||
|
||||
Dynamically monkey patches the modules with their quantized versions. Internally, the
|
||||
state is maintained by a helper class object which helps in replacing the original
|
||||
modules back.
|
||||
|
||||
Args:
|
||||
float_module_list: A list. User supplied list which indicates which modules to not monkey patch.
|
||||
custom_quant_modules: A dict. A mapping provided by user to indicate any other module apart
|
||||
from torch.nn and its corresponding quantized version.
|
||||
|
||||
Returns:
|
||||
nothing.
|
||||
|
||||
Typical usage example:
|
||||
|
||||
# Define the deny list for torch.nn modules and custom map for modules other than torch.nn.
|
||||
float_module_list = ["Linear"]
|
||||
custom_quant_modules = [(torch.nn, "Linear", quant_nn.QuantLinear)]
|
||||
## Monkey patch the modules
|
||||
pytorch_quantization.quant_modules.initialize(float_module_list, custom_modules)
|
||||
## Use the quantized modules
|
||||
pytorch_quantization.quant_modules.deactivate()
|
||||
"""
|
||||
_quant_module_helper_object.prepare_state(float_module_list, custom_quant_modules)
|
||||
_quant_module_helper_object.apply_quant_modules()
|
||||
|
||||
|
||||
def deactivate():
|
||||
"""Dynamic module replacement which reverses the monkey patching.
|
||||
|
||||
Dynamically replaces back the original modules that were monkey patched earlier
|
||||
in the initialize() function call using helper class object which maintains the state.
|
||||
"""
|
||||
_quant_module_helper_object.restore_float_modules()
|
||||
|
||||
|
||||
# Global object that maintains the state of the modules that are replaced.
|
||||
_quant_module_helper_object = QuantModuleReplacementHelper()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def enable_onnx_export():
|
||||
"""Context manager to enable onnx export.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with pytorch_quantization.enable_onnx_export():
|
||||
# export onnx model
|
||||
torch.onnx.export(model, ...)
|
||||
|
||||
"""
|
||||
quant_nn.TensorQuantizer._enable_onnx_export = True
|
||||
yield
|
||||
|
||||
quant_nn.TensorQuantizer._enable_onnx_export = False
|
||||
@@ -0,0 +1,624 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
"""Basic tensor quantization functions"""
|
||||
import numpy as np
|
||||
import numbers
|
||||
import yaml
|
||||
|
||||
from absl import logging
|
||||
|
||||
import torch
|
||||
import torch._C._onnx as _C_onnx
|
||||
from torch.autograd import Function
|
||||
|
||||
from pytorch_quantization import cuda_ext
|
||||
|
||||
from torch.onnx import symbolic_helper
|
||||
|
||||
|
||||
class ScaledQuantDescriptor():
|
||||
"""Supportive descriptor of quantization
|
||||
|
||||
Describe how a tensor should be quantized. A QuantDescriptor and a tensor defines a quantized tensor.
|
||||
|
||||
Args:
|
||||
num_bits: An integer or a tuple of two integers.
|
||||
Specifically, `num_bits` can be:
|
||||
|
||||
#. A positive integer argument for integer qunatization. `num_bits` specify
|
||||
the number of bits used for integer quantization.
|
||||
|
||||
#. Constant integer tuple (4,3) for E4M3 floating point quantization emulating
|
||||
Nvidia's FP8 quantization. E4M3 quantization only supports per-tensor quantization.
|
||||
|
||||
Default: 8.
|
||||
name: Seems a nice thing to have
|
||||
|
||||
Keyword Arguments:
|
||||
fake_quant: A boolean. If True, use fake quantization mode. Default True.
|
||||
axis: None, int or tuple of int. axes which will have its own max for computing scaling factor.
|
||||
If None (the default), use per tensor scale.
|
||||
Must be in the range [-rank(input_tensor), rank(input_tensor)).
|
||||
e.g. For a KCRS weight tensor, quant_axis=(0) will yield per channel scaling.
|
||||
Default None.
|
||||
amax: A float or list/ndarray of floats of user specified absolute max range. If supplied,
|
||||
ignore quant_axis and use this to quantize. If learn_amax is True, will be used to initialize
|
||||
learnable amax. Default None.
|
||||
learn_amax: A boolean. If True, learn amax. Default False.
|
||||
scale_amax: A float. If supplied, multiply amax by scale_amax. Default None. It is useful for some
|
||||
quick experiment.
|
||||
calib_method: A string. One of ["max", "histogram"] indicates which calibration to use. Except the simple
|
||||
max calibration, other methods are all hisogram based. Default "max".
|
||||
unsigned: A Boolean. If True, use unsigned. Default False.
|
||||
|
||||
Raises:
|
||||
TypeError: If unsupported type is passed in.
|
||||
|
||||
Read-only properties:
|
||||
- fake_quant:
|
||||
- name:
|
||||
- learn_amax:
|
||||
- scale_amax:
|
||||
- axis:
|
||||
- calib_method:
|
||||
- num_bits:
|
||||
- amax:
|
||||
- unsigned:
|
||||
"""
|
||||
|
||||
def __init__(self, num_bits=8, name=None, **kwargs):
|
||||
if isinstance(num_bits, int):
|
||||
if num_bits < 0:
|
||||
raise ValueError("num_bits must be > 0, not {}.".format(num_bits))
|
||||
if num_bits == 0:
|
||||
logging.error("num_bits is 0. This will result in the tensor being quantized to all zeros."
|
||||
" This mode should only be used for debugging purposes.")
|
||||
elif num_bits != (4, 3):
|
||||
raise TypeError("num_bits must be a postive integer or tuple (4,3), not {}.".format(type(num_bits)))
|
||||
|
||||
self._num_bits = num_bits
|
||||
if not isinstance(name, str) and name is not None:
|
||||
raise TypeError("name must be a string or None, not {}.".format(type(name)))
|
||||
self._name = name
|
||||
|
||||
self._fake_quant = kwargs.pop('fake_quant', True)
|
||||
self._axis = kwargs.pop('axis', None)
|
||||
if self._axis is not None:
|
||||
logging.debug("Meaning of axis has changed since v2.0. Make sure to update.")
|
||||
self._learn_amax = kwargs.pop('learn_amax', False)
|
||||
if self._learn_amax and self._axis is not None:
|
||||
raise TypeError("axis is ignored and must be None when learn_amax is true, got {}.".format(type(
|
||||
self._axis)))
|
||||
amax = kwargs.pop('amax', None)
|
||||
if amax is not None:
|
||||
if not isinstance(amax, float) and not isinstance(amax, list) and not isinstance(amax, np.ndarray):
|
||||
raise TypeError("amax must be float, list or ndarray, not {}".format(type(amax)))
|
||||
# Make it single precision array
|
||||
self._amax = np.array(amax, dtype=np.float32)
|
||||
else:
|
||||
self._amax = amax
|
||||
|
||||
self._scale_amax = kwargs.pop('scale_amax', None)
|
||||
self._calib_method = kwargs.pop('calib_method', "max")
|
||||
self._unsigned = kwargs.pop('unsigned', False)
|
||||
self._narrow_range = kwargs.pop('narrow_range', False)
|
||||
|
||||
if kwargs:
|
||||
raise TypeError("Unused keys: {}".format(kwargs.keys()))
|
||||
|
||||
# pylint:disable=missing-docstring
|
||||
@property
|
||||
def num_bits(self):
|
||||
return self._num_bits
|
||||
|
||||
@property
|
||||
def fake_quant(self):
|
||||
return self._fake_quant
|
||||
|
||||
@property
|
||||
def axis(self):
|
||||
return self._axis
|
||||
|
||||
@property
|
||||
def amax(self):
|
||||
return self._amax
|
||||
|
||||
@property
|
||||
def learn_amax(self):
|
||||
return self._learn_amax
|
||||
|
||||
@property
|
||||
def scale_amax(self):
|
||||
return self._scale_amax
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def calib_method(self):
|
||||
return self._calib_method
|
||||
|
||||
@property
|
||||
def unsigned(self):
|
||||
return self._unsigned
|
||||
|
||||
@property
|
||||
def narrow_range(self):
|
||||
return self._narrow_range
|
||||
|
||||
# pylint:enable=missing-docstring
|
||||
|
||||
def __str__(self):
|
||||
s = (self._name + ': ') if self._name is not None else 'QuantDescriptor'
|
||||
s += "({}{}bit".format("unsigned " if self._unsigned else "", self._num_bits)
|
||||
s += " fake" if self._fake_quant else " real"
|
||||
s += " axis={}".format(self._axis if self._axis is not None else " per-tensor")
|
||||
if isinstance(self._amax, torch.Tensor):
|
||||
s += " amax={}".format(
|
||||
np.array2string(self._amax.cpu().numpy().flatten(), edgeitems=1, formatter={'all': "{:.2e}".format}))
|
||||
elif self._amax is not None:
|
||||
s += " amax={_amax}"
|
||||
s += " full_range"
|
||||
if self._learn_amax:
|
||||
s += " learn_amax"
|
||||
if self._scale_amax:
|
||||
s += " scale_amax={_scale_amax}"
|
||||
s += ")"
|
||||
return s.format(**self.__dict__)
|
||||
|
||||
def __eq__(self, rhs):
|
||||
"""Compare 2 descriptors"""
|
||||
return self.__dict__ == rhs.__dict__
|
||||
|
||||
def dict(self):
|
||||
"""Serialize to dict
|
||||
|
||||
The build-in __dict__ method returns all the attributes, which includes those have default value and have
|
||||
protected prefix "_". This method only returns those have values other than the default one and don't have _ in
|
||||
key. Construct a instance by dict returned by this method should get exactly the same instance.
|
||||
"""
|
||||
obj_dict = {}
|
||||
obj_dict['num_bits'] = self._num_bits
|
||||
obj_dict['name'] = self._name
|
||||
|
||||
if not self._fake_quant:
|
||||
obj_dict['fake_quant'] = self._fake_quant
|
||||
if self._axis is not None:
|
||||
obj_dict['axis'] = self._axis
|
||||
if self._amax is not None:
|
||||
obj_dict['amax'] = self._amax.tolist()
|
||||
if self._scale_amax is not None:
|
||||
obj_dict['scale_amax'] = self._scale_amax
|
||||
if self._learn_amax:
|
||||
obj_dict['learn_amax'] = self._learn_amax
|
||||
if self._unsigned:
|
||||
obj_dict['unsigned'] = self._unsigned
|
||||
|
||||
return obj_dict
|
||||
|
||||
def to_yaml(self):
|
||||
"""Create yaml serialization
|
||||
Some attributes need special treatment to have human readable form, including amax, axis.
|
||||
"""
|
||||
obj_dict = self.dict()
|
||||
|
||||
if "axis" in obj_dict:
|
||||
obj_dict['axis'] = list(obj_dict['axis'])
|
||||
return yaml.dump(obj_dict, width=120)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, yaml_str):
|
||||
"""Create descriptor from yaml str"""
|
||||
obj_dict = yaml.safe_load(yaml_str)
|
||||
|
||||
if 'axis' in obj_dict:
|
||||
obj_dict['axis'] = tuple(obj_dict['axis'])
|
||||
|
||||
quant_desc = cls(**obj_dict)
|
||||
|
||||
return quant_desc
|
||||
|
||||
|
||||
QuantDescriptor = ScaledQuantDescriptor
|
||||
|
||||
# Predefined descriptors
|
||||
QUANT_DESC_8BIT_PER_TENSOR = QuantDescriptor(num_bits=8)
|
||||
QUANT_DESC_UNSIGNED_8BIT_PER_TENSOR = QuantDescriptor(num_bits=8, unsigned=True)
|
||||
QUANT_DESC_8BIT_CONV1D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
|
||||
QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
|
||||
QUANT_DESC_8BIT_CONV3D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
|
||||
QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW = QuantDescriptor(num_bits=8, axis=(0))
|
||||
QUANT_DESC_8BIT_CONVTRANSPOSE1D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
|
||||
QUANT_DESC_8BIT_CONVTRANSPOSE2D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
|
||||
QUANT_DESC_8BIT_CONVTRANSPOSE3D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
|
||||
|
||||
|
||||
@torch.jit.script
|
||||
def _fake_tensor_quant_backward(inputs, amax, grad_outputs):
|
||||
zero = grad_outputs.new_zeros(1)
|
||||
grad_inputs = torch.where(inputs.abs() <= amax, grad_outputs, zero)
|
||||
return grad_inputs
|
||||
|
||||
|
||||
def _onnx_int8_helper(g, inputs, amax, num_bits, unsigned, narrow_range):
|
||||
assert num_bits == 8, "Only INT8 ONNX export is supported for now."
|
||||
maxbound = (1 << (num_bits - 1 + int(unsigned))) - 1
|
||||
|
||||
if amax.numel() == 1:
|
||||
zero_point, axis = torch.tensor(0.0, device=amax.device), None
|
||||
else:
|
||||
amax_init_shape = amax.shape
|
||||
amax = amax.squeeze().data
|
||||
assert len(amax.shape) == 1, "ONNX does not support multi-axis quantization."
|
||||
zero_point = torch.zeros_like(amax, dtype=torch.int32).data
|
||||
axis = list(amax_init_shape).index(list(amax.shape)[0])
|
||||
|
||||
zero_point = g.op("Constant", value_t=zero_point)
|
||||
|
||||
if not unsigned:
|
||||
assert not narrow_range, "ONNX does not support unsigned narrow range INT8."
|
||||
zero_point = g.op("Cast", zero_point, to_i=_C_onnx.TensorProtoDataType.INT8)
|
||||
else:
|
||||
zero_point = g.op("Cast", zero_point, to_i=_C_onnx.TensorProtoDataType.UINT8)
|
||||
|
||||
scale = amax / maxbound
|
||||
scale.masked_fill_(scale == 0, 1.0)
|
||||
scale = g.op("Constant", value_t=scale)
|
||||
|
||||
input_type = inputs.type().scalarType()
|
||||
|
||||
# Q inputs are currently constrained to FP32 due to a similar limitation in ORT
|
||||
# custom ops, so cast the input if needed.
|
||||
if input_type == "Half" or input_type == "BFloat16":
|
||||
inputs = g.op("Cast", inputs, to_i=_C_onnx.TensorProtoDataType.FLOAT)
|
||||
|
||||
quantized = g.op("QuantizeLinear", inputs, scale, zero_point, axis_i=axis)
|
||||
out = g.op("DequantizeLinear", quantized, scale, zero_point, axis_i=axis)
|
||||
|
||||
# DQ outputs are currently constrained to FP32 due to a similar limitation in ORT
|
||||
# custom ops, so cast the output if needed.
|
||||
if input_type == "Half":
|
||||
out = g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.FLOAT16)
|
||||
elif input_type == "BFloat16":
|
||||
out = g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.BFLOAT16)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class FakeTensorQuantFunction(Function):
|
||||
"""Fake version of TensorQuantFunction use CUDA extension"""
|
||||
|
||||
@staticmethod
|
||||
@symbolic_helper.parse_args("v", "t", "i", "b", "b")
|
||||
def symbolic(g, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
|
||||
return _onnx_int8_helper(g, inputs, amax, num_bits, unsigned, narrow_range)
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
|
||||
ctx.save_for_backward(inputs, amax)
|
||||
|
||||
def legacy_quant_func():
|
||||
# The LegacyFakeTensorQuantFunction support cpu and amax with any shape that can be broadcasted to inputs.
|
||||
outputs, scale = _tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
|
||||
return outputs / scale.to(inputs.dtype)
|
||||
|
||||
if not inputs.is_cuda:
|
||||
outputs = legacy_quant_func()
|
||||
else:
|
||||
try:
|
||||
if amax.numel() == 1:
|
||||
outputs = cuda_ext.fake_tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
|
||||
else:
|
||||
axis = amax.shape.index(amax.numel())
|
||||
|
||||
outputs = cuda_ext.fake_tensor_quant_with_axis(inputs, amax.squeeze(), axis, num_bits, unsigned,
|
||||
narrow_range)
|
||||
except (RuntimeError, ValueError) as error:
|
||||
outputs = legacy_quant_func()
|
||||
|
||||
return outputs
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_outputs):
|
||||
inputs, amax = ctx.saved_tensors
|
||||
return _fake_tensor_quant_backward(inputs, amax, grad_outputs), None, None, None, None
|
||||
|
||||
|
||||
def _onnx_fp8_quantize(g, inputs, scale_inv):
|
||||
"""Helper Function for Quantization"""
|
||||
output_shape = torch.onnx.symbolic_helper._get_tensor_sizes(inputs)
|
||||
|
||||
# Q inputs are currently constrained to FP32 due to a similar limitation in ORT
|
||||
# custom ops, so cast the input if needed.
|
||||
if inputs.type().scalarType() == "Half" or inputs.type().scalarType() == "BFloat16":
|
||||
inputs = g.op("Cast", inputs, to_i=_C_onnx.TensorProtoDataType.FLOAT)
|
||||
|
||||
scale = g.op("Constant", value_t=torch.tensor(scale_inv))
|
||||
q_op = g.op("trt::TRT_FP8QuantizeLinear", inputs,
|
||||
scale).setType(inputs.type().with_dtype(torch.uint8).with_sizes(output_shape))
|
||||
return q_op
|
||||
|
||||
|
||||
def _onnx_fp8_dequantize(g, inputs, scale_inv, otype=None):
|
||||
"""Helper Function for Dequantization"""
|
||||
output_shape = torch.onnx.symbolic_helper._get_tensor_sizes(inputs)
|
||||
|
||||
scale = g.op("Constant", value_t=torch.tensor(scale_inv))
|
||||
out = g.op("trt::TRT_FP8DequantizeLinear", inputs,
|
||||
scale).setType(inputs.type().with_dtype(torch.float32).with_sizes(output_shape))
|
||||
|
||||
# DQ outputs are currently constrained to FP32 due to a similar limitation in ORT
|
||||
# custom ops, so cast the output if needed.
|
||||
if otype == "Half":
|
||||
out = g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.FLOAT16)
|
||||
elif otype == "BFloat16":
|
||||
out = g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.BFLOAT16)
|
||||
return out
|
||||
|
||||
|
||||
class ScaledE4M3Function(Function):
|
||||
"""E4M3fy input with scale"""
|
||||
|
||||
@staticmethod
|
||||
@symbolic_helper.parse_args("v", "t", "i", "b", "b")
|
||||
def symbolic(g, inputs, amax=None, E=4, M=3):
|
||||
if amax is None:
|
||||
scale = 1.0
|
||||
else:
|
||||
scale = 448.0 / amax
|
||||
scale = float(scale.masked_fill_(scale == 0, 1.0))
|
||||
|
||||
input_type = inputs.type().scalarType()
|
||||
q_tensor = _onnx_fp8_quantize(g, inputs, 1.0 / scale)
|
||||
return _onnx_fp8_dequantize(g, q_tensor, 1.0 / scale, input_type)
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, inputs, amax=None, E=4, M=3):
|
||||
if E != 4 or M != 3:
|
||||
raise NotImplementedError("Only support E=4 & M=3 for now.")
|
||||
|
||||
ctx.save_for_backward(inputs)
|
||||
ctx.amax = amax
|
||||
zero_mask = (inputs.abs() < 1. / (1 << 24))
|
||||
|
||||
if amax is None:
|
||||
outputs = cuda_ext.fake_e4m3fy(inputs)
|
||||
else:
|
||||
# FP8 ONNX export requires scalar `scale`.
|
||||
# To simplify implementation, amax is enforced to be a scalar.
|
||||
scale = 448.0 / amax
|
||||
outputs = cuda_ext.fake_e4m3fy(inputs * scale) / scale
|
||||
|
||||
# Zero out values that are tiny.
|
||||
# Tiny values could lead to tiny amax and then large scale which cause overflow/saturation
|
||||
# and won't go back to normal value after dividing by scale. The right behavior is to mark them
|
||||
# as zero which also get rid of inf/nan
|
||||
outputs[zero_mask] = 0.
|
||||
|
||||
return outputs
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_outputs):
|
||||
inputs, = ctx.saved_tensors
|
||||
amax = torch.tensor(ctx.amax if ctx.amax is not None else 448.0, dtype=torch.float32, device=inputs.device)
|
||||
grad_inputs = _fake_tensor_quant_backward(inputs, amax, grad_outputs)
|
||||
return grad_inputs, None, None, None
|
||||
|
||||
|
||||
class TensorQuantFunction(Function):
|
||||
"""A universal tensor quantization function
|
||||
|
||||
Take an input tensor, output an quantized tensor. The granularity of scale can be interpreted from the
|
||||
shape of amax.
|
||||
output_dtype indicates whether the quantized value will be stored in integer or float. The reason we want to store
|
||||
it in float is the pytorch function takes the quantized value may not accept integer input, e.g. Conv2D.
|
||||
|
||||
It uses 2^num_bits -1 values instead of 2^num_bits. e.g., for num_bits=8, it uses [-127, 127] instead of [-128, 127]
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@symbolic_helper.parse_args("v", "t", "i", "b", "b")
|
||||
def symbolic(g, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
|
||||
return _onnx_int8_helper(g, inputs, amax, num_bits, unsigned, narrow_range)
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
|
||||
"""
|
||||
|
||||
Follow tensorflow convention, max value is passed in and used to decide scale, instead of inputing scale
|
||||
directly. Though inputing scale directly may be more natural to use.
|
||||
|
||||
Args:
|
||||
ctx: A Context object to store tensors for backward.
|
||||
inputs: A Tensor of type float32.
|
||||
amax: A Tensor of type float32. Inputs will be quantized within range [-amax, amax]
|
||||
amax will be broadcasted to inputs tensor.
|
||||
num_bits: A integer used to calculate scaling factor, scale = (2^(num_bits-1) - 1) / max
|
||||
Effectively, it indicates how many integer bits is used to represent the value. Default 8.
|
||||
output_dtype: A type of Tensor. torch.int32 or torch.float32.
|
||||
unsigned: A boolean. Use unsigned integer range. E.g. [0, 255] for num_bits=8. Default False.
|
||||
narrow_range: A boolean. Use symmetric integer range for signed quantization
|
||||
E.g. [-127,127] instead of [-128,127] for num_bits=8. Default True.
|
||||
|
||||
Returns:
|
||||
outputs: A Tensor of type output_dtype.
|
||||
scale: A Tensor of type float32. outputs / scale will dequantize outputs tensor.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
"""
|
||||
ctx.save_for_backward(inputs, amax)
|
||||
outputs, scale = _tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
|
||||
# Check if scale overflows FP16
|
||||
if outputs.dtype == torch.half and scale.max() > 65504:
|
||||
raise ValueError("scale is too large for FP16 with amax={}".format(amax))
|
||||
return outputs, scale.to(inputs.dtype)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_outputs, grad_scale):
|
||||
"""
|
||||
Implements straight through estimation with clipping. For -amax <= input <= amax
|
||||
the gradient passes straight through, otherwise the gradient is zero.
|
||||
|
||||
Args:
|
||||
ctx: A Context object with saved tensors from forward.
|
||||
grad_outputs: A tensor of gradient of outputs.
|
||||
grad_scale: A tensor of gradient of scale.
|
||||
|
||||
Returns:
|
||||
grad_inputs: A tensor of gradient.
|
||||
"""
|
||||
inputs, amax = ctx.saved_tensors
|
||||
zero = grad_outputs.new_zeros(1) # create a zero tensor with the same type and device
|
||||
grad_inputs = torch.where(inputs.abs() <= amax, grad_outputs, zero)
|
||||
return grad_inputs, None, None, None, None
|
||||
|
||||
|
||||
class LegacyFakeTensorQuantFunction(Function):
|
||||
"""Fake version of TensorQuantFunction
|
||||
See comments of TensorQuantFunction, arguments are the same.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
|
||||
ctx.save_for_backward(inputs, amax)
|
||||
outputs, scale = _tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
|
||||
return outputs / scale.to(inputs.dtype)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_outputs):
|
||||
inputs, amax = ctx.saved_tensors
|
||||
zero = grad_outputs.new_zeros(1)
|
||||
grad_inputs = torch.where(inputs.abs() <= amax, grad_outputs, zero)
|
||||
return grad_inputs, None, None, None, None
|
||||
|
||||
|
||||
def _tensor_quant(inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
|
||||
"""Shared function body between TensorQuantFunction and FakeTensorQuantFunction"""
|
||||
# Fine scale, per channel scale will be handled by broadcasting, which could be tricky. Pop a warning.
|
||||
if isinstance(amax, torch.Tensor) and inputs.dim() != amax.dim():
|
||||
logging.debug("amax %s has different shape than inputs %s. Make sure broadcast works as expected!", amax.size(),
|
||||
inputs.size())
|
||||
|
||||
logging.debug("{} bits quantization on shape {} tensor.".format(num_bits, inputs.size()))
|
||||
|
||||
if unsigned:
|
||||
if inputs.min() < 0.:
|
||||
raise TypeError("Negative values encountered in unsigned quantization.")
|
||||
|
||||
# Computation must be in FP32 to prevent potential over flow.
|
||||
input_dtype = inputs.dtype
|
||||
if inputs.dtype == torch.half:
|
||||
inputs = inputs.float()
|
||||
if amax.dtype == torch.half:
|
||||
amax = amax.float()
|
||||
|
||||
min_amax = amax.min()
|
||||
if min_amax < 0:
|
||||
raise ValueError("Negative values in amax")
|
||||
|
||||
max_bound = torch.tensor((2.0**(num_bits - 1 + int(unsigned))) - 1.0, device=amax.device)
|
||||
if unsigned:
|
||||
min_bound = 0
|
||||
elif narrow_range:
|
||||
min_bound = -max_bound
|
||||
else:
|
||||
min_bound = -max_bound - 1
|
||||
scale = max_bound / amax
|
||||
|
||||
epsilon = 1. / (1 << 24)
|
||||
if min_amax <= epsilon: # Treat amax smaller than minimum representable of fp16 0
|
||||
zero_amax_mask = (amax <= epsilon)
|
||||
scale[zero_amax_mask] = 0 # Value quantized with amax=0 should all be 0
|
||||
|
||||
outputs = torch.clamp((inputs * scale).round_(), min_bound, max_bound)
|
||||
|
||||
if min_amax <= epsilon:
|
||||
scale[zero_amax_mask] = 1. # Return 1 makes more sense for values quantized to 0 with amax=0
|
||||
|
||||
if input_dtype == torch.half:
|
||||
outputs = outputs.half()
|
||||
|
||||
return outputs, scale
|
||||
|
||||
|
||||
class FakeAffineTensorQuantFunction(Function):
|
||||
"""Fake version of affine quantization
|
||||
|
||||
gemmlowp style scale+shift quantization. See more details in
|
||||
https://github.com/google/gemmlowp/blob/master/doc/quantization.md.
|
||||
|
||||
We DO NOT recommend affine quantization on weights for performance reason. There might be value to affine quantize
|
||||
activation as it can be cancelled by bias and comes with no performance penalty. This functionality is only added
|
||||
for experimental purpose.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, inputs, min_range, max_range, num_bits=8):
|
||||
"""
|
||||
|
||||
As it will be only applied on activation with per tensor granularity, broadcast is not needed.
|
||||
|
||||
Args:
|
||||
ctx: Pytorch convention.
|
||||
inputs: A Tensor of type float32.
|
||||
min_range: A float.
|
||||
max_range: A float.
|
||||
num_bits: An integer
|
||||
|
||||
Returns:
|
||||
outputs: A Tensor of type output_dtype
|
||||
"""
|
||||
logging.debug("{} bits quantization on shape {} tensor.".format(num_bits, inputs.size()))
|
||||
ctx.save_for_backward(inputs, min_range, max_range)
|
||||
|
||||
step_size = (max_range - min_range) / (2.0**num_bits - 1)
|
||||
|
||||
min_bound = -2.0**(num_bits - 1)
|
||||
max_bound = 2.0**(num_bits - 1) - 1
|
||||
|
||||
quant_zero = torch.round(min_range / step_size) - min_bound
|
||||
quantized = torch.round(inputs / step_size) - quant_zero
|
||||
quantized = torch.clamp(quantized, min_bound, max_bound)
|
||||
|
||||
outputs = (quantized + quant_zero) * step_size
|
||||
|
||||
return outputs
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_outputs):
|
||||
"""
|
||||
Args:
|
||||
ctx: Pytorch convention.
|
||||
grad_output: A tensor of gradient of outputs
|
||||
|
||||
Returns:
|
||||
grad_inputs: A tensor of gradient
|
||||
"""
|
||||
|
||||
inputs, min_range, max_range = ctx.saved_tensors
|
||||
zero = grad_outputs.new_zeros(1)
|
||||
grad_inputs = torch.where((inputs <= max_range) * (inputs >= min_range), grad_outputs, zero)
|
||||
return grad_inputs, None, None, None
|
||||
|
||||
|
||||
tensor_quant = TensorQuantFunction.apply
|
||||
legacy_fake_tensor_quant = LegacyFakeTensorQuantFunction.apply
|
||||
fake_tensor_quant = FakeTensorQuantFunction.apply
|
||||
fake_affine_tensor_quant = FakeAffineTensorQuantFunction.apply
|
||||
scaled_e4m3 = ScaledE4M3Function.apply
|
||||
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Main entry of all utils"""
|
||||
|
||||
from .reduce_amax import reduce_amax
|
||||
@@ -0,0 +1,27 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""A WAR for codes that messes up logging format"""
|
||||
|
||||
import logging
|
||||
|
||||
def reset_logger_handler():
|
||||
"""Remove all handler in root logger"""
|
||||
root_logger = logging.getLogger()
|
||||
while root_logger.handlers:
|
||||
root_logger.removeHandler(root_logger.handlers[0])
|
||||
@@ -0,0 +1,63 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Function to get absolute maximum of a tensor
|
||||
Follow numpy fashion, which is more generic as pytorch's
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
def reduce_amax(input, axis=None, keepdims=True):
|
||||
"""Compute the absolute maximum value of a tensor.
|
||||
|
||||
Reduces input_tensor along the dimensions given in axis. Unless keepdims is true,
|
||||
the rank of the tensor is reduced by 1 for each entry in axis. If keepdims is true,
|
||||
the reduced dimensions are retained with length 1.
|
||||
|
||||
.. note::
|
||||
Gradient computeation is disabled as this function is never meant learning reduces amax
|
||||
|
||||
Args:
|
||||
input: Input tensor
|
||||
axis: The dimensions to reduce. None or int or tuple of ints. If None (the default),
|
||||
reduces all dimensions. Must be in the range [-rank(input_tensor), rank(input_tensor)).
|
||||
keepdims: A boolean. If true, retains reduced dimensions with length 1. Default True
|
||||
granularity: DEPRECTED. specifies if the statistic has to be calculated at tensor or channel granularity
|
||||
|
||||
Returns:
|
||||
The reduced tensor.
|
||||
|
||||
Raises:
|
||||
ValueError: Any axis which doesn't make sense or is not supported
|
||||
ValueError: If unknown granularity is passed in.
|
||||
"""
|
||||
with torch.no_grad():
|
||||
output = input.abs()
|
||||
if axis is None:
|
||||
output = torch.max(output)
|
||||
else:
|
||||
if isinstance(axis, int):
|
||||
output, _ = torch.max(output, dim=axis, keepdim=keepdims)
|
||||
else:
|
||||
if isinstance(axis, tuple) and len(axis) > input.dim():
|
||||
raise ValueError("Cannot reduce more axes than tensor's dim.")
|
||||
for i in axis:
|
||||
output, _ = torch.max(output, dim=i, keepdim=True)
|
||||
if not keepdims or output.numel() == 1:
|
||||
output.squeeze_()
|
||||
return output
|
||||
@@ -0,0 +1,6 @@
|
||||
numpy
|
||||
absl-py>=0.7.0
|
||||
scipy
|
||||
sphinx_glpi_theme
|
||||
prettytable
|
||||
pyyaml
|
||||
@@ -0,0 +1,6 @@
|
||||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --verbose
|
||||
python_files = tests/*.py
|
||||
@@ -0,0 +1,80 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Simple setup script"""
|
||||
|
||||
import os
|
||||
from setuptools import setup, find_packages
|
||||
from torch.utils.cpp_extension import BuildExtension, CppExtension, CUDAExtension
|
||||
|
||||
abspath = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
with open(os.path.join(abspath, "requirements.txt")) as f:
|
||||
requirements = f.read().splitlines()
|
||||
|
||||
license_header = """#
|
||||
# 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.
|
||||
#
|
||||
"""
|
||||
|
||||
# Generate version file
|
||||
with open(os.path.join(abspath, "VERSION")) as f:
|
||||
version = f.read().strip()
|
||||
with open(os.path.join(abspath, "pytorch_quantization/version.py"), "w") as f:
|
||||
f.write(license_header)
|
||||
f.write(F"__version__ = \"{version}\"")
|
||||
|
||||
setup(
|
||||
name="pytorch_quantization",
|
||||
version=version,
|
||||
description="NVIDIA Pytorch quantization toolkit",
|
||||
packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
|
||||
setup_requires=["pytest-runner"],
|
||||
tests_require=["pytest"],
|
||||
|
||||
install_requires=requirements,
|
||||
ext_modules=[
|
||||
CUDAExtension(
|
||||
name="pytorch_quantization.cuda_ext",
|
||||
sources=[os.path.join(abspath, "src/tensor_quant.cpp"),
|
||||
os.path.join(abspath, "src/tensor_quant_gpu.cu")])
|
||||
],
|
||||
cmdclass={
|
||||
"build_ext": BuildExtension
|
||||
},
|
||||
zip_safe=False,
|
||||
|
||||
long_description=open("README.md", "r", encoding="utf-8").read(),
|
||||
url="https://github.com/nvidia/tensorrt/tools/pytorch-quantization",
|
||||
author="NVIDIA",
|
||||
author_email="nvidia@nvidia.com",
|
||||
license="Apache 2.0",
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
#include <cuda.h>
|
||||
#include <torch/extension.h>
|
||||
|
||||
void fake_tensor_quant_cuda_inplace(at::Tensor, at::Tensor, int, bool, bool);
|
||||
at::Tensor fake_tensor_quant_cuda(at::Tensor, at::Tensor, int, bool, bool);
|
||||
at::Tensor fake_tensor_quant_with_axis_cuda(at::Tensor, at::Tensor, int, int, bool, bool);
|
||||
float bits_to_bound(int, int);
|
||||
at::Tensor fake_e4m3fy_cuda(at::Tensor inputs);
|
||||
|
||||
void fake_tensor_quant_(at::Tensor inputs, at::Tensor amax, int num_bits = 8,
|
||||
bool is_unsigned = false, bool narrow_range = true) {
|
||||
TORCH_CHECK(inputs.is_cuda());
|
||||
TORCH_CHECK(inputs.is_contiguous()) // in-place on non-contiguous tensor is more difficult
|
||||
TORCH_CHECK(amax.numel(), 1);
|
||||
fake_tensor_quant_cuda_inplace(inputs, amax, num_bits, is_unsigned, narrow_range);
|
||||
}
|
||||
|
||||
at::Tensor fake_tensor_quant(at::Tensor inputs, at::Tensor amax, int num_bits = 8,
|
||||
bool is_unsigned = false, bool narrow_range = true) {
|
||||
TORCH_CHECK(inputs.is_cuda());
|
||||
TORCH_CHECK(amax.numel(), 1);
|
||||
return fake_tensor_quant_cuda(inputs.contiguous(), amax.contiguous(), num_bits, is_unsigned, narrow_range);
|
||||
}
|
||||
|
||||
at::Tensor fake_tensor_quant_with_axis(at::Tensor inputs, at::Tensor amax, int axis,
|
||||
int num_bits = 8, bool is_unsigned = false,
|
||||
bool narrow_range = true) {
|
||||
TORCH_CHECK(inputs.is_cuda());
|
||||
TORCH_CHECK(amax.numel(), inputs.size(axis));
|
||||
return fake_tensor_quant_with_axis_cuda(
|
||||
inputs.contiguous(), amax.contiguous(), axis, num_bits, is_unsigned, narrow_range);
|
||||
}
|
||||
|
||||
|
||||
#if CUDA_VERSION > 11070
|
||||
|
||||
#include <cuda_fp8.h>
|
||||
#include <ATen/ATen.h>
|
||||
|
||||
at::Tensor fake_e4m3fy_cuda(at::Tensor inputs);
|
||||
|
||||
at::Tensor fake_e4m3fy(at::Tensor inputs) {
|
||||
if (inputs.is_cuda()) {
|
||||
return fake_e4m3fy_cuda(inputs.contiguous());
|
||||
} else {
|
||||
TORCH_CHECK(inputs.dtype() == at::ScalarType::Float);
|
||||
TORCH_CHECK(inputs.is_contiguous());
|
||||
auto out = at::zeros_like(inputs);
|
||||
for (int i = 0; i < inputs.numel(); ++i) {
|
||||
out.data_ptr<float>()[i] = static_cast<float>(static_cast<__nv_fp8_e4m3>(inputs.data_ptr<float>()[i]));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
at::Tensor fake_e4m3fy(at::Tensor inputs) {
|
||||
throw std::runtime_error("FP8 emulation is not supported on CUDA 11.7 and below");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
m.def("fake_tensor_quant_", &fake_tensor_quant_, "Fake Tensor Quant Inplace", py::arg("inputs"),
|
||||
py::arg("amax"), py::arg("num_bits") = 8, py::arg("unsigned") = false,
|
||||
py::arg("narrow_range") = true);
|
||||
m.def("fake_tensor_quant", &fake_tensor_quant, "Fake Tensor Quant", py::arg("inputs"),
|
||||
py::arg("amax"), py::arg("num_bits") = 8, py::arg("unsigned") = false,
|
||||
py::arg("narrow_range") = true);
|
||||
m.def("fake_tensor_quant_with_axis", &fake_tensor_quant_with_axis, "Fake Tensor Quant with axis",
|
||||
py::arg("inputs"), py::arg("amax"), py::arg("axis"), py::arg("num_bits") = 8,
|
||||
py::arg("unsigned") = false, py::arg("narrow_range") = true);
|
||||
|
||||
m.def("fake_e4m3fy", &fake_e4m3fy, "Reduce precision to E4M3",
|
||||
py::arg("inputs"));
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <ATen/ATen.h>
|
||||
#include <cuda.h>
|
||||
#include <cuda_fp16.h>
|
||||
#include <cuda_runtime.h>
|
||||
#include <math.h>
|
||||
#include <torch/extension.h>
|
||||
|
||||
#define BLOCK_SIZE 128
|
||||
#define EPSILON (1. / (1<<24)) // Minimum representable of fp16
|
||||
|
||||
#define AT_DISPATCH_CASE_FLOATING_TYPES(...) \
|
||||
AT_DISPATCH_CASE(at::ScalarType::Double, __VA_ARGS__) \
|
||||
AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \
|
||||
AT_DISPATCH_CASE(at::ScalarType::Half, __VA_ARGS__) \
|
||||
AT_DISPATCH_CASE(at::ScalarType::BFloat16, __VA_ARGS__)
|
||||
|
||||
#define AT_DISPATCH_FLOATING_TYPES(TYPE, NAME, ...) \
|
||||
AT_DISPATCH_SWITCH( \
|
||||
TYPE, NAME, AT_DISPATCH_CASE_FLOATING_TYPES(__VA_ARGS__))
|
||||
|
||||
__host__ __device__ float bits_to_bound(int num_bits, int is_unsigned) {
|
||||
float bound = (1 << (num_bits - 1 + int(is_unsigned))) - 1;
|
||||
return bound;
|
||||
}
|
||||
|
||||
__device__ float fake_tensor_quant_device(float input, float amax, int min_bound, int max_bound) {
|
||||
CUDA_KERNEL_ASSERT(amax >= 0);
|
||||
|
||||
if (amax < EPSILON) {
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
float scale = max_bound / amax;
|
||||
float output = rint(input * scale);
|
||||
output = output > max_bound ? max_bound : output;
|
||||
output = output < min_bound ? min_bound : output;
|
||||
|
||||
return output / scale;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
__global__ void fake_tensor_quant_kernel(const T* inputs, size_t n, T* outputs, const float* amax,
|
||||
int num_bits = 8, bool is_unsigned = false,
|
||||
bool narrow_range = true) {
|
||||
int tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
|
||||
if (tid < n) {
|
||||
if (is_unsigned) {
|
||||
CUDA_KERNEL_ASSERT(inputs[tid] >= 0);
|
||||
}
|
||||
float bound = bits_to_bound(num_bits, is_unsigned);
|
||||
float max_bound = bound;
|
||||
float min_bound = -(bound + !narrow_range);
|
||||
outputs[tid] = fake_tensor_quant_device((float)inputs[tid], amax[0], min_bound, max_bound);
|
||||
}
|
||||
}
|
||||
|
||||
void fake_tensor_quant_cuda_inplace(at::Tensor inputs, at::Tensor amax, int num_bits = 8,
|
||||
bool is_unsigned = false, bool narrow_range = true) {
|
||||
size_t numel = inputs.numel();
|
||||
AT_DISPATCH_FLOATING_TYPES(
|
||||
inputs.type().scalarType(), "fake_tensor_quant_cuda_inplace", [&] {
|
||||
fake_tensor_quant_kernel<<<numel / BLOCK_SIZE + 1, BLOCK_SIZE>>>(
|
||||
inputs.data_ptr<scalar_t>(), numel, inputs.data_ptr<scalar_t>(),
|
||||
amax.to(at::ScalarType::Float).data_ptr<float>(), num_bits, is_unsigned);
|
||||
});
|
||||
}
|
||||
|
||||
at::Tensor fake_tensor_quant_cuda(at::Tensor inputs, at::Tensor amax, int num_bits = 8,
|
||||
bool is_unsigned = false, bool narrow_range = true) {
|
||||
size_t numel = inputs.numel();
|
||||
auto outputs = torch::empty_like(inputs);
|
||||
AT_DISPATCH_FLOATING_TYPES(inputs.type().scalarType(), "fake_tensor_quant_cuda", [&] {
|
||||
fake_tensor_quant_kernel<<<numel / BLOCK_SIZE + 1, BLOCK_SIZE>>>(
|
||||
inputs.data_ptr<scalar_t>(), numel, outputs.data_ptr<scalar_t>(),
|
||||
amax.to(at::ScalarType::Float).data_ptr<float>(), num_bits, is_unsigned);
|
||||
});
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
__global__ void fake_tensor_quant_with_axis_cuda_kernel(const T* inputs, size_t n,
|
||||
T* outputs, const float* amax,
|
||||
int axis_size, int outer_size,
|
||||
int num_bits = 8, bool is_unsigned = false,
|
||||
bool narrow_range = true) {
|
||||
int tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
|
||||
float bound = bits_to_bound(num_bits, is_unsigned);
|
||||
float max_bound = bound;
|
||||
float min_bound = -(bound + !narrow_range);
|
||||
|
||||
for (int idx = 4 * tid; idx < 4 * (tid + 1) && idx < n; ++idx) {
|
||||
if (is_unsigned) {
|
||||
CUDA_KERNEL_ASSERT(inputs[idx] >= 0);
|
||||
}
|
||||
int axis_idx = (idx / outer_size) % axis_size;
|
||||
|
||||
outputs[idx] = fake_tensor_quant_device((float)inputs[idx], amax[axis_idx], min_bound, max_bound);
|
||||
}
|
||||
}
|
||||
|
||||
at::Tensor fake_tensor_quant_with_axis_cuda(at::Tensor inputs, at::Tensor amax, int axis,
|
||||
int num_bits = 8, bool is_unsigned = false,
|
||||
bool narrow_range = true) {
|
||||
auto outputs = torch::empty_like(inputs);
|
||||
size_t numel = inputs.numel();
|
||||
int axis_size = inputs.size(axis);
|
||||
|
||||
int outer_size = inputs.stride(axis);
|
||||
|
||||
AT_DISPATCH_FLOATING_TYPES(inputs.type().scalarType(), "fake_tensor_quant_cuda_with_axis", [&] {
|
||||
fake_tensor_quant_with_axis_cuda_kernel<<<numel / (BLOCK_SIZE * 4) + 1, BLOCK_SIZE>>>(
|
||||
inputs.data_ptr<scalar_t>(), numel, outputs.data_ptr<scalar_t>(),
|
||||
amax.to(at::ScalarType::Float).data_ptr<float>(), axis_size, outer_size, num_bits, is_unsigned);
|
||||
});
|
||||
return outputs;
|
||||
}
|
||||
|
||||
#if CUDA_VERSION > 11070
|
||||
#include <cuda_fp8.h>
|
||||
|
||||
template <typename T>
|
||||
__global__ void fake_e4m3fy_kernel(const T* inputs, size_t n, T* outputs) {
|
||||
int tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
|
||||
for (int idx = 4 * tid; idx < 4 * (tid + 1) && idx < n; ++idx) {
|
||||
outputs[idx] = static_cast<T>(static_cast<float>(static_cast<__nv_fp8_e4m3>(static_cast<float>(inputs[idx]))));
|
||||
}
|
||||
}
|
||||
|
||||
at::Tensor fake_e4m3fy_cuda(at::Tensor inputs) {
|
||||
size_t numel = inputs.numel();
|
||||
auto outputs = torch::empty_like(inputs);
|
||||
AT_DISPATCH_FLOATING_TYPES(inputs.type().scalarType(), "fake_e4m3fy_cuda", [&] {
|
||||
fake_e4m3fy_kernel<<<numel / (BLOCK_SIZE * 4) + 1, BLOCK_SIZE>>>(
|
||||
inputs.data_ptr<scalar_t>(), numel, outputs.data_ptr<scalar_t>());
|
||||
});
|
||||
return outputs;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,452 @@
|
||||
#
|
||||
# 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 calibrators"""
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
from pytorch_quantization import calib
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
import tests.utils as test_utils
|
||||
from tests.fixtures import verbose
|
||||
from tests.fixtures.models import QuantLeNet
|
||||
|
||||
np.random.seed(12345)
|
||||
torch.manual_seed(12345)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
class TestMaxCalibrator():
|
||||
|
||||
def test_simple_run(self):
|
||||
max_calibrator = calib.MaxCalibrator(8, None, False)
|
||||
|
||||
x_1 = torch.rand(129).cuda()
|
||||
x_2 = torch.rand(127).cuda()
|
||||
max_calibrator.collect(x_1)
|
||||
max_calibrator.collect(x_2)
|
||||
|
||||
test_utils.compare(max_calibrator.compute_amax(), torch.max(x_1.max(), x_2.max()), atol=0, rtol=0, ctol=0)
|
||||
|
||||
# Nothing to test other than creation
|
||||
max_calibrator = calib.MaxCalibrator(8, None, True)
|
||||
|
||||
def test_fine_grain(self):
|
||||
axis = 0
|
||||
reducs_axis = (1, 2, 3)
|
||||
max_calibrator = calib.MaxCalibrator(8, axis, False)
|
||||
|
||||
x_1 = torch.rand(31, 63, 7, 7).cuda()
|
||||
x_2 = torch.rand(31, 63, 7, 7).cuda()
|
||||
max_calibrator.collect(x_1)
|
||||
max_calibrator.collect(x_2)
|
||||
|
||||
assert max_calibrator.compute_amax().shape[0] == 31
|
||||
|
||||
test_utils.compare(max_calibrator.compute_amax(),
|
||||
quant_utils.reduce_amax(torch.max(x_1, x_2), axis=reducs_axis),
|
||||
atol=0, rtol=0, ctol=0)
|
||||
|
||||
max_calibrator.reset()
|
||||
assert max_calibrator.compute_amax() is None
|
||||
|
||||
def test_reverse_axis(self):
|
||||
axis = -4
|
||||
reducs_axis = (1, 2, 3)
|
||||
max_calibrator = calib.MaxCalibrator(8, axis, False)
|
||||
|
||||
x_1 = torch.rand(31, 63, 7, 7).cuda()
|
||||
x_2 = torch.rand(31, 63, 7, 7).cuda()
|
||||
max_calibrator.collect(x_1)
|
||||
max_calibrator.collect(x_2)
|
||||
|
||||
assert max_calibrator.compute_amax().shape[0] == 31
|
||||
|
||||
test_utils.compare(max_calibrator.compute_amax(),
|
||||
quant_utils.reduce_amax(torch.max(x_1, x_2), axis=reducs_axis),
|
||||
atol=0, rtol=0, ctol=0)
|
||||
|
||||
max_calibrator.reset()
|
||||
assert max_calibrator.compute_amax() is None
|
||||
|
||||
def test_raises(self):
|
||||
axis = 0
|
||||
max_calibrator = calib.MaxCalibrator(8, axis, False)
|
||||
|
||||
x_2 = torch.rand(32, 63, 7, 7).cuda()
|
||||
x_3 = torch.rand(33, 63, 7, 7).cuda()
|
||||
max_calibrator.collect(x_2)
|
||||
with pytest.raises(RuntimeError, match="shape changed"):
|
||||
max_calibrator.collect(x_3)
|
||||
|
||||
def test_track_amax(self):
|
||||
max_calibrator = calib.MaxCalibrator(8, None, False, track_amax=True)
|
||||
|
||||
x_1 = torch.rand(129).cuda()
|
||||
x_2 = torch.rand(127).cuda()
|
||||
max_calibrator.collect(x_1)
|
||||
max_calibrator.collect(x_2)
|
||||
|
||||
test_utils.compare(max_calibrator.compute_amax(), torch.max(x_1.max(), x_2.max()), atol=0, rtol=0, ctol=0)
|
||||
np.testing.assert_array_equal(max_calibrator.amaxs[0], x_1.max().cpu().numpy())
|
||||
np.testing.assert_array_equal(max_calibrator.amaxs[1], x_2.max().cpu().numpy())
|
||||
|
||||
def test_repr(self):
|
||||
max_calibrator = calib.MaxCalibrator(8, None, False, track_amax=True)
|
||||
repr(max_calibrator)
|
||||
|
||||
class TestHistogramCalibrator():
|
||||
|
||||
def test_grow(self, verbose):
|
||||
x_1 = torch.tensor([0, 255, 255, 255, 255, 255]).cuda()
|
||||
x_2 = torch.tensor([0, 255, 255, 255, 255, 256]).cuda()
|
||||
|
||||
hist_calibrator = calib.HistogramCalibrator(8, None, False, grow_method='stretch')
|
||||
hist_calibrator.collect(x_1)
|
||||
hist_calibrator.collect(x_2)
|
||||
|
||||
amax = hist_calibrator.compute_amax(method='entropy')
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be closer to 256 because the last bin gets stretched to (~255, 257)
|
||||
assert (amax - 255.).abs() < (amax - 256.).abs()
|
||||
|
||||
hist_calibrator = calib.HistogramCalibrator(8, None, False, grow_method='append')
|
||||
hist_calibrator.collect(x_1)
|
||||
hist_calibrator.collect(x_2)
|
||||
|
||||
amax = hist_calibrator.compute_amax(method='mse')
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be closer to 255
|
||||
assert (amax - 255.).abs() < 0.5
|
||||
|
||||
def test_skip_zeros(self, verbose):
|
||||
x_1 = torch.tensor([0, 0, 0, 0, 0, 1, 2, 3, 4, 5])
|
||||
x_2 = torch.tensor([0, 0, 0, 0, 0, 6, 7, 8, 9, 10])
|
||||
|
||||
calibrator = calib.HistogramCalibrator(8, None, False, skip_zeros=True)
|
||||
calibrator.collect(x_1)
|
||||
calibrator.collect(x_2)
|
||||
|
||||
amax = calibrator.compute_amax("percentile", percentile=50)
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be close to 5
|
||||
assert (amax - 5.).abs() < 10/2048
|
||||
|
||||
def test_torch_hist(self):
|
||||
x_1 = torch.rand(1023, device="cuda")
|
||||
x_1[0] = 0
|
||||
x_2 = torch.rand(1023, device="cuda") + 1 # Make sure histogram bins need to be grown
|
||||
x_2[1] = 0
|
||||
|
||||
calibrator_np = calib.HistogramCalibrator(8, None, False, num_bins=19, torch_hist=False)
|
||||
calibrator_torch = calib.HistogramCalibrator(8, None, False, num_bins=19, torch_hist=True)
|
||||
|
||||
calibrator_np.collect(x_1)
|
||||
calibrator_torch.collect(x_1)
|
||||
assert calibrator_torch._calib_hist.numel() == calibrator_torch._calib_bin_edges.numel() - 1
|
||||
np.testing.assert_array_equal(calibrator_np._calib_hist, calibrator_torch._calib_hist.cpu().numpy())
|
||||
np.testing.assert_array_almost_equal(
|
||||
calibrator_np._calib_bin_edges, calibrator_torch._calib_bin_edges.cpu().numpy())
|
||||
|
||||
# Test multiple collections with some of them needs to expand range
|
||||
for _ in range(3):
|
||||
calibrator_np.collect(x_2)
|
||||
calibrator_torch.collect(x_2)
|
||||
calibrator_np.collect(x_1)
|
||||
calibrator_torch.collect(x_1)
|
||||
|
||||
# Test compute_amax function doesn't convert _calib_hist and _calib_bin_edges unnecessarily
|
||||
calibrator_np.compute_amax("percentile", percentile=99.99)
|
||||
calibrator_torch.compute_amax("percentile", percentile=99.99)
|
||||
|
||||
np.testing.assert_array_equal(calibrator_np._calib_hist, calibrator_torch._calib_hist.cpu().numpy())
|
||||
np.testing.assert_array_almost_equal(
|
||||
calibrator_np._calib_bin_edges, calibrator_torch._calib_bin_edges.cpu().numpy())
|
||||
assert calibrator_torch._calib_hist.numel() == calibrator_torch._calib_bin_edges.numel() - 1
|
||||
|
||||
|
||||
class TestEntropyCalibrator():
|
||||
|
||||
def test_one_tensor(self, verbose):
|
||||
hist_calibrator = calib.HistogramCalibrator(8, None, False, grow_method='stretch')
|
||||
|
||||
x_2 = torch.rand(11, 7, 3, 3).cuda() # uniform in (0,1)
|
||||
x_2[1, 1, 1, 1] = 10. # create outlier
|
||||
hist_calibrator.collect(x_2)
|
||||
|
||||
# Don't have a better test metric. One outlier 10 should be discared by KL-divergence
|
||||
amax = hist_calibrator.compute_amax("entropy")
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
assert amax < 1.1
|
||||
|
||||
def test_unsigned(self, verbose):
|
||||
hist_calibrator = calib.HistogramCalibrator(8, None, True, grow_method='stretch')
|
||||
|
||||
x_2 = torch.rand(11, 7, 3, 3).cuda() # uniform in (0,1)
|
||||
x_2[1, 1, 1, 1] = 10. # create outlier
|
||||
hist_calibrator.collect(x_2)
|
||||
|
||||
amax = hist_calibrator.compute_amax("entropy")
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
assert amax < 1.1
|
||||
|
||||
@pytest.mark.parametrize("torch_hist", [False, True])
|
||||
def test_two_tensor(self, torch_hist, verbose):
|
||||
hist_calibrator = calib.HistogramCalibrator(8, None, False, torch_hist=torch_hist)
|
||||
|
||||
x_2 = torch.rand(11, 7, 3, 3).cuda() # uniform in (0,1)
|
||||
x_2[1, 1, 1, 1] = 10. # create outlier
|
||||
|
||||
|
||||
x_2 = torch.rand(11, 7, 3, 3).cuda() # uniform in (0,1)
|
||||
x_2[1, 1, 1, 1] = 10. # create outlier
|
||||
hist_calibrator.collect(x_2)
|
||||
x_3 = torch.rand(11, 7, 3, 3).cuda()
|
||||
hist_calibrator.collect(x_3)
|
||||
|
||||
# Don't have a better test metric. One outlier 10 should be discared by KL-divergence
|
||||
amax = hist_calibrator.compute_amax("entropy")
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
assert amax < 1.1
|
||||
|
||||
def test_repr(self):
|
||||
hist_calibrator = calib.HistogramCalibrator(8, None, True)
|
||||
repr(hist_calibrator)
|
||||
|
||||
class TestMSECalibrator():
|
||||
|
||||
def test_one_tensor(self, verbose):
|
||||
calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
|
||||
x_1 = torch.ones(11, 7, 3, 3).cuda() * 255.
|
||||
x_1[1, 1, 1, 1] = 256. # create an outlier
|
||||
calibrator.collect(x_1)
|
||||
|
||||
amax = calibrator.compute_amax("mse")
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be closer to 255
|
||||
assert (amax - 255.).abs() < (amax - 256.).abs()
|
||||
|
||||
def test_unsigned_one_tensor(self, verbose):
|
||||
calibrator = calib.HistogramCalibrator(8, None, True)
|
||||
|
||||
x_1 = torch.ones(11, 7, 3, 3).cuda() * 512.
|
||||
x_1[1, 1, 1, 1] = 513. # create an outlier
|
||||
calibrator.collect(x_1)
|
||||
|
||||
amax = calibrator.compute_amax("mse")
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be closer to 512
|
||||
assert (amax - 512.).abs() < (amax - 513.).abs()
|
||||
|
||||
@pytest.mark.parametrize("torch_hist", [False, True])
|
||||
def test_two_tensor(self, torch_hist, verbose):
|
||||
calibrator = calib.HistogramCalibrator(8, None, False, torch_hist=torch_hist)
|
||||
|
||||
x_1 = torch.ones(11, 7, 3, 3).cuda() * 255.
|
||||
x_1[1, 1, 1, 1] = 256. # create an outlier
|
||||
calibrator.collect(x_1)
|
||||
x_2 = torch.ones(11, 7, 3, 3).cuda() * 255.
|
||||
calibrator.collect(x_2)
|
||||
|
||||
amax = calibrator.compute_amax("mse")
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be closer to 255
|
||||
assert (amax - 255.).abs() < (amax - 256.).abs()
|
||||
|
||||
def test_repr(self):
|
||||
calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
repr(calibrator)
|
||||
|
||||
class TestPercentileCalibrator():
|
||||
|
||||
def test_one_tensor(self, verbose):
|
||||
calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
|
||||
x_1 = torch.arange(100)
|
||||
calibrator.collect(x_1)
|
||||
|
||||
amax = calibrator.compute_amax("percentile", percentile=90)
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be approximately 89
|
||||
assert (amax - 89.).abs() < 100/1024
|
||||
|
||||
def test_unsigned_one_tensor(self, verbose):
|
||||
calibrator = calib.HistogramCalibrator( 8, None, True)
|
||||
|
||||
x_1 = torch.arange(100)
|
||||
calibrator.collect(x_1)
|
||||
|
||||
amax = calibrator.compute_amax("percentile", percentile=80)
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be approximately 79
|
||||
assert (amax - 79.).abs() < 100/2048
|
||||
|
||||
@pytest.mark.parametrize("torch_hist", [False, True])
|
||||
def test_two_tensor(self, torch_hist, verbose):
|
||||
calibrator = calib.HistogramCalibrator(8, None, False, torch_hist=torch_hist)
|
||||
|
||||
x_1 = torch.arange(100)
|
||||
calibrator.collect(x_1)
|
||||
x_2 = torch.arange(0, 50, 0.5)
|
||||
calibrator.collect(x_2)
|
||||
amax = calibrator.compute_amax("percentile", percentile=99)
|
||||
|
||||
if verbose:
|
||||
print('amax={:.4f}'.format(amax.item()), end=' ')
|
||||
|
||||
# amax should be approximately 97
|
||||
assert (amax - 97.).abs() < 100/1024
|
||||
|
||||
def test_repr(self):
|
||||
calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
repr(calibrator)
|
||||
|
||||
def test_range(self):
|
||||
calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
x_1 = torch.arange(100)
|
||||
calibrator.collect(x_1)
|
||||
with pytest.raises(ValueError, match="range"):
|
||||
calibrator.compute_amax("percentile", percentile=-10)
|
||||
with pytest.raises(ValueError, match="range"):
|
||||
calibrator.compute_amax("percentile", percentile=200)
|
||||
|
||||
class TestCalibrateWeights():
|
||||
|
||||
def test_max(self):
|
||||
torch.manual_seed(12345)
|
||||
ref_lenet = QuantLeNet()
|
||||
torch.manual_seed(12345)
|
||||
test_lenet = QuantLeNet()
|
||||
|
||||
for module in ref_lenet.modules():
|
||||
if isinstance(module, (quant_nn.QuantConv2d, quant_nn.QuantLinear)):
|
||||
module.weight_quantizer.enable_calib()
|
||||
module.weight_quantizer.disable_quant()
|
||||
module.weight_quantizer(module.weight)
|
||||
module.weight_quantizer.load_calib_amax()
|
||||
|
||||
calib.calibrate_weights(test_lenet, method="max")
|
||||
|
||||
for ref_module, test_module in zip(ref_lenet.modules(), test_lenet.modules()):
|
||||
if isinstance(ref_module, (quant_nn.QuantConv2d, quant_nn.QuantLinear)):
|
||||
test_utils.compare(
|
||||
ref_module.weight_quantizer.amax, test_module.weight_quantizer.amax, rtol=0, atol=0, ctol=0)
|
||||
assert ref_module.weight_quantizer.amax.shape == test_module.weight_quantizer.amax.shape
|
||||
|
||||
def test_shape_with_axis(self):
|
||||
"""Check calibrate_weight function returns same shape as TensorQuantizer"""
|
||||
torch.manual_seed(12345)
|
||||
ref_lenet = QuantLeNet()
|
||||
torch.manual_seed(12345)
|
||||
test_lenet = QuantLeNet()
|
||||
|
||||
for module in ref_lenet.modules():
|
||||
if isinstance(module, (quant_nn.QuantConv2d, quant_nn.QuantLinear)):
|
||||
module.weight_quantizer.enable_calib()
|
||||
module.weight_quantizer.disable_quant()
|
||||
module.weight_quantizer(module.weight)
|
||||
module.weight_quantizer.load_calib_amax()
|
||||
|
||||
calib.calibrate_weights(test_lenet, method="percentile")
|
||||
|
||||
for ref_module, test_module in zip(ref_lenet.modules(), test_lenet.modules()):
|
||||
if isinstance(ref_module, (quant_nn.QuantConv2d, quant_nn.QuantLinear)):
|
||||
assert ref_module.weight_quantizer.amax.shape == test_module.weight_quantizer.amax.shape
|
||||
|
||||
def test_percentile(self):
|
||||
torch.manual_seed(12345)
|
||||
test_lenet = QuantLeNet()
|
||||
test_percentile = 99.99
|
||||
|
||||
ref_calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
|
||||
calib.calibrate_weights(test_lenet, method="percentile", perchannel=False, percentile=test_percentile)
|
||||
ref_calibrator.collect(test_lenet.conv1.weight)
|
||||
ref_amax = ref_calibrator.compute_amax("percentile", percentile=test_percentile)
|
||||
test_utils.compare(ref_amax, test_lenet.conv1.weight_quantizer.amax, rtol=0, atol=0, ctol=0)
|
||||
|
||||
def test_percentile_with_axis(self):
|
||||
torch.manual_seed(12345)
|
||||
test_lenet = QuantLeNet()
|
||||
test_percentile = 99.99
|
||||
|
||||
ref_calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
|
||||
calib.calibrate_weights(test_lenet, method="percentile", perchannel=True, percentile=test_percentile)
|
||||
ref_calibrator.collect(test_lenet.conv2.weight[1])
|
||||
ref_amax = ref_calibrator.compute_amax("percentile", percentile=test_percentile)
|
||||
test_utils.compare(ref_amax, test_lenet.conv2.weight_quantizer.amax[1], rtol=0, atol=0, ctol=0)
|
||||
|
||||
def test_mse(self):
|
||||
torch.manual_seed(12345)
|
||||
test_lenet = QuantLeNet()
|
||||
|
||||
ref_calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
|
||||
calib.calibrate_weights(test_lenet, method="mse", perchannel=False)
|
||||
ref_calibrator.collect(test_lenet.conv1.weight)
|
||||
ref_amax = ref_calibrator.compute_amax("mse")
|
||||
test_utils.compare(ref_amax, test_lenet.conv1.weight_quantizer.amax, rtol=0, atol=0, ctol=0)
|
||||
|
||||
def test_mse_with_axis(self):
|
||||
torch.manual_seed(12345)
|
||||
test_lenet = QuantLeNet()
|
||||
|
||||
ref_calibrator = calib.HistogramCalibrator(8, None, False)
|
||||
|
||||
calib.calibrate_weights(test_lenet, method="mse", perchannel=True)
|
||||
ref_calibrator.collect(test_lenet.conv2.weight[1])
|
||||
ref_amax = ref_calibrator.compute_amax("mse")
|
||||
test_utils.compare(ref_amax, test_lenet.conv2.weight_quantizer.amax[1], rtol=0, atol=0, ctol=0)
|
||||
@@ -0,0 +1,72 @@
|
||||
#
|
||||
# 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 the classification flow"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from os import path
|
||||
import glob
|
||||
import pytest
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
class TestClassificationFlow():
|
||||
|
||||
def test_resnet18(self, request, pytestconfig):
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
dataset_dir = pytestconfig.getoption('--data-dir')
|
||||
|
||||
# skip if the data dir flag was not set
|
||||
if not dataset_dir:
|
||||
pytest.skip("Prepare required dataset and use --data-dir option to enable")
|
||||
|
||||
# Verify data dir exists
|
||||
if not path.exists(dataset_dir):
|
||||
print("Dataset path %s doesn't exist"%(dataset_dir), file=sys.stderr)
|
||||
assert path.exists(dataset_dir)
|
||||
|
||||
# Append required paths to PYTHONPATH
|
||||
test_env = os.environ.copy()
|
||||
if 'PYTHONPATH' not in test_env:
|
||||
test_env['PYTHONPATH'] = ""
|
||||
|
||||
# Add project root and torchvision to the path (assuming running in nvcr.io/nvidia/pytorch:20.08-py3)
|
||||
test_env['PYTHONPATH'] += ":/opt/pytorch/vision/references/classification/:%s/../"%(dir_path)
|
||||
|
||||
# Add requirement egg files manually to path since we're spawning a new process (downloaded by setuptools)
|
||||
for egg in glob.glob(dir_path + "/../.eggs/*.egg"):
|
||||
test_env['PYTHONPATH'] += ":%s"%(egg)
|
||||
|
||||
# Run in a subprocess to avoid contaminating the module state for other test cases
|
||||
ret = subprocess.run(
|
||||
[
|
||||
'python3', dir_path + '/../examples/torchvision/classification_flow.py',
|
||||
'--data-dir', dataset_dir,
|
||||
'--model', 'resnet18', '--pretrained',
|
||||
'-t', '0.5',
|
||||
'--num-finetune-epochs', '2',
|
||||
'--evaluate-onnx',
|
||||
],
|
||||
env=test_env,
|
||||
check=False, stdout=subprocess.PIPE)
|
||||
|
||||
# If the test failed dump the output to stderr for better logging
|
||||
if ret.returncode != 0:
|
||||
print(ret.stdout, file=sys.stderr)
|
||||
|
||||
assert ret.returncode == 0
|
||||
@@ -0,0 +1,69 @@
|
||||
#
|
||||
# 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 Clip module."""
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
|
||||
from pytorch_quantization.nn.modules import clip
|
||||
|
||||
# make everything run on the GPU
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
|
||||
np.random.seed(1234)
|
||||
torch.manual_seed(1234)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
|
||||
class TestClip():
|
||||
|
||||
def test_simple_run(self):
|
||||
x_np = np.random.rand(1023).astype(np.float32)
|
||||
x_torch = torch.Tensor(x_np)
|
||||
clip_op = clip.Clip(torch.tensor(0.3), torch.tensor(0.7))
|
||||
clip_x_np = np.clip(x_np, 0.3, 0.7)
|
||||
clip_x_torch = clip_op(x_torch)
|
||||
np.testing.assert_array_equal(clip_x_torch.cpu().numpy(), clip_x_np)
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError, match="must be scalar"):
|
||||
clip_op = clip.Clip(torch.tensor(0.3), torch.tensor(0.7), learn_min=True)
|
||||
|
||||
def test_backward(self):
|
||||
x = torch.randn(3, 7, requires_grad=True)
|
||||
x.retain_grad()
|
||||
|
||||
min_value = 0.3
|
||||
max_value = 0.7
|
||||
clip_op = clip.Clip(min_value, max_value, learn_min=True, learn_max=True)
|
||||
clip_x = clip_op(x)
|
||||
clip_x.retain_grad()
|
||||
|
||||
labels = torch.randint(6, (3,)).type(torch.LongTensor).cuda()
|
||||
criterion = torch.nn.CrossEntropyLoss()
|
||||
loss = criterion(clip_x, labels)
|
||||
|
||||
loss.backward()
|
||||
|
||||
assert x.grad.cpu()[x.cpu() < min_value].sum() == 0
|
||||
assert x.grad.cpu()[x.cpu() > max_value].sum() == 0
|
||||
assert torch.equal(clip_x.grad[(x > min_value) & (x < max_value)], x.grad[(x > min_value) & (x < max_value)])
|
||||
@@ -0,0 +1,25 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""local configuration for pytests"""
|
||||
|
||||
import pytest
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption('--data-dir', type=str, dest="data_dir",
|
||||
default='', help="set dataset dir for tests")
|
||||
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
import pytest
|
||||
@pytest.fixture
|
||||
def verbose(request):
|
||||
return request.config.getoption("verbose")
|
||||
@@ -0,0 +1,69 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Model used for tests"""
|
||||
|
||||
import pytest
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from pytorch_quantization.nn import QuantConv2d, QuantLinear
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
|
||||
class LeNet(nn.Module):
|
||||
def __init__(self, **kwargs):
|
||||
super(LeNet, self).__init__()
|
||||
self.conv1 = nn.Conv2d(1, 10, kernel_size=5, **kwargs)
|
||||
self.conv2 = nn.Conv2d(10, 20, kernel_size=5, **kwargs)
|
||||
self.fc1 = nn.Linear(320, 50, **kwargs)
|
||||
self.fc2 = nn.Linear(50, 10, **kwargs)
|
||||
|
||||
def forward(self, x):
|
||||
x = F.relu(F.max_pool2d(self.conv1(x), 2))
|
||||
x = F.relu(F.max_pool2d(self.conv2(x), 2))
|
||||
x = x.view(-1, 320)
|
||||
x = F.relu(self.fc1(x))
|
||||
x = F.dropout(x, training=self.training)
|
||||
x = self.fc2(x)
|
||||
return F.log_softmax(x, dim=1)
|
||||
|
||||
|
||||
class QuantLeNet(nn.Module):
|
||||
def __init__(self, **kwargs):
|
||||
super(QuantLeNet, self).__init__()
|
||||
self.conv1 = QuantConv2d(1, 10, kernel_size=5, **kwargs)
|
||||
self.conv2 = QuantConv2d(10, 20, kernel_size=5, **kwargs)
|
||||
self.fc1 = QuantLinear(320, 50, **kwargs)
|
||||
self.fc2 = QuantLinear(50, 10, **kwargs)
|
||||
|
||||
def forward(self, x):
|
||||
x = F.relu(F.max_pool2d(self.conv1(x), 2))
|
||||
x = F.relu(F.max_pool2d(self.conv2(x), 2))
|
||||
x = x.view(-1, 320)
|
||||
x = F.relu(self.fc1(x))
|
||||
x = F.dropout(x, training=self.training)
|
||||
x = self.fc2(x)
|
||||
return F.log_softmax(x, dim=1)
|
||||
|
||||
@pytest.fixture
|
||||
def resnet18():
|
||||
import torchvision
|
||||
return torchvision.models.resnet18()
|
||||
|
||||
@pytest.fixture
|
||||
def quant_lenet():
|
||||
return QuantLeNet(quant_desc_input=QuantDescriptor(), quant_desc_weight=QuantDescriptor())
|
||||
@@ -0,0 +1,94 @@
|
||||
#
|
||||
# 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 supportive functions"""
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
|
||||
import pytorch_quantization.nn.functional as QF
|
||||
|
||||
np.random.seed(1234)
|
||||
torch.manual_seed(1234)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
|
||||
|
||||
class TestClip():
|
||||
|
||||
def test_simple_run(self):
|
||||
x_np = np.random.rand(1023).astype(np.float32)
|
||||
x_torch = torch.Tensor(x_np)
|
||||
clip_x_np = np.clip(x_np, 0.3, 0.7)
|
||||
clip_x_torch = QF.clip(x_torch, torch.tensor(0.3), torch.tensor(0.7))
|
||||
np.testing.assert_array_equal(clip_x_torch.cpu().numpy(), clip_x_np)
|
||||
|
||||
def test_raise(self):
|
||||
x = torch.randn(3, 7, requires_grad=True)
|
||||
|
||||
min_value = torch.Tensor(3, 7)
|
||||
max_value = torch.Tensor(3, 7)
|
||||
min_value.requires_grad = True
|
||||
max_value.requires_grad = True
|
||||
clip_x = QF.clip(x, min_value, max_value)
|
||||
|
||||
labels = torch.randint(6, (3,)).type(torch.LongTensor).cuda()
|
||||
criterion = torch.nn.CrossEntropyLoss()
|
||||
loss = criterion(clip_x, labels)
|
||||
with pytest.raises(ValueError, match="can only be scalar"):
|
||||
loss.backward()
|
||||
|
||||
def test_broadcast(self):
|
||||
"""Test broadcast behavior by randomly picked shuffling of np.random.rand"""
|
||||
x_np = np.random.rand(1023, 4, 5, 6).astype(np.float32) - 0.5
|
||||
x_torch = torch.Tensor(x_np)
|
||||
min_value = np.random.rand(1, 4, 1, 1).astype(np.float32) * 0.1 - 0.2
|
||||
max_value = np.random.rand(1, 4, 1, 1).astype(np.float32) * 10 + 0.5
|
||||
clip_x_np = np.clip(x_np, min_value, max_value)
|
||||
clip_x_torch = QF.clip(x_torch, torch.tensor(min_value), torch.tensor(max_value))
|
||||
np.testing.assert_array_equal(clip_x_torch.cpu().numpy(), clip_x_np)
|
||||
|
||||
def test_backward(self):
|
||||
x = torch.randn(3, 1025, requires_grad=True)
|
||||
x.retain_grad()
|
||||
|
||||
min_value = torch.tensor(0.3)
|
||||
max_value = torch.tensor(0.7)
|
||||
min_value.requires_grad = True
|
||||
max_value.requires_grad = True
|
||||
min_value.retain_grad()
|
||||
max_value.retain_grad()
|
||||
clip_x = QF.clip(x, min_value, max_value)
|
||||
clip_x.retain_grad()
|
||||
|
||||
labels = torch.randint(6, (3,)).type(torch.LongTensor).cuda()
|
||||
criterion = torch.nn.CrossEntropyLoss()
|
||||
loss = criterion(clip_x, labels)
|
||||
loss.backward()
|
||||
|
||||
np.testing.assert_array_almost_equal(
|
||||
clip_x.grad[x < min_value].sum().cpu().numpy(), min_value.grad.cpu().numpy(), decimal=6)
|
||||
np.testing.assert_array_almost_equal(
|
||||
clip_x.grad[x > max_value].sum().cpu().numpy(), max_value.grad.cpu().numpy(), decimal=6)
|
||||
assert x.grad.cpu()[x.cpu() < min_value.cpu()].sum() == 0
|
||||
assert x.grad.cpu()[x.cpu() > max_value.cpu()].sum() == 0
|
||||
assert torch.equal(clip_x.grad[(x > min_value) & (x < max_value)], x.grad[(x > min_value) & (x < max_value)])
|
||||
@@ -0,0 +1,216 @@
|
||||
#
|
||||
# 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 integrating Quant layers into a network"""
|
||||
|
||||
import pytest
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import torch.optim as optim
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization import quant_modules
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
from pytorch_quantization.nn.modules import tensor_quantizer
|
||||
from tests.fixtures.models import LeNet, QuantLeNet
|
||||
from tests.fixtures import verbose
|
||||
|
||||
np.random.seed(12345) # seed 1234 causes 1 number mismatch at 6th decimal in one of the tests
|
||||
|
||||
# make everything run on the GPU
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
class TestNetwork():
|
||||
"""test basic operations of quantized network"""
|
||||
|
||||
def test_simple_build(self):
|
||||
"""test instantiation"""
|
||||
quant_model = QuantLeNet(quant_desc_input=QuantDescriptor(), quant_desc_weight=QuantDescriptor())
|
||||
for name, module in quant_model.named_modules():
|
||||
if "quantizer" in name:
|
||||
module.disable()
|
||||
|
||||
input_desc = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
weight_desc = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
quant_model = QuantLeNet(quant_desc_input=input_desc, quant_desc_weight=weight_desc)
|
||||
|
||||
input_desc = QuantDescriptor(amax=6.)
|
||||
weight_desc = QuantDescriptor(amax=1.)
|
||||
quant_model = QuantLeNet(quant_desc_input=input_desc, quant_desc_weight=weight_desc)
|
||||
|
||||
|
||||
def test_forward(self):
|
||||
"""test forward pass with random data"""
|
||||
input_desc = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
weight_desc = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
quant_model = QuantLeNet(quant_desc_input=input_desc, quant_desc_weight=weight_desc)
|
||||
output = quant_model(torch.empty(16, 1, 28, 28))
|
||||
|
||||
def test_backward(self):
|
||||
"""test one iteration with random data and labels"""
|
||||
input_desc = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
weight_desc = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
quant_model = QuantLeNet(quant_desc_input=input_desc, quant_desc_weight=weight_desc)
|
||||
optimizer = optim.SGD(quant_model.parameters(), lr=0.01)
|
||||
optimizer.zero_grad()
|
||||
output = quant_model(torch.empty(16, 1, 28, 28))
|
||||
loss = F.nll_loss(output, torch.randint(10, (16,), dtype=torch.int64))
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
def test_native_amp_fp16(self):
|
||||
"""test one iteration with random data and labels"""
|
||||
input_desc = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
weight_desc = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
|
||||
model = QuantLeNet(quant_desc_input=input_desc, quant_desc_weight=weight_desc)
|
||||
optimizer = optim.SGD(model.parameters(), lr=0.01)
|
||||
optimizer.zero_grad()
|
||||
with torch.cuda.amp.autocast():
|
||||
output = model(torch.empty(16, 1, 28, 28))
|
||||
loss = F.nll_loss(output, torch.randint(10, (16,), dtype=torch.int64))
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
assert loss.dtype == torch.float32
|
||||
|
||||
def test_asp(self):
|
||||
"""test Sparsity (ASP) and QAT toolkits together"""
|
||||
try:
|
||||
from apex.contrib.sparsity import ASP
|
||||
except ImportError:
|
||||
pytest.skip("ASP is not available.")
|
||||
|
||||
quant_modules.initialize()
|
||||
model = LeNet()
|
||||
quant_modules.deactivate()
|
||||
|
||||
optimizer = optim.SGD(model.parameters(), lr=0.01)
|
||||
|
||||
ASP.init_model_for_pruning(
|
||||
model,
|
||||
mask_calculator="m4n2_1d",
|
||||
verbosity=2,
|
||||
whitelist=[torch.nn.Linear, torch.nn.Conv2d, torch.nn.Conv3d, quant_nn.modules.quant_linear.QuantLinear],
|
||||
allow_recompute_mask=False,
|
||||
custom_layer_dict={
|
||||
quant_nn.QuantConv1d: ['weight'],
|
||||
quant_nn.QuantConv2d: ['weight'],
|
||||
quant_nn.QuantConv3d: ['weight'],
|
||||
quant_nn.QuantConvTranspose1d: ['weight'],
|
||||
quant_nn.QuantConvTranspose2d: ['weight'],
|
||||
quant_nn.QuantConvTranspose3d: ['weight'],
|
||||
quant_nn.QuantLinear: ['weight']
|
||||
},
|
||||
allow_permutation=False)
|
||||
ASP.init_optimizer_for_pruning(optimizer)
|
||||
ASP.compute_sparse_masks()
|
||||
|
||||
model = model.to('cuda')
|
||||
output = model(torch.empty(16, 1, 28, 28).to('cuda'))
|
||||
optimizer.zero_grad()
|
||||
loss = F.nll_loss(output, torch.randint(10, (16,), dtype=torch.int64))
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
def test_quant_module_replacement(self):
|
||||
"""test monkey patching of modules with their quantized versions"""
|
||||
lenet = LeNet()
|
||||
qlenet = QuantLeNet()
|
||||
|
||||
mod_list = [type(mod) for name, mod in lenet.named_modules()]
|
||||
mod_list = mod_list[1:]
|
||||
qmod_list = [type(mod) for name, mod in qlenet.named_modules()]
|
||||
qmod_list = qmod_list[1:]
|
||||
|
||||
# Before any monkey patching, the networks should be different
|
||||
assert(mod_list != qmod_list)
|
||||
|
||||
# Monkey patch the modules
|
||||
no_replace_list = ["Linear"]
|
||||
custom_quant_modules = [(torch.nn, "Linear", quant_nn.QuantLinear)]
|
||||
|
||||
quant_modules.initialize(no_replace_list, custom_quant_modules)
|
||||
|
||||
lenet = LeNet()
|
||||
qlenet = QuantLeNet()
|
||||
|
||||
mod_list = [type(mod) for name, mod in lenet.named_modules()]
|
||||
mod_list = mod_list[1:]
|
||||
qmod_list = [type(mod) for name, mod in qlenet.named_modules()]
|
||||
qmod_list = qmod_list[1:]
|
||||
|
||||
# After monkey patching, the networks should be same
|
||||
assert(mod_list == qmod_list)
|
||||
|
||||
# Reverse monkey patching
|
||||
quant_modules.deactivate()
|
||||
|
||||
lenet = LeNet()
|
||||
qlenet = QuantLeNet()
|
||||
|
||||
mod_list = [type(mod) for name, mod in lenet.named_modules()]
|
||||
mod_list = mod_list[1:]
|
||||
qmod_list = [type(mod) for name, mod in qlenet.named_modules()]
|
||||
qmod_list = qmod_list[1:]
|
||||
|
||||
# After reversing monkey patching, the networks should again be different
|
||||
assert(mod_list != qmod_list)
|
||||
|
||||
def test_calibration(self):
|
||||
quant_model = QuantLeNet(quant_desc_input=QuantDescriptor(), quant_desc_weight=QuantDescriptor()).cuda()
|
||||
|
||||
for name, module in quant_model.named_modules():
|
||||
if name.endswith("_quantizer"):
|
||||
if module._calibrator is not None:
|
||||
module.disable_quant()
|
||||
module.enable_calib()
|
||||
else:
|
||||
module.disable()
|
||||
print(F"{name:40}: {module}")
|
||||
|
||||
quant_model(torch.rand(16, 1, 224, 224, device="cuda"))
|
||||
|
||||
# Load calib result and disable calibration
|
||||
for name, module in quant_model.named_modules():
|
||||
if name.endswith("_quantizer"):
|
||||
if module._calibrator is not None:
|
||||
module.load_calib_amax()
|
||||
module.enable_quant()
|
||||
module.disable_calib()
|
||||
else:
|
||||
module.enable()
|
||||
quant_model.cuda()
|
||||
|
||||
def test_state_load(self):
|
||||
quant_desc = tensor_quant.QuantDescriptor(axis=1, num_bits=8, amax=127.0)
|
||||
quantizer = tensor_quantizer.TensorQuantizer(quant_desc).cuda()
|
||||
quantizer2 = tensor_quantizer.TensorQuantizer(quant_desc).cuda()
|
||||
quantizer2.pre_quant_scale = torch.Tensor([[1.0, 2.0, 3.0, 4.0]]).cuda()
|
||||
buffer = io.BytesIO()
|
||||
torch.save(quantizer2.state_dict(), buffer)
|
||||
|
||||
buffer.seek(0)
|
||||
quantizer.load_state_dict(torch.load(buffer))
|
||||
|
||||
assert torch.allclose(quantizer.pre_quant_scale, quantizer2.pre_quant_scale)
|
||||
@@ -0,0 +1,86 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""test the license of source files."""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from filecmp import cmp
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
class TestLicense():
|
||||
|
||||
def test_license(self):
|
||||
root = Path(__file__).parent.parent.absolute()
|
||||
root_len = len(str(root))
|
||||
|
||||
# Collect files ending with relevant extensions
|
||||
file_list = []
|
||||
file_types = ['*.py', '*.cpp', '*.cu', '*.h', '*.hpp', '*.c', '*.sh']
|
||||
for ft in file_types:
|
||||
file_list += list(root.rglob(ft))
|
||||
|
||||
# Trim files from build folders
|
||||
build_folders = ['build', 'dist', '.eggs', '.vscode']
|
||||
build_files = []
|
||||
for src_file in file_list:
|
||||
local_path = str(src_file.parents[0])[root_len : ]
|
||||
for folder in build_folders:
|
||||
if folder in local_path:
|
||||
build_files.append(src_file)
|
||||
|
||||
for bf in build_files:
|
||||
file_list.remove(bf)
|
||||
|
||||
print (f"Found {len(file_list)} source files")
|
||||
|
||||
cpp_header = (root / 'tests' / 'license_test_header_cpp.txt').open().readlines()
|
||||
py_header = (root / 'tests' / 'license_test_header_py.txt').open().readlines()
|
||||
sh_header = (root / 'tests' / 'license_test_header_sh.txt').open().readlines()
|
||||
|
||||
invalid_files = []
|
||||
for f in file_list:
|
||||
with open(f) as src_file:
|
||||
src_lines = src_file.readlines()
|
||||
|
||||
# Skip empty files
|
||||
if len(src_lines) == 0:
|
||||
continue
|
||||
|
||||
if f.suffix == '.py':
|
||||
header = py_header
|
||||
elif f.suffix == '.sh':
|
||||
header = sh_header
|
||||
else:
|
||||
header = cpp_header
|
||||
|
||||
num_lines = len(header)
|
||||
if len(src_lines) < num_lines:
|
||||
invalid_files.append(f)
|
||||
continue
|
||||
|
||||
for i in range(num_lines):
|
||||
if src_lines[i] != header[i]:
|
||||
invalid_files.append(f)
|
||||
break
|
||||
|
||||
if len(invalid_files) > 0:
|
||||
for f in invalid_files:
|
||||
print(f"The file {f} has an invalid header!")
|
||||
raise AssertionError("%d files have invalid headers!" % (len(invalid_files)))
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@@ -0,0 +1,16 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
@@ -0,0 +1,92 @@
|
||||
#
|
||||
# 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 calibrators"""
|
||||
import inspect
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
|
||||
from pytorch_quantization import enable_onnx_export
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
from pytorch_quantization import calib
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
import tests.utils as test_utils
|
||||
from examples.torchvision.models.classification import *
|
||||
from tests.fixtures import verbose
|
||||
from tests.fixtures.models import QuantLeNet
|
||||
|
||||
np.random.seed(12345)
|
||||
torch.manual_seed(12345)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
|
||||
class TestExampleModels():
|
||||
|
||||
def test_resnet50(self):
|
||||
model = resnet50(pretrained=True, quantize=True)
|
||||
model.eval()
|
||||
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith('_quantizer'):
|
||||
module.amax = 2.50
|
||||
|
||||
model.cuda()
|
||||
dummy_input = torch.randn(1, 3, 224, 224, device='cuda')
|
||||
with enable_onnx_export():
|
||||
if "enable_onnx_checker" in inspect.signature(torch.onnx.export).parameters:
|
||||
torch.onnx.export(model,
|
||||
dummy_input,
|
||||
"/tmp/resnet50.onnx",
|
||||
verbose=False,
|
||||
opset_version=13,
|
||||
enable_onnx_checker=False,
|
||||
do_constant_folding=True)
|
||||
else:
|
||||
torch.onnx.export(model,
|
||||
dummy_input,
|
||||
"/tmp/resnet50.onnx",
|
||||
verbose=False,
|
||||
opset_version=13,
|
||||
do_constant_folding=True)
|
||||
|
||||
def test_resnet50_cpu(self):
|
||||
model = resnet50(pretrained=True, quantize=True)
|
||||
model.eval()
|
||||
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith('_quantizer'):
|
||||
module.amax = 2.50
|
||||
|
||||
dummy_input = torch.randn(1, 3, 224, 224)
|
||||
with enable_onnx_export():
|
||||
if "enable_onnx_checker" in inspect.signature(torch.onnx.export).parameters:
|
||||
torch.onnx.export(model,
|
||||
dummy_input,
|
||||
"/tmp/resnet50_cpu.onnx",
|
||||
verbose=False,
|
||||
opset_version=13,
|
||||
enable_onnx_checker=False,
|
||||
do_constant_folding=True)
|
||||
else:
|
||||
torch.onnx.export(model,
|
||||
dummy_input,
|
||||
"/tmp/resnet50.onnx",
|
||||
verbose=False,
|
||||
opset_version=13,
|
||||
do_constant_folding=True)
|
||||
@@ -0,0 +1,111 @@
|
||||
#
|
||||
# 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 helper functions for quant optimizer"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
import torch.optim as optim
|
||||
|
||||
from pytorch_quantization.optim import helper
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
from .fixtures.models import QuantLeNet
|
||||
from .fixtures.models import resnet18
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
class TestMatchParameters():
|
||||
|
||||
def test_single_key(self, resnet18):
|
||||
param = helper.match_parameters(resnet18, ['downsample.0.weight'])
|
||||
assert len(list(param)) == 3
|
||||
|
||||
def test_multi_keys(self, resnet18):
|
||||
param = list(helper.match_parameters(resnet18, ['conv1', 'downsample']))
|
||||
assert len(param) == 18
|
||||
|
||||
def test_regex(self, resnet18):
|
||||
param = helper.match_parameters(resnet18, ['downsample.*.weight$'])
|
||||
assert len(list(param)) == 6
|
||||
|
||||
param = helper.match_parameters(resnet18, ['downsample.*.wei$'])
|
||||
assert not list(param)
|
||||
|
||||
|
||||
class TestGroupParameters():
|
||||
|
||||
def test_single_key(self, resnet18):
|
||||
param_groups = helper.group_parameters(resnet18, [['downsample.1.weight']])
|
||||
assert len(list(param_groups[0]['params'])) == 3
|
||||
|
||||
def test_lr_momentum_decay(self, resnet18):
|
||||
lrs = [0.01, 0.001]
|
||||
momentums = [0.02, 0.002]
|
||||
weight_decays = [0.03, 0.003]
|
||||
param_groups = helper.group_parameters(
|
||||
resnet18, [['conv1.*weight'], ['downsample.*.weight']], lrs, momentums, weight_decays)
|
||||
|
||||
assert param_groups[0]['lr'] == lrs[0]
|
||||
assert param_groups[1]['lr'] == lrs[1]
|
||||
assert param_groups[0]['momentum'] == momentums[0]
|
||||
assert param_groups[1]['momentum'] == momentums[1]
|
||||
assert param_groups[0]['weight_decay'] == weight_decays[0]
|
||||
assert param_groups[1]['weight_decay'] == weight_decays[1]
|
||||
|
||||
def test_optimizer_feed(self, resnet18):
|
||||
"""Feed grouped parameters to optimizer, see what happens"""
|
||||
lrs = [0.01, 0.001]
|
||||
momentums = [0.02, 0.002]
|
||||
weight_decays = [0.03, 0.003]
|
||||
param_groups = helper.group_parameters(
|
||||
resnet18, [['conv1.*weight'], ['downsample.*.weight']], lrs, momentums, weight_decays)
|
||||
optimizer = optim.SGD(param_groups)
|
||||
optimizer.step()
|
||||
|
||||
def test_raises(self):
|
||||
with pytest.raises(TypeError, match="must be list of list of patterns"):
|
||||
helper.group_parameters(None, [['downsample.1.weight'], 'conv1'])
|
||||
|
||||
with pytest.raises(TypeError, match="must match"):
|
||||
helper.group_parameters(None, [['downsample.1.weight'], ['conv1']], lrs=[0.1])
|
||||
|
||||
with pytest.raises(TypeError, match="must match"):
|
||||
helper.group_parameters(None, [['downsample.1.weight'], ['conv1']], momentums=[0.1])
|
||||
|
||||
with pytest.raises(TypeError, match="must match"):
|
||||
helper.group_parameters(None, [['downsample.1.weight'], ['conv1']], weight_decays=[0.1])
|
||||
|
||||
|
||||
class TestFreezeParameters():
|
||||
|
||||
def test_simple(self, resnet18):
|
||||
helper.freeze_parameters(resnet18, ['downsample.0.weight'])
|
||||
for name, param in resnet18.named_parameters():
|
||||
if 'downsample.0.weight' in name:
|
||||
assert not param.requires_grad
|
||||
|
||||
class TestQuantWeightInPlace():
|
||||
|
||||
def test_simple(self):
|
||||
quant_lenet = QuantLeNet(
|
||||
quant_desc_input=QuantDescriptor(),
|
||||
quant_desc_weight=QuantDescriptor())
|
||||
quant_lenet.eval()
|
||||
helper.quant_weight_inplace(quant_lenet)
|
||||
@@ -0,0 +1,60 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""test for str and repr
|
||||
Make sure things can print and in a nice form. Put all the print tests together so that running this test file alone
|
||||
can inspect all the print messages in the project
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from pytorch_quantization import calib
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
from pytorch_quantization.nn.modules.tensor_quantizer import TensorQuantizer
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
class TestPrint():
|
||||
|
||||
def test_print_descriptor(self):
|
||||
test_desc = tensor_quant.QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL
|
||||
print(test_desc)
|
||||
|
||||
def test_print_tensor_quantizer(self):
|
||||
test_quantizer = TensorQuantizer()
|
||||
print(test_quantizer)
|
||||
|
||||
def test_print_module(self):
|
||||
class _TestModule(nn.Module):
|
||||
def __init__(self):
|
||||
super(_TestModule, self).__init__()
|
||||
self.conv = nn.Conv2d(33, 65, 3)
|
||||
self.quant_conv = quant_nn.Conv2d(33, 65, 3)
|
||||
self.linear = nn.Linear(33, 65)
|
||||
self.quant_linear = quant_nn.Linear(33, 65)
|
||||
|
||||
test_module = _TestModule()
|
||||
print(test_module)
|
||||
|
||||
def test_print_calibrator(self):
|
||||
print(calib.MaxCalibrator(7, 1, False))
|
||||
hist_calibrator = calib.HistogramCalibrator(8, None, True)
|
||||
hist_calibrator.collect(torch.rand(10))
|
||||
print(hist_calibrator)
|
||||
@@ -0,0 +1,557 @@
|
||||
#
|
||||
# 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 QuantConv module.
|
||||
Mose tests check the functionality of all the combinations in Quant conv against the corresponding functionalities in
|
||||
tensor_quant. There are tests for all the three QuantConv1D, QuantConv2D, and QuantConv3D
|
||||
"""
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
from pytorch_quantization.nn.modules.tensor_quantizer import TensorQuantizer
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
from pytorch_quantization.nn.modules import quant_conv
|
||||
import tests.utils as test_utils
|
||||
|
||||
# make everything run on the GPU
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
|
||||
torch.backends.cudnn.deterministic = True
|
||||
|
||||
np.random.seed(1234)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
_NUM_IN_CHANNELS = 13
|
||||
_NUM_OUT_CHANNELS = 17
|
||||
|
||||
class TestQuantConv2D():
|
||||
#Quantizing weight
|
||||
|
||||
def test_no_quant(self):
|
||||
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256, 256)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_copy
|
||||
|
||||
out1 = F.conv2d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_quant_per_tensor(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=QuantDescriptor())
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256, 256)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.conv2d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_quant_per_channel(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256, 256)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(
|
||||
weight_copy,
|
||||
torch.max(torch.abs(weight_copy).view(_NUM_OUT_CHANNELS, -1), dim=1, keepdim=True)[0].view(
|
||||
_NUM_OUT_CHANNELS, 1, 1, 1))
|
||||
|
||||
out1 = F.conv2d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_in_feature_fake_quant(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256, 256)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.conv2d(quant_input, quant_conv_object.weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv2d(
|
||||
_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=False, quant_desc_weight=QuantDescriptor())
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.conv2d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv2d(_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=False,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL)
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(
|
||||
weight_copy,
|
||||
torch.max(torch.abs(weight_copy).view(_NUM_OUT_CHANNELS, -1), dim=1, keepdim=True)[0].view(
|
||||
_NUM_OUT_CHANNELS, 1, 1, 1))
|
||||
|
||||
out1 = F.conv2d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_other_prec(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_desc_input = QuantDescriptor(num_bits=4)
|
||||
quant_desc_weight = QuantDescriptor(num_bits=3)
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_input=quant_desc_input,
|
||||
quant_desc_weight=quant_desc_weight)
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16, 16)
|
||||
|
||||
test_input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
weight_quantizer = TensorQuantizer(quant_desc_weight)
|
||||
|
||||
quant_input = test_input_quantizer(test_input)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_quantizer(weight_copy)
|
||||
|
||||
out1 = F.conv2d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_bias(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv2d(_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=True,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL)
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(
|
||||
weight_copy,
|
||||
torch.max(torch.abs(weight_copy).view(_NUM_OUT_CHANNELS, -1), dim=1, keepdim=True)[0].view(
|
||||
_NUM_OUT_CHANNELS, 1, 1, 1))
|
||||
|
||||
out1 = F.conv2d(quant_input, quant_weight, bias=quant_conv_object.bias)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_against_unquantized(self):
|
||||
kernel_size = 3
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 24, 24).cuda()
|
||||
|
||||
torch.manual_seed(12345)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(12345)
|
||||
fake_quant_conv2d = quant_conv.QuantConv2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_input=QuantDescriptor(num_bits=16),
|
||||
quant_desc_weight=QuantDescriptor(num_bits=16, axis=(0)))
|
||||
|
||||
# Reset seed. Make sure weight and bias are the same
|
||||
torch.manual_seed(12345)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(12345)
|
||||
conv2d = nn.Conv2d(_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=True)
|
||||
|
||||
fake_quant_output = fake_quant_conv2d(test_input)
|
||||
output = conv2d(test_input)
|
||||
|
||||
test_utils.compare(fake_quant_output, output, rtol=1e-6, atol=1.5e-4)
|
||||
|
||||
|
||||
def test_set_default_quant_desc(self):
|
||||
quant_conv_layer = quant_conv.Conv2d(32, 257, 3)
|
||||
assert quant_conv_layer.input_quantizer._axis == None
|
||||
assert quant_conv_layer.weight_quantizer._axis == (0)
|
||||
|
||||
# set default to a different one
|
||||
quant_desc_input = QuantDescriptor(num_bits=11)
|
||||
quant_desc_weight = QuantDescriptor(num_bits=13, axis=(1))
|
||||
quant_conv.QuantConv2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_conv.QuantConv2d.set_default_quant_desc_weight(quant_desc_weight)
|
||||
|
||||
# Create one with default descriptor
|
||||
quant_conv_layer = quant_conv.Conv2d(32, 257, 3)
|
||||
# Check quant_desc in quantizer created with default descriptor
|
||||
assert quant_conv_layer.input_quantizer._num_bits == quant_desc_input.num_bits
|
||||
assert quant_conv_layer.weight_quantizer._axis == quant_desc_weight.axis
|
||||
|
||||
# Test default is per class
|
||||
quant_conv_layer = quant_conv.Conv3d(31, 255, 5)
|
||||
assert quant_conv_layer.input_quantizer._num_bits != quant_desc_input.num_bits
|
||||
assert quant_conv_layer.weight_quantizer._axis != quant_desc_weight.axis
|
||||
|
||||
# Reset default
|
||||
quant_conv.QuantConv2d.set_default_quant_desc_input(QuantDescriptor())
|
||||
quant_conv.QuantConv2d.set_default_quant_desc_weight(QuantDescriptor(axis=(0)))
|
||||
|
||||
def test_unused_kwargs(self):
|
||||
with pytest.raises(TypeError, match="Unused keys"):
|
||||
quant_conv.Conv2d(32, 257, 3, descriptor='oops')
|
||||
|
||||
class TestQuantConv1D():
|
||||
|
||||
def test_no_quant(self):
|
||||
kernel_size = 8
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_copy
|
||||
|
||||
out1 = F.conv1d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_quant_per_tensor(self):
|
||||
kernel_size = 8
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=QuantDescriptor())
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.conv1d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_quant_per_channel(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=QuantDescriptor(axis=(0)))
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=(1, 2))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.conv1d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_input(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(20, _NUM_IN_CHANNELS, 50)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.conv1d(quant_input, quant_conv_object.weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=False, quant_desc_weight=QuantDescriptor())
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.conv1d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=QuantDescriptor(axis=(0)))
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(
|
||||
weight_copy,
|
||||
torch.max(torch.abs(weight_copy).view(_NUM_OUT_CHANNELS, -1), dim=1, keepdim=True)[0].view(
|
||||
_NUM_OUT_CHANNELS, 1, 1))
|
||||
|
||||
out1 = F.conv1d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_other_prec(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_desc_input = QuantDescriptor(num_bits=4)
|
||||
quant_desc_weight = QuantDescriptor(num_bits=3, axis=(0))
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_input=quant_desc_input,
|
||||
quant_desc_weight=quant_desc_weight)
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16)
|
||||
|
||||
test_input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
weight_quantizer = TensorQuantizer(quant_desc_weight)
|
||||
|
||||
quant_input = test_input_quantizer(test_input)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_quantizer(weight_copy)
|
||||
|
||||
out1 = F.conv1d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_bias(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_weight=QuantDescriptor(axis=(0)))
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(
|
||||
weight_copy,
|
||||
torch.max(torch.abs(weight_copy).view(_NUM_OUT_CHANNELS, -1), dim=1, keepdim=True)[0].view(
|
||||
_NUM_OUT_CHANNELS, 1, 1))
|
||||
|
||||
out1 = F.conv1d(quant_input, quant_weight, bias=quant_conv_object.bias)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_against_unquantized(self):
|
||||
kernel_size = 3
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 24).cuda()
|
||||
|
||||
torch.manual_seed(12345)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(12345)
|
||||
fake_quant_conv1d = quant_conv.QuantConv1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_input=QuantDescriptor(num_bits=16),
|
||||
quant_desc_weight=QuantDescriptor(num_bits=16, axis=(0)))
|
||||
|
||||
# Reset seed. Make sure weight and bias are the same
|
||||
torch.manual_seed(12345)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(12345)
|
||||
conv1d = nn.Conv1d(_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=True)
|
||||
|
||||
fake_quant_output = fake_quant_conv1d(test_input)
|
||||
output = conv1d(test_input)
|
||||
|
||||
test_utils.compare(fake_quant_output, output, rtol=1e-5, atol=1e-4)
|
||||
|
||||
|
||||
class TestQuantConv3D():
|
||||
#Quantizing weight
|
||||
|
||||
def test_no_quant(self):
|
||||
kernel_size = 8
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv3d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 8, 8, 8)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_copy
|
||||
|
||||
out1 = F.conv3d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_quant_per_channel_other_prec(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_desc_input = QuantDescriptor(num_bits=4)
|
||||
quant_desc_weight = QuantDescriptor(num_bits=3, axis=(0))
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv3d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_input=quant_desc_input,
|
||||
quant_desc_weight=quant_desc_weight)
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 8, 8, 8)
|
||||
|
||||
test_input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
weight_quantizer = TensorQuantizer(quant_desc_weight)
|
||||
|
||||
quant_input = test_input_quantizer(test_input)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_quantizer(weight_copy)
|
||||
|
||||
out1 = F.conv3d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_quant_per_channel_bias(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConv3d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_weight=QuantDescriptor(axis=(0)))
|
||||
test_input = torch.randn(8, _NUM_IN_CHANNELS, 8, 8, 8)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(
|
||||
weight_copy,
|
||||
torch.max(torch.abs(weight_copy).view(_NUM_OUT_CHANNELS, -1), dim=1, keepdim=True)[0].view(
|
||||
_NUM_OUT_CHANNELS, 1, 1, 1, 1))
|
||||
|
||||
out1 = F.conv3d(quant_input, quant_weight, bias=quant_conv_object.bias)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_against_unquantized(self):
|
||||
kernel_size = 3
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 24, 24, 24).cuda()
|
||||
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
fake_quant_conv3d = quant_conv.QuantConv3d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_input=QuantDescriptor(num_bits=16),
|
||||
quant_desc_weight=QuantDescriptor(num_bits=16, axis=(0)))
|
||||
|
||||
# Reset seed. Make sure weight and bias are the same
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
conv3d = nn.Conv3d(_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=True)
|
||||
|
||||
fake_quant_output = fake_quant_conv3d(test_input)
|
||||
output = conv3d(test_input)
|
||||
|
||||
test_utils.compare(fake_quant_output, output, rtol=1e-6, atol=2e-4)
|
||||
@@ -0,0 +1,522 @@
|
||||
#
|
||||
# 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 QuantConv module.
|
||||
Test for QuantConvTransposed
|
||||
"""
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
from pytorch_quantization.nn.modules.tensor_quantizer import TensorQuantizer
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
from pytorch_quantization.nn.modules import quant_conv
|
||||
import tests.utils as test_utils
|
||||
|
||||
# make everything run on the GPU
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
|
||||
torch.backends.cudnn.deterministic = True
|
||||
|
||||
np.random.seed(1234)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
_NUM_IN_CHANNELS = 13
|
||||
_NUM_OUT_CHANNELS = 17
|
||||
|
||||
|
||||
class TestQuantConvTranspose2D():
|
||||
|
||||
def test_no_quant(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 32, 32)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_copy
|
||||
|
||||
out1 = F.conv_transpose2d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_quant_per_tensor(self):
|
||||
kernel_size = 8
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=QuantDescriptor())
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
test_input = torch.randn(256, _NUM_IN_CHANNELS, 32, 32)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.conv_transpose2d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_quant_per_channel(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE2D_WEIGHT_PER_CHANNEL)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256, 256)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=(0, 2, 3))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.conv_transpose2d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_input(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(20, _NUM_IN_CHANNELS, 50, 50)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.conv_transpose2d(quant_input, quant_conv_object.weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=False, quant_desc_weight=QuantDescriptor())
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.conv_transpose2d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=QuantDescriptor(axis=(1)))
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=(0, 2, 3))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.conv_transpose2d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_other_prec(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_desc_input = QuantDescriptor(num_bits=4)
|
||||
quant_desc_weight = QuantDescriptor(num_bits=3, axis=(1))
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_input=quant_desc_input,
|
||||
quant_desc_weight=quant_desc_weight)
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16, 16)
|
||||
|
||||
test_input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
weight_quantizer = TensorQuantizer(quant_desc_weight)
|
||||
|
||||
quant_input = test_input_quantizer(test_input)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_quantizer(weight_copy)
|
||||
|
||||
out1 = F.conv_transpose2d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_bias(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_weight=QuantDescriptor(axis=(1)))
|
||||
test_input = torch.randn(2, _NUM_IN_CHANNELS, 2, 2)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=(0, 2, 3))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.conv_transpose2d(quant_input, quant_weight, bias=quant_conv_object.bias)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_against_unquantized(self):
|
||||
kernel_size = 3
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 32, 32).cuda()
|
||||
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
fake_quant_conv2d = quant_conv.QuantConvTranspose2d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_input=QuantDescriptor(num_bits=16),
|
||||
quant_desc_weight=QuantDescriptor(num_bits=16, axis=(1)))
|
||||
|
||||
# Reset seed. Make sure weight and bias are the same
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
conv2d = nn.ConvTranspose2d(_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=True)
|
||||
|
||||
fake_quant_output = fake_quant_conv2d(test_input)
|
||||
output = conv2d(test_input)
|
||||
|
||||
test_utils.compare(fake_quant_output, output, rtol=1e-5, atol=2e-4)
|
||||
|
||||
|
||||
class TestQuantConvTranspose3D():
|
||||
|
||||
def test_no_quant(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose3d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 32, 32, 32)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_copy
|
||||
|
||||
out1 = F.conv_transpose3d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_other_prec(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_desc_input = QuantDescriptor(num_bits=4)
|
||||
quant_desc_weight = QuantDescriptor(num_bits=3, axis=(1))
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose3d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_input=quant_desc_input,
|
||||
quant_desc_weight=quant_desc_weight)
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16, 16, 16)
|
||||
|
||||
test_input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
weight_quantizer = TensorQuantizer(quant_desc_weight)
|
||||
|
||||
quant_input = test_input_quantizer(test_input)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_quantizer(weight_copy)
|
||||
|
||||
out1 = F.conv_transpose3d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_bias(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose3d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE3D_WEIGHT_PER_CHANNEL)
|
||||
test_input = torch.randn(2, _NUM_IN_CHANNELS, 2, 2, 2)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=(0, 2, 3, 4))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.conv_transpose3d(quant_input, quant_weight, bias=quant_conv_object.bias)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_against_unquantized(self):
|
||||
kernel_size = 3
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 32, 32, 32).cuda()
|
||||
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
fake_quant_conv3d = quant_conv.QuantConvTranspose3d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_input=QuantDescriptor(num_bits=16),
|
||||
quant_desc_weight=QuantDescriptor(num_bits=16, axis=(1)))
|
||||
|
||||
# Reset seed. Make sure weight and bias are the same
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
conv3d = nn.ConvTranspose3d(_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=True)
|
||||
|
||||
fake_quant_output = fake_quant_conv3d(test_input)
|
||||
output = conv3d(test_input)
|
||||
|
||||
test_utils.compare(fake_quant_output, output, rtol=1e-5, atol=2e-4)
|
||||
|
||||
|
||||
class TestQuantConvTranspose1D():
|
||||
|
||||
def test_no_quant(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 32)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_copy
|
||||
|
||||
out1 = F.conv_transpose1d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_quant_per_tensor(self):
|
||||
kernel_size = 8
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=QuantDescriptor())
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
test_input = torch.randn(256, _NUM_IN_CHANNELS, 32)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.conv_transpose1d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_quant_per_channel(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE1D_WEIGHT_PER_CHANNEL)
|
||||
quant_conv_object.input_quantizer.disable()
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 256)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=(0, 2))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.conv_transpose1d(test_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_input(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False)
|
||||
quant_conv_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(20, _NUM_IN_CHANNELS, 50)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.conv_transpose1d(quant_input, quant_conv_object.weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=False, quant_desc_weight=QuantDescriptor())
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.conv_transpose1d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_weight=QuantDescriptor(axis=(1)))
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=(0, 2))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.conv_transpose1d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_other_prec(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_desc_input = QuantDescriptor(num_bits=4)
|
||||
quant_desc_weight = QuantDescriptor(num_bits=3, axis=(1))
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=False,
|
||||
quant_desc_input=quant_desc_input,
|
||||
quant_desc_weight=quant_desc_weight)
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 16)
|
||||
|
||||
test_input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
weight_quantizer = TensorQuantizer(quant_desc_weight)
|
||||
|
||||
quant_input = test_input_quantizer(test_input)
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
quant_weight = weight_quantizer(weight_copy)
|
||||
|
||||
out1 = F.conv_transpose1d(quant_input, quant_weight)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_bias(self):
|
||||
kernel_size = 3
|
||||
|
||||
quant_conv_object = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_weight=QuantDescriptor(axis=(1)))
|
||||
test_input = torch.randn(2, _NUM_IN_CHANNELS, 2)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
weight_copy = quant_conv_object.weight.clone()
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=(0, 2))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.conv_transpose1d(quant_input, quant_weight, bias=quant_conv_object.bias)
|
||||
out2 = quant_conv_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_against_unquantized(self):
|
||||
kernel_size = 3
|
||||
test_input = torch.randn(16, _NUM_IN_CHANNELS, 24).cuda()
|
||||
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
fake_quant_conv1d = quant_conv.QuantConvTranspose1d(
|
||||
_NUM_IN_CHANNELS,
|
||||
_NUM_OUT_CHANNELS,
|
||||
kernel_size,
|
||||
bias=True,
|
||||
quant_desc_input=QuantDescriptor(num_bits=16),
|
||||
quant_desc_weight=QuantDescriptor(num_bits=16, axis=(1)))
|
||||
|
||||
# Reset seed. Make sure weight and bias are the same
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
conv1d = nn.ConvTranspose1d(_NUM_IN_CHANNELS, _NUM_OUT_CHANNELS, kernel_size, bias=True)
|
||||
|
||||
fake_quant_output = fake_quant_conv1d(test_input)
|
||||
output = conv1d(test_input)
|
||||
|
||||
test_utils.compare(fake_quant_output, output, rtol=1e-5, atol=1e-4)
|
||||
@@ -0,0 +1,199 @@
|
||||
#
|
||||
# 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 QuantInstanceNorm module.
|
||||
Mose tests check the functionality of all the combinations in Quant instancenorm against the corresponding functionalities in
|
||||
tensor_quant. There are tests for all the three QuantInstaceNorm1D, QuantInstanceNorm2D, and QuantInstanceNorm3D
|
||||
"""
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
from pytorch_quantization.nn.modules.tensor_quantizer import TensorQuantizer
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
from pytorch_quantization.nn.modules import quant_instancenorm
|
||||
#import tests.utils as test_utils
|
||||
|
||||
# make everything run on the GPU
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
|
||||
torch.backends.cudnn.deterministic = True
|
||||
|
||||
np.random.seed(1234)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
NUM_CHANNELS = 15
|
||||
|
||||
class TestQuantInstanceNorm1D():
|
||||
|
||||
def test_no_quant(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm1d(NUM_CHANNELS, affine=True)
|
||||
quant_instancenorm_object.input_quantizer.disable()
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128)
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(test_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm1d(NUM_CHANNELS, affine=True,
|
||||
quant_desc_input=QuantDescriptor())
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128)
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(quant_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm1d(NUM_CHANNELS, affine=True,
|
||||
quant_desc_input=QuantDescriptor(axis=(1)))
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128)
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input,
|
||||
torch.abs(test_input).max(0, keepdim=True)[0].max(2, keepdim=True)[0])
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(quant_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
|
||||
class TestQuantInstanceNorm2D():
|
||||
|
||||
def test_no_quant(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm2d(NUM_CHANNELS, affine=True)
|
||||
quant_instancenorm_object.input_quantizer.disable()
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128, 128)
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(test_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm2d(NUM_CHANNELS, affine=True,
|
||||
quant_desc_input=QuantDescriptor())
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128, 128)
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(quant_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm2d(NUM_CHANNELS, affine=True,
|
||||
quant_desc_input=QuantDescriptor(axis=(1)))
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128, 128)
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input,
|
||||
torch.abs(test_input).max(0, keepdim=True)[0].max(2, keepdim=True)[0].max(3, keepdim=True)[0])
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(quant_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
|
||||
|
||||
class TestQuantInstanceNorm3D():
|
||||
|
||||
def test_no_quant(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm3d(NUM_CHANNELS, affine=True)
|
||||
quant_instancenorm_object.input_quantizer.disable()
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128, 128, 128)
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(test_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm3d(NUM_CHANNELS, affine=True,
|
||||
quant_desc_input=QuantDescriptor())
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128, 128, 128)
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(quant_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel(self):
|
||||
|
||||
quant_instancenorm_object = quant_instancenorm.QuantInstanceNorm3d(NUM_CHANNELS, affine=True,
|
||||
quant_desc_input=QuantDescriptor(axis=(1)))
|
||||
|
||||
test_input = torch.randn(8, NUM_CHANNELS, 128, 128, 128)
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input,
|
||||
torch.abs(test_input).max(0, keepdim=True)[0].max(2, keepdim=True)[0]
|
||||
.max(3, keepdim=True)[0].max(4, keepdim=True)[0])
|
||||
|
||||
out1 = quant_instancenorm_object(test_input)
|
||||
out2 = F.instance_norm(quant_input,
|
||||
quant_instancenorm_object.running_mean,
|
||||
quant_instancenorm_object.running_var,
|
||||
quant_instancenorm_object.weight,
|
||||
quant_instancenorm_object.bias)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
@@ -0,0 +1,231 @@
|
||||
#
|
||||
# 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 QuantLinear module.
|
||||
Most tests check the functionality of all the combinations in Quant Linear against the corresponding functionalities
|
||||
in tensor_quant.
|
||||
"""
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization.nn.modules.tensor_quantizer import TensorQuantizer
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
from pytorch_quantization.nn.modules import quant_linear
|
||||
import tests.utils as test_utils
|
||||
|
||||
# make everything run on the GPU
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
|
||||
np.random.seed(1234)
|
||||
torch.manual_seed(1234)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
|
||||
class TestQuantLinear():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_linear_object = quant_linear.QuantLinear(
|
||||
7, 9, bias=False, quant_desc_weight=tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
#Quantizing weight
|
||||
def test_weight_fake_per_tensor(self):
|
||||
with torch.cuda.device(0):
|
||||
size = 256
|
||||
quant_linear_object = quant_linear.QuantLinear(
|
||||
size,
|
||||
size,
|
||||
bias=False,
|
||||
quant_desc_weight=tensor_quant.QuantDescriptor(axis=None))
|
||||
quant_linear_object.input_quantizer.disable()
|
||||
test_input = torch.randn(size, size)
|
||||
|
||||
weight_copy = quant_linear_object.weight.clone()
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.linear(test_input, quant_weight)
|
||||
out2 = quant_linear_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_weight_fake_per_channel(self):
|
||||
size_in = 255
|
||||
size_out = 257
|
||||
quant_linear_object = quant_linear.QuantLinear(
|
||||
size_in, size_out, bias=False,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW)
|
||||
quant_linear_object.input_quantizer.disable()
|
||||
test_input = torch.randn(32, size_in)
|
||||
|
||||
weight_copy = quant_linear_object.weight.clone()
|
||||
amax = quant_utils.reduce_amax(weight_copy, axis=1, keepdims=True)
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, amax)
|
||||
|
||||
out1 = F.linear(test_input, quant_weight)
|
||||
out2 = quant_linear_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
# Quantizing activations
|
||||
def test_test_input_fake_per_tensor(self):
|
||||
size_in = 255
|
||||
size_out = 257
|
||||
quant_linear_object = quant_linear.QuantLinear(
|
||||
size_in, size_out, bias=False)
|
||||
quant_linear_object.weight_quantizer.disable()
|
||||
test_input = torch.randn(32, size_in)
|
||||
|
||||
weight_copy = quant_linear_object.weight.clone()
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.linear(quant_input, weight_copy)
|
||||
out2 = quant_linear_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor(self):
|
||||
"""quantize everything, activations will scaled per tensor in ALL cases"""
|
||||
size_in = 255
|
||||
size_out = 257
|
||||
quant_linear_object = quant_linear.QuantLinear(
|
||||
size_in, size_out, bias=False, quant_desc_weight=tensor_quant.QuantDescriptor())
|
||||
test_input = torch.randn(32, size_in)
|
||||
|
||||
weight_copy = quant_linear_object.weight.clone()
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.linear(quant_input, quant_weight)
|
||||
out2 = quant_linear_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_tensor_with_bias(self):
|
||||
"""quantize everything, activations will scaled per tensor in ALL cases"""
|
||||
size_in = 255
|
||||
size_out = 257
|
||||
quant_linear_object = quant_linear.QuantLinear(
|
||||
size_in, size_out, bias=False, quant_desc_weight=tensor_quant.QuantDescriptor())
|
||||
test_input = torch.randn(32, 17, 93, size_in) # Test input other than 2 dimensional
|
||||
|
||||
weight_copy = quant_linear_object.weight.clone()
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy, torch.max(torch.abs(weight_copy)))
|
||||
|
||||
out1 = F.linear(quant_input, quant_weight, bias=quant_linear_object.bias)
|
||||
out2 = quant_linear_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel(self):
|
||||
"""quantize everything, activations will scaled per tensor in ALL cases"""
|
||||
size_in = 255
|
||||
size_out = 257
|
||||
quant_linear_object = quant_linear.QuantLinear(size_in, size_out, bias=False,
|
||||
quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW)
|
||||
test_input = torch.randn(32, size_in)
|
||||
|
||||
weight_copy = quant_linear_object.weight.clone()
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
quant_weight = tensor_quant.fake_tensor_quant(weight_copy,
|
||||
torch.max(torch.abs(weight_copy), dim=1, keepdim=True)[0])
|
||||
|
||||
out1 = F.linear(quant_input, quant_weight)
|
||||
out2 = quant_linear_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_per_channel_other_precs(self):
|
||||
"""Test some precisions other than 8bit."""
|
||||
size_in = 255
|
||||
size_out = 257
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=4)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=3)
|
||||
quant_linear_object = quant_linear.QuantLinear(
|
||||
size_in,
|
||||
size_out,
|
||||
bias=False,
|
||||
quant_desc_input=quant_desc_input,
|
||||
quant_desc_weight=quant_desc_weight)
|
||||
weight_quantizer = TensorQuantizer(quant_desc_weight)
|
||||
test_input_quantizer = TensorQuantizer(quant_desc_input)
|
||||
|
||||
test_input = torch.randn(32, size_in)
|
||||
|
||||
weight_copy = quant_linear_object.weight.clone()
|
||||
quant_input = test_input_quantizer(test_input)
|
||||
quant_weight = weight_quantizer(weight_copy)
|
||||
|
||||
out1 = F.linear(quant_input, quant_weight)
|
||||
out2 = quant_linear_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_fake_quant_against_unquantized(self):
|
||||
"""
|
||||
Quantized Linear should introduce bounded error compare to Linear
|
||||
"""
|
||||
size_in = 255
|
||||
size_out = 257
|
||||
test_input = torch.randn(32, size_in).cuda()
|
||||
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
quant_linear_layer = quant_linear.QuantLinear(
|
||||
size_in,
|
||||
size_out,
|
||||
bias=True,
|
||||
quant_desc_input=tensor_quant.QuantDescriptor(num_bits=16),
|
||||
quant_desc_weight=tensor_quant.QuantDescriptor(num_bits=16, axis=0))
|
||||
|
||||
# Reset seed. Make sure weight and bias are the same
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
linear_layer = nn.Linear(size_in, size_out, bias=True)
|
||||
|
||||
quant_out_features = quant_linear_layer(test_input)
|
||||
out_features = linear_layer(test_input)
|
||||
|
||||
# The difference between Linear and QuantLinear should be bounded in a range
|
||||
# Small values which become 0 after quantization lead to large relative errors. rtol and atol could be
|
||||
# much smaller without those values
|
||||
np.testing.assert_allclose(
|
||||
quant_out_features.detach().cpu().numpy(), out_features.detach().cpu().numpy(), rtol=0.01, atol=1e-4)
|
||||
|
||||
def test_set_default_quant_desc(self):
|
||||
quant_linear_layer = quant_linear.QuantLinear(32, 257)
|
||||
assert quant_linear_layer.input_quantizer.axis == None
|
||||
assert quant_linear_layer.weight_quantizer.axis == (0)
|
||||
|
||||
# set default to a different one
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=11)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=13, axis=1)
|
||||
quant_linear.Linear.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_linear.Linear.set_default_quant_desc_weight(quant_desc_weight)
|
||||
|
||||
# Create one with default descriptor
|
||||
quant_linear_layer = quant_linear.QuantLinear(32, 257)
|
||||
# Check quant_desc in quantizer created with default descriptor
|
||||
assert quant_linear_layer.input_quantizer.num_bits == quant_desc_input.num_bits
|
||||
assert quant_linear_layer.weight_quantizer.axis == quant_desc_weight.axis
|
||||
|
||||
def test_unused_kwargs(self):
|
||||
with pytest.raises(TypeError, match="Unused keys"):
|
||||
quant_linear_layer = quant_linear.QuantLinear(32, 257, descriptor='oops')
|
||||
@@ -0,0 +1,83 @@
|
||||
#
|
||||
# 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 Quant Module Replacement"""
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
|
||||
from pytorch_quantization import nn as quant_nn
|
||||
from pytorch_quantization import quant_modules
|
||||
from pytorch_quantization.quant_modules import QuantModuleReplacementHelper
|
||||
import tests.utils as test_utils
|
||||
from tests.fixtures import verbose
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
class TestQuantModuleReplace():
|
||||
|
||||
def test_simple_default_args(self):
|
||||
replacement_helper = QuantModuleReplacementHelper()
|
||||
replacement_helper.prepare_state()
|
||||
replacement_helper.apply_quant_modules()
|
||||
|
||||
# Linear module should not be replaced with its quantized version
|
||||
assert(type(quant_nn.QuantLinear(16, 256, 3)) == type(torch.nn.Linear(16, 256, 3)))
|
||||
assert(type(quant_nn.QuantConv2d(16, 256, 3)) == type(torch.nn.Conv2d(16, 256, 3)))
|
||||
|
||||
replacement_helper.restore_float_modules()
|
||||
|
||||
def test_with_no_replace_list(self):
|
||||
no_replace_list = ["Linear"]
|
||||
custom_quant_modules = None
|
||||
replacement_helper = QuantModuleReplacementHelper()
|
||||
replacement_helper.prepare_state(no_replace_list, custom_quant_modules)
|
||||
replacement_helper.apply_quant_modules()
|
||||
|
||||
# Linear module should not be replaced with its quantized version
|
||||
assert(type(quant_nn.QuantLinear(16, 256, 3)) != type(torch.nn.Linear(16, 256, 3)))
|
||||
assert(type(quant_nn.QuantConv2d(16, 256, 3)) == type(torch.nn.Conv2d(16, 256, 3)))
|
||||
|
||||
replacement_helper.restore_float_modules()
|
||||
|
||||
def test_with_custom_quant_modules(self):
|
||||
no_replace_list = ["Linear"]
|
||||
custom_quant_modules = [(torch.nn, "Linear", quant_nn.QuantLinear)]
|
||||
replacement_helper = QuantModuleReplacementHelper()
|
||||
replacement_helper.prepare_state(no_replace_list, custom_quant_modules)
|
||||
replacement_helper.apply_quant_modules()
|
||||
|
||||
# Although no replace list indicates Linear module should not be replaced with its
|
||||
# quantized version, since the custom_quant_modules still contains the Linear module's
|
||||
# mapping, it will replaced.
|
||||
assert(type(quant_nn.QuantLinear(16, 256, 3)) == type(torch.nn.Linear(16, 256, 3)))
|
||||
assert(type(quant_nn.QuantConv2d(16, 256, 3)) == type(torch.nn.Conv2d(16, 256, 3)))
|
||||
|
||||
replacement_helper.restore_float_modules()
|
||||
|
||||
def test_initialize_deactivate(self):
|
||||
no_replace_list = ["Linear"]
|
||||
custom_quant_modules = [(torch.nn, "Linear", quant_nn.QuantLinear)]
|
||||
|
||||
quant_modules.initialize(no_replace_list, custom_quant_modules)
|
||||
|
||||
assert(type(quant_nn.QuantLinear(16, 256, 3)) == type(torch.nn.Linear(16, 256, 3)))
|
||||
assert(type(quant_nn.QuantConv2d(16, 256, 3)) == type(torch.nn.Conv2d(16, 256, 3)))
|
||||
|
||||
quant_modules.deactivate()
|
||||
@@ -0,0 +1,321 @@
|
||||
#
|
||||
# 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 QuantPooling module.
|
||||
Most tests check the functionality of all the combinations in Quant Pooling against the corresponding functionalities
|
||||
in tensor_quant.
|
||||
"""
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization.nn.modules import quant_pooling
|
||||
|
||||
# make everything run on the GPU
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
|
||||
np.random.seed(1234)
|
||||
torch.manual_seed(1234)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
class TestQuantMaxPool1d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool1d(kernel_size=3, stride=1,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool1d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.max_pool1d(quant_input, 3, 1, 0, 1, False, False)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
class TestQuantMaxPool2d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool2d(kernel_size=3, stride=1,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool2d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.max_pool2d(quant_input, 3, 1, 0, 1, False, False)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_input_variable_bits(self):
|
||||
# Repeat checking the output for variable number of bits to QuantDescriptor
|
||||
for bits in [2, 4, 6]:
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=bits)
|
||||
|
||||
quant_pooling.QuantMaxPool2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool2d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)), bits)
|
||||
|
||||
out1 = F.max_pool2d(quant_input, 3, 1, 0, 1, False, False)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_input_fake_quant_disable(self):
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool2d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_pooling_object.input_quantizer.disable()
|
||||
|
||||
out1 = F.max_pool2d(test_input, 3, 1, 0, 1, False, False)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_input_multi_axis(self):
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=8, axis=(0, 1))
|
||||
|
||||
quant_pooling.QuantMaxPool2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool2d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(16, 7, 5, 5, dtype=torch.double)
|
||||
input_amax = torch.amax(torch.abs(test_input), dim=(2, 3), keepdim=True)
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, input_amax)
|
||||
|
||||
out1 = F.max_pool2d(quant_input, 3, 1, 0, 1, False, False)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
class TestQuantMaxPool3d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool3d(kernel_size=3, stride=1,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantMaxPool3d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(5, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.max_pool3d(quant_input, 3, 1, 0, 1, False, False)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
class TestQuantAvgPool1d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantAvgPool1d(kernel_size=3, stride=1,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantAvgPool1d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.avg_pool1d(quant_input, 3, 1, 0, False, True)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
class TestQuantAvgPool2d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantAvgPool2d(kernel_size=3, stride=1,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantAvgPool2d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.avg_pool2d(quant_input, 3, 1, 0, False, True, None)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_input_variable_bits(self):
|
||||
# Repeat checking the output for variable number of bits to QuantDescriptor
|
||||
for bits in [2, 4, 6]:
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=bits)
|
||||
|
||||
quant_pooling.QuantAvgPool2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_pooling_object = quant_pooling.QuantAvgPool2d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)), bits)
|
||||
|
||||
out1 = F.avg_pool2d(quant_input, 3, 1, 0, False, True, None)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_input_fake_quant_disable(self):
|
||||
quant_pooling_object = quant_pooling.QuantAvgPool2d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_pooling_object.input_quantizer.disable()
|
||||
|
||||
out1 = F.avg_pool2d(test_input, 3, 1, 0, False, True, None)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
class TestQuantAvgPool3d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantAvgPool3d(kernel_size=3, stride=1,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantAvgPool3d(kernel_size=3, stride=1)
|
||||
|
||||
test_input = torch.randn(5, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.avg_pool3d(quant_input, 3, 1, 0, False, True, None)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
class TestQuantAdaptiveAvgPool1d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantAdaptiveAvgPool1d(output_size=3,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantAdaptiveAvgPool1d(output_size=3)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.adaptive_avg_pool1d(quant_input, 3)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
class TestQuantAdaptiveAvgPool2d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantAdaptiveAvgPool2d(output_size=3,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantAdaptiveAvgPool2d(output_size=3)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.adaptive_avg_pool2d(quant_input, 3)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_input_variable_bits(self):
|
||||
# Repeat checking the output for variable number of bits to QuantDescriptor
|
||||
for bits in [2, 4, 6]:
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=bits)
|
||||
|
||||
quant_pooling.QuantAdaptiveAvgPool2d.set_default_quant_desc_input(quant_desc_input)
|
||||
quant_pooling_object = quant_pooling.QuantAdaptiveAvgPool2d(output_size=3)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)), bits)
|
||||
|
||||
out1 = F.adaptive_avg_pool2d(quant_input, 3)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
def test_input_fake_quant_disable(self):
|
||||
quant_pooling_object = quant_pooling.QuantAdaptiveAvgPool2d(output_size=3)
|
||||
|
||||
test_input = torch.randn(1, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_pooling_object.input_quantizer.disable()
|
||||
|
||||
out1 = F.adaptive_avg_pool2d(test_input, 3)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
|
||||
class TestQuantAdaptiveAvgPool3d():
|
||||
|
||||
def test_raise(self):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_pooling_object = quant_pooling.QuantAdaptiveAvgPool3d(output_size=3,
|
||||
quant_desc_input=
|
||||
tensor_quant.QuantDescriptor(fake_quant=False))
|
||||
assert "Only fake quantization is supported" in str(excinfo.value)
|
||||
|
||||
# Quantizing activations
|
||||
def test_input_fake_quant(self):
|
||||
quant_pooling_object = quant_pooling.QuantAdaptiveAvgPool3d(output_size=3)
|
||||
|
||||
test_input = torch.randn(5, 5, 5, 5, dtype=torch.double)
|
||||
|
||||
quant_input = tensor_quant.fake_tensor_quant(test_input, torch.max(torch.abs(test_input)))
|
||||
|
||||
out1 = F.adaptive_avg_pool3d(quant_input, 3)
|
||||
out2 = quant_pooling_object(test_input)
|
||||
np.testing.assert_array_equal(out1.detach().cpu().numpy(), out2.detach().cpu().numpy())
|
||||
@@ -0,0 +1,520 @@
|
||||
#
|
||||
# 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 QuantRNN module.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
import numpy as np
|
||||
|
||||
from pytorch_quantization.nn.modules import quant_rnn
|
||||
from pytorch_quantization import tensor_quant
|
||||
|
||||
from tests.fixtures import verbose
|
||||
|
||||
from . import utils
|
||||
|
||||
# make everything run on the GPU
|
||||
torch.set_default_tensor_type('torch.cuda.FloatTensor')
|
||||
# change default type to double if utils.compare flags a small error, may just be floating point rounding error
|
||||
# torch.set_default_tensor_type('torch.cuda.DoubleTensor')
|
||||
|
||||
np.random.seed(1234)
|
||||
torch.manual_seed(1234)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(1234)
|
||||
|
||||
# pylint: disable=no-self-use, missing-docstring, redefined-builtin, bad-continuation
|
||||
|
||||
# global state for saving/loading test vectors
|
||||
SAVE_VECTORS = 0
|
||||
VECTOR_FILE = 'tests/quant_rnn_test_vectors.pt'
|
||||
if SAVE_VECTORS:
|
||||
TEST_VECTORS = dict()
|
||||
else:
|
||||
TEST_VECTORS = torch.load(VECTOR_FILE)
|
||||
|
||||
|
||||
class TestQuantLSTMCell():
|
||||
"""
|
||||
tests for quant_rnn.QuantLSTMCell
|
||||
default parameters in QuantLSTMCell:
|
||||
bias=True,
|
||||
num_bits_weight=8, quant_mode_weight='per_channel',
|
||||
num_bits_input=8, quant_mode_input='per_tensor'
|
||||
|
||||
Tests of real quantization mode (nonfake) are disabled as it is not fully supported yet.
|
||||
"""
|
||||
|
||||
def test_basic_forward(self, verbose):
|
||||
"""Do a forward pass on the cell module and see if anything catches fire."""
|
||||
batch = 7
|
||||
input_size = 11
|
||||
hidden_size = 9
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=8)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=8, axis=(1,))
|
||||
quant_rnn_object = quant_rnn.QuantLSTMCell(input_size, hidden_size, bias=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
quant_rnn_object._input_quantizer.disable()
|
||||
quant_rnn_object._weight_quantizer.disable()
|
||||
|
||||
input = torch.randn(batch, input_size)
|
||||
hidden = torch.randn(batch, hidden_size)
|
||||
cell = torch.randn(batch, hidden_size)
|
||||
|
||||
quant_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
def test_no_quant_input_hidden(self, verbose):
|
||||
"""QuantLSTM with quantization disabled vs. pytorch LSTM for input and hidden inputs."""
|
||||
batch = 17
|
||||
input_size = 13
|
||||
hidden_size = 7
|
||||
|
||||
quant_rnn_object = quant_rnn.QuantLSTMCell(input_size, hidden_size, bias=False)
|
||||
quant_rnn_object._input_quantizer.disable()
|
||||
quant_rnn_object._weight_quantizer.disable()
|
||||
ref_rnn_object = nn.LSTMCell(input_size, hidden_size, bias=False)
|
||||
|
||||
# copy weights from one rnn to the other
|
||||
ref_rnn_object.load_state_dict(quant_rnn_object.state_dict())
|
||||
|
||||
input = torch.randn(batch, input_size)
|
||||
hidden = torch.randn(batch, hidden_size)
|
||||
cell = torch.randn(batch, hidden_size)
|
||||
|
||||
quant_hout, quant_cout = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_hout, ref_cout = ref_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
def test_no_quant_input_hidden_bias(self, verbose):
|
||||
"""QuantLSTMCell with quantization disabled vs. pytorch LSTMCell for input, hidden inputs and bias."""
|
||||
batch = 19
|
||||
input_size = 11
|
||||
hidden_size = 3
|
||||
|
||||
quant_rnn_object = quant_rnn.QuantLSTMCell(input_size, hidden_size, bias=True)
|
||||
quant_rnn_object._input_quantizer.disable()
|
||||
quant_rnn_object._weight_quantizer.disable()
|
||||
ref_rnn_object = nn.LSTMCell(input_size, hidden_size, bias=True)
|
||||
|
||||
# copy weights from one rnn to the other
|
||||
ref_rnn_object.load_state_dict(quant_rnn_object.state_dict())
|
||||
|
||||
input = torch.randn(batch, input_size)
|
||||
hidden = torch.randn(batch, hidden_size)
|
||||
cell = torch.randn(batch, hidden_size)
|
||||
|
||||
quant_hout, quant_cout = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_hout, ref_cout = ref_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
def test_against_unquantized(self, verbose):
|
||||
"""Quantization should introduce bounded error utils.compare to pytorch implementation."""
|
||||
batch = 9
|
||||
input_size = 13
|
||||
hidden_size = 7
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=16)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=16, axis=(1,))
|
||||
quant_rnn_object = quant_rnn.QuantLSTMCell(input_size, hidden_size, bias=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
ref_rnn_object = nn.LSTMCell(input_size, hidden_size, bias=False)
|
||||
|
||||
# copy weights from one rnn to the other
|
||||
ref_rnn_object.load_state_dict(quant_rnn_object.state_dict())
|
||||
|
||||
input = torch.randn(batch, input_size)
|
||||
hidden = torch.randn(batch, hidden_size)
|
||||
cell = torch.randn(batch, hidden_size)
|
||||
|
||||
quant_hout, quant_cout = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_hout, ref_cout = ref_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
# The difference between reference and quantized should be bounded in a range
|
||||
# Small values which become 0 after quantization lead to large relative errors. rtol and atol could be
|
||||
# much smaller without those values
|
||||
utils.compare(quant_hout, ref_hout, rtol=1e-4, atol=1e-4)
|
||||
utils.compare(quant_cout, ref_cout, rtol=1e-4, atol=1e-4)
|
||||
|
||||
# check that quantization introduces some error
|
||||
utils.assert_min_mse(quant_hout, ref_hout, tol=1e-20)
|
||||
utils.assert_min_mse(quant_cout, ref_cout, tol=1e-20)
|
||||
|
||||
def test_quant_input_hidden(self, verbose):
|
||||
"""QuantLSTMCell vs. manual input quantization + pytorchLSTMCell."""
|
||||
batch = 15
|
||||
input_size = 121
|
||||
hidden_size = 51
|
||||
num_bits = 4
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=num_bits)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=num_bits)
|
||||
quant_rnn_object = quant_rnn.QuantLSTMCell(input_size, hidden_size, bias=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
ref_rnn_object = nn.LSTMCell(input_size, hidden_size, bias=False)
|
||||
|
||||
input = torch.randn(batch, input_size)
|
||||
hidden = torch.randn(batch, hidden_size)
|
||||
cell = torch.randn(batch, hidden_size)
|
||||
|
||||
quant_hout, quant_cout = quant_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
quant_input, quant_hidden = utils.quantize_by_range_fused((input, hidden), num_bits)
|
||||
|
||||
utils.copy_state_and_quantize_fused(ref_rnn_object, quant_rnn_object, num_bits)
|
||||
|
||||
ref_hout, ref_cout = ref_rnn_object(quant_input, hx=(quant_hidden, cell))
|
||||
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
def test_quant_input_hidden_bias(self, verbose):
|
||||
"""QuantLSTMCell vs. manual input quantization + pytorchLSTMCell
|
||||
bias should not be quantized
|
||||
"""
|
||||
batch = 9
|
||||
input_size = 23
|
||||
hidden_size = 31
|
||||
num_bits = 7
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=num_bits)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=num_bits)
|
||||
quant_rnn_object = quant_rnn.QuantLSTMCell(input_size, hidden_size, bias=True,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
ref_rnn_object = nn.LSTMCell(input_size, hidden_size, bias=True)
|
||||
|
||||
input = torch.randn(batch, input_size)
|
||||
hidden = torch.randn(batch, hidden_size)
|
||||
cell = torch.randn(batch, hidden_size)
|
||||
|
||||
quant_hout, quant_cout = quant_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
quant_input, quant_hidden = utils.quantize_by_range_fused((input, hidden), num_bits)
|
||||
|
||||
utils.copy_state_and_quantize_fused(ref_rnn_object, quant_rnn_object, num_bits)
|
||||
|
||||
ref_hout, ref_cout = ref_rnn_object(quant_input, hx=(quant_hidden, cell))
|
||||
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
def test_quant_different_prec(self, verbose):
|
||||
"""QuantLSTMCell vs. manual input quantization + pytorch LSTMCell
|
||||
different input and weight precisions
|
||||
"""
|
||||
batch = 27
|
||||
input_size = 11
|
||||
hidden_size = 10
|
||||
num_bits_weight = 4
|
||||
num_bits_input = 8
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=num_bits_input)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=num_bits_weight)
|
||||
quant_rnn_object = quant_rnn.QuantLSTMCell(input_size, hidden_size, bias=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
ref_rnn_object = nn.LSTMCell(input_size, hidden_size, bias=False)
|
||||
|
||||
input = torch.randn(batch, input_size)
|
||||
hidden = torch.randn(batch, hidden_size)
|
||||
cell = torch.randn(batch, hidden_size)
|
||||
|
||||
quant_hout, quant_cout = quant_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
quant_input, quant_hidden = utils.quantize_by_range_fused((input, hidden), num_bits_input)
|
||||
|
||||
utils.copy_state_and_quantize_fused(ref_rnn_object, quant_rnn_object, num_bits_weight)
|
||||
|
||||
ref_hout, ref_cout = ref_rnn_object(quant_input, hx=(quant_hidden, cell))
|
||||
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
|
||||
class TestQuantLSTM():
|
||||
"""
|
||||
tests for quant_rnn.QuantLSTM
|
||||
default parameters in QuantLSTM:
|
||||
bias=True,
|
||||
quant_weight=True, bits_weight=8, fake_quantTrue, quant_mode_weight='channel',
|
||||
quant_input=True, bits_acts=8, quant_mode_input='tensor'
|
||||
|
||||
Tests of real quantization mode (nonfake) are disabled as it is not fully supported yet.
|
||||
"""
|
||||
|
||||
def test_basic_forward(self, verbose):
|
||||
"""Do a forward pass on the layer module and see if anything catches fire."""
|
||||
batch = 5
|
||||
input_size = 13
|
||||
hidden_size = 31
|
||||
seq_len = 1
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=8)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=8, axis=(1,))
|
||||
quant_rnn_object = quant_rnn.QuantLSTM(input_size, hidden_size,
|
||||
num_layers=1, bias=False, batch_first=False, dropout=0, bidirectional=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
input = torch.randn(seq_len, batch, input_size)
|
||||
hidden = torch.randn(seq_len, batch, hidden_size)
|
||||
cell = torch.randn(seq_len, batch, hidden_size)
|
||||
quant_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
def test_no_quant(self, verbose):
|
||||
"""QuantLSTM with quantization disabled vs. pytorch LSTM."""
|
||||
batch = 11
|
||||
input_size = 14
|
||||
hidden_size = 22
|
||||
seq_len = 1
|
||||
quant_rnn_object = quant_rnn.QuantLSTM(input_size, hidden_size,
|
||||
num_layers=1, bias=False, batch_first=False, dropout=0, bidirectional=False)
|
||||
quant_rnn_object._input_quantizers[0].disable()
|
||||
quant_rnn_object._weight_quantizers[0].disable()
|
||||
ref_rnn_object = nn.LSTM(input_size, hidden_size,
|
||||
num_layers=1, bias=False, batch_first=False, dropout=0, bidirectional=False)
|
||||
|
||||
# copy weights from one rnn to the other
|
||||
ref_rnn_object.load_state_dict(quant_rnn_object.state_dict())
|
||||
|
||||
input = torch.randn(seq_len, batch, input_size)
|
||||
hidden = torch.randn(seq_len, batch, hidden_size)
|
||||
cell = torch.randn(seq_len, batch, hidden_size)
|
||||
|
||||
quant_out, (quant_hout, quant_cout) = quant_rnn_object(input)
|
||||
ref_out, (ref_hout, ref_cout) = ref_rnn_object(input)
|
||||
|
||||
utils.compare(quant_out, ref_out)
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
def test_no_quant_input_hidden(self, verbose):
|
||||
"""QuantLSTM with quantization disabled vs. pytorch LSTM for input and hidden inputs."""
|
||||
batch = 13
|
||||
input_size = 19
|
||||
hidden_size = 20
|
||||
seq_len = 1
|
||||
|
||||
quant_rnn_object = quant_rnn.QuantLSTM(input_size, hidden_size,
|
||||
num_layers=1, bias=False, batch_first=False, dropout=0, bidirectional=False)
|
||||
quant_rnn_object._input_quantizers[0].disable()
|
||||
quant_rnn_object._weight_quantizers[0].disable()
|
||||
ref_rnn_object = nn.LSTM(input_size, hidden_size,
|
||||
num_layers=1, bias=False, batch_first=False, dropout=0, bidirectional=False)
|
||||
|
||||
# copy weights from one rnn to the other
|
||||
ref_rnn_object.load_state_dict(quant_rnn_object.state_dict())
|
||||
|
||||
input = torch.randn(seq_len, batch, input_size)
|
||||
hidden = torch.randn(seq_len, batch, hidden_size)
|
||||
cell = torch.randn(seq_len, batch, hidden_size)
|
||||
|
||||
quant_out, (quant_hout, quant_cout) = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_out, (ref_hout, ref_cout) = ref_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
utils.compare(quant_out, ref_out)
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
def test_no_quant_all_modes(self, verbose):
|
||||
"""QuantLSTM with quantization disabled vs. pytorch LSTM for all modes."""
|
||||
|
||||
def testcase(input_size, hidden_size, seq_len, batch, num_layers, bias, batch_first, dropout, bidirectional):
|
||||
|
||||
quant_rnn_object = quant_rnn.QuantLSTM(input_size, hidden_size,
|
||||
num_layers=num_layers, bias=bias, batch_first=batch_first, dropout=dropout,
|
||||
bidirectional=bidirectional)
|
||||
|
||||
num_quantizers = num_layers * 2 if bidirectional else num_layers
|
||||
for i in range(num_quantizers):
|
||||
quant_rnn_object._input_quantizers[i].disable()
|
||||
quant_rnn_object._weight_quantizers[i].disable()
|
||||
|
||||
ref_rnn_object = nn.LSTM(input_size, hidden_size,
|
||||
num_layers=num_layers, bias=bias, batch_first=batch_first, dropout=dropout,
|
||||
bidirectional=bidirectional)
|
||||
|
||||
# copy state from one rnn to the other
|
||||
ref_rnn_object.load_state_dict(quant_rnn_object.state_dict())
|
||||
|
||||
input = torch.randn(seq_len, batch, input_size)
|
||||
num_directions = 2 if bidirectional else 1
|
||||
hidden = torch.randn(num_layers*num_directions, batch, hidden_size)
|
||||
cell = torch.randn(num_layers*num_directions, batch, hidden_size)
|
||||
|
||||
quant_out, (quant_hout, quant_cout) = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_out, (ref_hout, ref_cout) = ref_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
utils.compare(quant_out, ref_out)
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
# test various permuatations of the following parameters:
|
||||
# size, num_layers, bias, batch_first, dropout, bidirectional
|
||||
testcase(32, 27, 1, 1, 1, False, False, 0, False)
|
||||
testcase(19, 63, 1, 1, 2, False, False, 0, False)
|
||||
testcase(11, 41, 1, 1, 1, True, False, 0, False)
|
||||
testcase(33, 31, 1, 1, 1, False, True, 0, False)
|
||||
# testcase(32, 32, 1, 1, 2, False, False, 0.5, False) #TODO(pjudd) this fails look into dropout seeding
|
||||
testcase(73, 13, 1, 1, 1, False, False, 0, True)
|
||||
|
||||
def test_against_unquantized(self, verbose):
|
||||
"""Quantization should introduce bounded error utils.compare to pytorch implementation."""
|
||||
batch = 21
|
||||
input_size = 33
|
||||
hidden_size = 25
|
||||
seq_len = 1
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=16)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=16, axis=(1,))
|
||||
quant_rnn_object = quant_rnn.QuantLSTM(input_size, hidden_size,
|
||||
num_layers=1, bias=False, batch_first=False, dropout=0, bidirectional=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
ref_rnn_object = nn.LSTM(input_size, hidden_size,
|
||||
num_layers=1, bias=False, batch_first=False, dropout=0, bidirectional=False)
|
||||
|
||||
# copy weights from one rnn to the other
|
||||
ref_rnn_object.load_state_dict(quant_rnn_object.state_dict())
|
||||
|
||||
input = torch.randn(seq_len, batch, input_size)
|
||||
hidden = torch.randn(seq_len, batch, hidden_size)
|
||||
cell = torch.randn(seq_len, batch, hidden_size)
|
||||
|
||||
quant_out, (quant_hout, quant_cout) = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_out, (ref_hout, ref_cout) = ref_rnn_object(input, hx=(hidden, cell))
|
||||
|
||||
# The difference between reference and quantized should be bounded in a range
|
||||
# Small values which become 0 after quantization lead to large relative errors. rtol and atol could be
|
||||
# much smaller without those values
|
||||
utils.compare(quant_out, ref_out, rtol=1e-4, atol=1e-4)
|
||||
utils.compare(quant_hout, ref_hout, rtol=1e-4, atol=1e-4)
|
||||
utils.compare(quant_cout, ref_cout, rtol=1e-4, atol=1e-4)
|
||||
|
||||
# check that quantization introduces some error
|
||||
utils.assert_min_mse(quant_out, ref_out, tol=1e-20)
|
||||
utils.assert_min_mse(quant_hout, ref_hout, tol=1e-20)
|
||||
utils.assert_min_mse(quant_cout, ref_cout, tol=1e-20)
|
||||
|
||||
def test_quant_input_hidden(self, verbose):
|
||||
"""QuantLSTM vs. manual input quantization + pytorchLSTM."""
|
||||
batch = 13
|
||||
input_size = 17
|
||||
hidden_size = 7
|
||||
seq_len = 1
|
||||
num_bits = 6
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=num_bits)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=num_bits)
|
||||
quant_rnn_object = quant_rnn.QuantLSTM(input_size, hidden_size, num_layers=1, bias=False,
|
||||
batch_first=False, dropout=0, bidirectional=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
ref_rnn_object = nn.LSTM(input_size, hidden_size, num_layers=1, bias=False,
|
||||
batch_first=False, dropout=0, bidirectional=False)
|
||||
|
||||
input = torch.randn(seq_len, batch, input_size)
|
||||
hidden = torch.randn(seq_len, batch, hidden_size)
|
||||
cell = torch.randn(seq_len, batch, hidden_size)
|
||||
|
||||
quant_input, quant_hidden = utils.quantize_by_range_fused((input, hidden), num_bits)
|
||||
|
||||
utils.copy_state_and_quantize_fused(ref_rnn_object, quant_rnn_object, num_bits)
|
||||
|
||||
quant_out, (quant_hout, quant_cout) = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_out, (ref_hout, ref_cout) = ref_rnn_object(quant_input, hx=(quant_hidden, cell))
|
||||
|
||||
utils.compare(quant_out, ref_out)
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
def test_quant_input_hidden_bias(self, verbose):
|
||||
"""QuantLSTM vs. manual input quantization + pytorchLSTM."""
|
||||
batch = 17
|
||||
input_size = 13
|
||||
hidden_size = 7
|
||||
seq_len = 1
|
||||
num_bits = 5
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=num_bits)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=num_bits)
|
||||
quant_rnn_object = quant_rnn.QuantLSTM(input_size, hidden_size, num_layers=1, bias=True,
|
||||
batch_first=False, dropout=0, bidirectional=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
ref_rnn_object = nn.LSTM(input_size, hidden_size, num_layers=1, bias=True,
|
||||
batch_first=False, dropout=0, bidirectional=False)
|
||||
|
||||
input = torch.randn(seq_len, batch, input_size)
|
||||
hidden = torch.randn(seq_len, batch, hidden_size)
|
||||
cell = torch.randn(seq_len, batch, hidden_size)
|
||||
|
||||
quant_input, quant_hidden = utils.quantize_by_range_fused((input, hidden), num_bits)
|
||||
|
||||
utils.copy_state_and_quantize_fused(ref_rnn_object, quant_rnn_object, num_bits)
|
||||
|
||||
quant_out, (quant_hout, quant_cout) = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_out, (ref_hout, ref_cout) = ref_rnn_object(quant_input, hx=(quant_hidden, cell))
|
||||
|
||||
utils.compare(quant_out, ref_out)
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
def test_quant_different_prec(self, verbose):
|
||||
"""QuantLSTM vs. manual input quantization + pytorchLSTM."""
|
||||
batch = 22
|
||||
input_size = 23
|
||||
hidden_size = 24
|
||||
seq_len = 1
|
||||
num_bits_weight = 4
|
||||
num_bits_input = 8
|
||||
|
||||
quant_desc_input = tensor_quant.QuantDescriptor(num_bits=num_bits_input)
|
||||
quant_desc_weight = tensor_quant.QuantDescriptor(num_bits=num_bits_weight)
|
||||
quant_rnn_object = quant_rnn.QuantLSTM(input_size, hidden_size, num_layers=1, bias=False,
|
||||
batch_first=False, dropout=0, bidirectional=False,
|
||||
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
|
||||
ref_rnn_object = nn.LSTM(input_size, hidden_size, num_layers=1, bias=False,
|
||||
batch_first=False, dropout=0, bidirectional=False)
|
||||
|
||||
input = torch.randn(seq_len, batch, input_size)
|
||||
hidden = torch.randn(seq_len, batch, hidden_size)
|
||||
cell = torch.randn(seq_len, batch, hidden_size)
|
||||
|
||||
quant_input, quant_hidden = utils.quantize_by_range_fused((input, hidden), num_bits_input)
|
||||
|
||||
utils.copy_state_and_quantize_fused(ref_rnn_object, quant_rnn_object, num_bits_weight)
|
||||
|
||||
quant_out, (quant_hout, quant_cout) = quant_rnn_object(input, hx=(hidden, cell))
|
||||
ref_out, (ref_hout, ref_cout) = ref_rnn_object(quant_input, hx=(quant_hidden, cell))
|
||||
|
||||
utils.compare(quant_out, ref_out)
|
||||
utils.compare(quant_hout, ref_hout)
|
||||
utils.compare(quant_cout, ref_cout)
|
||||
|
||||
|
||||
class TestEpilogue():
|
||||
"""Run after all tests to save globals."""
|
||||
|
||||
def test_save_vectors(self, verbose):
|
||||
"""Save test vectors to file."""
|
||||
if SAVE_VECTORS:
|
||||
torch.save(TEST_VECTORS, VECTOR_FILE)
|
||||
raise Exception('Saved test vectors to {}, for testing set SAVE_VECTORS = 0'.format(VECTOR_FILE))
|
||||
Binary file not shown.
@@ -0,0 +1,52 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Test pytorch_quantization.utils"""
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
from tests.fixtures import verbose
|
||||
|
||||
np.random.seed(12345)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
class TestQuantUtils():
|
||||
|
||||
def test_reduce_amax(self):
|
||||
x_np = (np.random.rand(3, 7, 11, 13, 17) - 0.1).astype(np.float32)
|
||||
x_torch = torch.tensor(x_np)
|
||||
|
||||
# Test reduce to one value
|
||||
amax_np = np.max(np.abs(x_np))
|
||||
amax_torch = quant_utils.reduce_amax(x_torch)
|
||||
np.testing.assert_array_equal(amax_np, amax_torch.cpu().numpy())
|
||||
|
||||
# Test different axis
|
||||
axes = [(1, 2, 3), (0, 2, 3), (0, 3), (0, 1, 3, 4)]
|
||||
for axis in axes:
|
||||
keepdims = np.random.rand() > 0.5
|
||||
amax_np = np.max(np.abs(x_np), axis=axis, keepdims=keepdims)
|
||||
amax_torch = quant_utils.reduce_amax(x_torch, axis=axis, keepdims=keepdims)
|
||||
np.testing.assert_array_almost_equal(amax_np, amax_torch.cpu().numpy())
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quant_utils.reduce_amax(x_torch, axis=(0, 1, 2, 3, 4, 5))
|
||||
assert "Cannot reduce more axes" in str(excinfo.value)
|
||||
@@ -0,0 +1,482 @@
|
||||
#
|
||||
# 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())
|
||||
@@ -0,0 +1,278 @@
|
||||
#
|
||||
# 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 quantizer"""
|
||||
import contextlib
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
from pytorch_quantization.nn.modules import tensor_quantizer
|
||||
from pytorch_quantization import utils as quant_utils
|
||||
import tests.utils as test_utils
|
||||
from tests.fixtures import verbose
|
||||
|
||||
np.random.seed(12345)
|
||||
|
||||
# pylint:disable=missing-docstring, no-self-use
|
||||
|
||||
|
||||
class TestTensorQuantizer():
|
||||
|
||||
def test_simple_run(self):
|
||||
"""Quantizer calls fake_tensor_quant by default"""
|
||||
x = torch.randn(3, 7).cuda()
|
||||
amax_x = torch.max(torch.abs(x))
|
||||
fn_quant_x = tensor_quant.fake_tensor_quant(x, amax_x)
|
||||
quantizer = tensor_quantizer.TensorQuantizer()
|
||||
module_quant_x = quantizer(x)
|
||||
np.testing.assert_array_equal(fn_quant_x.cpu().numpy(), module_quant_x.cpu().numpy())
|
||||
|
||||
def test_simple_run_no_fake(self):
|
||||
"""Quantizer fake_quant=False calls tensor_quant and sets the scale property"""
|
||||
x = torch.randn(3, 7).cuda()
|
||||
amax_x = torch.max(torch.abs(x))
|
||||
fn_quant_x, fn_scale = tensor_quant.tensor_quant(x, amax_x)
|
||||
quantizer = tensor_quantizer.TensorQuantizer(tensor_quant.QuantDescriptor(num_bits=8, fake_quant=False))
|
||||
module_quant_x = quantizer(x)
|
||||
module_scale = quantizer.scale
|
||||
np.testing.assert_array_equal(fn_quant_x.cpu().numpy(), module_quant_x.cpu().numpy())
|
||||
np.testing.assert_array_equal(fn_scale.cpu().numpy(), module_scale.cpu().numpy())
|
||||
|
||||
def test_per_tensor_scale(self):
|
||||
"""Quantizer performs expected quantization"""
|
||||
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)))
|
||||
quantizer = tensor_quantizer.TensorQuantizer(tensor_quant.QuantDescriptor(num_bits=8, fake_quant=False))
|
||||
module_quant_x = quantizer(x_torch)
|
||||
np.testing.assert_array_equal(module_quant_x.cpu().numpy(), quant_x_np)
|
||||
|
||||
def test_per_channel_scale(self, verbose):
|
||||
"""Quantizer performs per channel scaling"""
|
||||
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)
|
||||
|
||||
quant_x_np = test_utils.quant_np(x_np, amax_x_np)
|
||||
quantizer = tensor_quantizer.TensorQuantizer(
|
||||
tensor_quant.QuantDescriptor(num_bits=8, axis=(0), fake_quant=False, scale_amax=0.7))
|
||||
quantizer.cuda()
|
||||
module_quant_x = quantizer(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
|
||||
error = np.abs(module_quant_x.cpu().numpy() - quant_x_np)
|
||||
np.testing.assert_array_less(error, 2)
|
||||
if verbose:
|
||||
mismatches = np.where(error >= 1)
|
||||
print("Mismatches:")
|
||||
print(" Original: ", x_np[mismatches])
|
||||
print(" numpy: ", quant_x_np[mismatches])
|
||||
print(" TensorQuantizer: ", module_quant_x.cpu().numpy()[mismatches])
|
||||
|
||||
def test_learn_amax(self):
|
||||
"""Test the clip implied by learn_amax"""
|
||||
x_np = np.random.rand(1023).astype(np.float32)
|
||||
x_torch = torch.Tensor(x_np).cuda()
|
||||
amax = 0.5
|
||||
quant_x_np = test_utils.quant_np(x_np, 0.5, fake=True)
|
||||
quantizer = tensor_quantizer.TensorQuantizer(
|
||||
tensor_quant.QuantDescriptor(num_bits=8, amax=amax, learn_amax=True)).cuda()
|
||||
assert hasattr(quantizer, 'clip')
|
||||
module_quant_x = quantizer(x_torch)
|
||||
np.testing.assert_array_equal(module_quant_x.cpu().detach().numpy(), quant_x_np)
|
||||
|
||||
def test_clip_mode(self):
|
||||
"""Test the clip stage only"""
|
||||
x_np = np.random.rand(1023).astype(np.float32)
|
||||
x_torch = torch.Tensor(x_np).cuda()
|
||||
amax = 0.5
|
||||
clip_x_np = np.clip(x_np, -amax, amax)
|
||||
quantizer = tensor_quantizer.TensorQuantizer(tensor_quant.QuantDescriptor(amax=amax, learn_amax=True),
|
||||
if_quant=False,
|
||||
if_clip=True).cuda()
|
||||
assert hasattr(quantizer, 'clip')
|
||||
module_clip_x = quantizer(x_torch)
|
||||
np.testing.assert_array_equal(module_clip_x.cpu().detach().numpy(), clip_x_np)
|
||||
|
||||
def test_scale_amax(self):
|
||||
x_np = np.random.rand(1023).astype(np.float32)
|
||||
x_torch = torch.Tensor(x_np).cuda()
|
||||
amax = 0.5
|
||||
scale_amax = 0.9
|
||||
quant_x_np = test_utils.quant_np(x_np, amax * scale_amax, fake=True)
|
||||
quantizer = tensor_quantizer.TensorQuantizer(
|
||||
tensor_quant.QuantDescriptor(num_bits=8, amax=amax, scale_amax=scale_amax)).cuda()
|
||||
module_quant_x = quantizer(x_torch)
|
||||
np.testing.assert_array_equal(module_quant_x.cpu().detach().numpy(), quant_x_np)
|
||||
|
||||
# Test twice. There was a but in scale amax logic that modify the amax every time
|
||||
module_quant_x = quantizer(x_torch)
|
||||
np.testing.assert_array_equal(module_quant_x.cpu().detach().numpy(), quant_x_np)
|
||||
|
||||
def test_disable(self):
|
||||
x = torch.randn(3, 7).cuda()
|
||||
amax_x = torch.max(torch.abs(x))
|
||||
quantizer = tensor_quantizer.TensorQuantizer(disabled=True).cuda()
|
||||
module_quant_x = quantizer(x)
|
||||
np.testing.assert_array_equal(x.cpu().numpy(), module_quant_x.cpu().numpy())
|
||||
|
||||
def test_state_loading(self):
|
||||
"""Test quant_desc loading via state_dict"""
|
||||
amax = [3.142, 2.718]
|
||||
quant_desc1 = tensor_quant.QuantDescriptor(amax=amax)
|
||||
quantizer1 = tensor_quantizer.TensorQuantizer(quant_desc1)
|
||||
|
||||
# copy state
|
||||
quantizer1.load_state_dict(quantizer1.state_dict())
|
||||
np.testing.assert_array_equal(quantizer1.amax.detach().cpu().numpy(), quant_desc1.amax)
|
||||
|
||||
def test_properties(self):
|
||||
quant_desc1 = tensor_quant.QuantDescriptor(amax=3.14)
|
||||
quantizer1 = tensor_quantizer.TensorQuantizer(quant_desc1)
|
||||
quantizer1.amax = 0.577
|
||||
|
||||
assert quantizer1.amax.detach().cpu().numpy() == np.float32(0.577)
|
||||
np.testing.assert_array_equal(quantizer1.amax.detach().cpu().numpy(), quantizer1.amax)
|
||||
assert quantizer1.step_size == 0.577 / 127.
|
||||
|
||||
quant_desc2 = tensor_quant.QuantDescriptor()
|
||||
quantizer2 = tensor_quantizer.TensorQuantizer(quant_desc2)
|
||||
amax_np = np.array([3.142, 2.718], dtype=np.float32)
|
||||
quantizer2.amax = amax_np
|
||||
np.testing.assert_array_equal(quantizer2.amax.detach().cpu().numpy(), amax_np)
|
||||
|
||||
quant_desc3 = tensor_quant.QuantDescriptor()
|
||||
quantizer3 = tensor_quantizer.TensorQuantizer(quant_desc3)
|
||||
assert quantizer3.amax is None
|
||||
|
||||
def test_init_calib(self):
|
||||
quant_desc2 = tensor_quant.QuantDescriptor(axis=(0, 1))
|
||||
quantizer2 = tensor_quantizer.TensorQuantizer(quant_desc2, if_calib=True, if_quant=False).cuda()
|
||||
|
||||
x_2 = torch.rand(127, 63, 7, 7).cuda()
|
||||
quantizer2(x_2)
|
||||
quantizer2.load_calib_amax()
|
||||
|
||||
assert quantizer2.amax.numel() == 127 * 63
|
||||
|
||||
def test_max_calib(self):
|
||||
axis = 0
|
||||
reduce_axis = (1, 2, 3)
|
||||
quant_desc1 = tensor_quant.QuantDescriptor(axis=axis)
|
||||
quantizer1 = tensor_quantizer.TensorQuantizer(quant_desc1).cuda()
|
||||
quantizer1.enable_calib()
|
||||
|
||||
quant_desc1 = tensor_quant.QuantDescriptor(axis=axis)
|
||||
quantizer1 = tensor_quantizer.TensorQuantizer(quant_desc1).cuda()
|
||||
quantizer1.enable_calib()
|
||||
|
||||
with pytest.raises(RuntimeError, match="Calibrator returned None"):
|
||||
quantizer1.load_calib_amax()
|
||||
|
||||
x_1 = torch.rand(127, 63, 7, 7).cuda()
|
||||
x_2 = torch.rand(127, 63, 7, 7).cuda()
|
||||
quantizer1(x_1)
|
||||
quantizer1(x_2)
|
||||
quantizer1.disable_calib()
|
||||
|
||||
global_amax = torch.max(quant_utils.reduce_amax(x_1, axis=reduce_axis, keepdims=True),
|
||||
quant_utils.reduce_amax(x_2, axis=reduce_axis, keepdims=True))
|
||||
test_utils.compare(quantizer1._calibrator.compute_amax(), global_amax, atol=0, rtol=0, ctol=0)
|
||||
|
||||
quantizer1.load_calib_amax()
|
||||
test_utils.compare(quantizer1.amax, global_amax, atol=0, rtol=0, ctol=0)
|
||||
|
||||
quant_desc2 = tensor_quant.QuantDescriptor(learn_amax=True)
|
||||
quantizer2 = tensor_quantizer.TensorQuantizer(quant_desc2).cuda()
|
||||
quantizer2.enable_calib()
|
||||
quantizer2(x_1)
|
||||
quantizer2(x_2)
|
||||
|
||||
quantizer2.load_calib_amax()
|
||||
quantizer2.init_learn_amax()
|
||||
test_utils.compare(quantizer2.clip.clip_value_min, -torch.max(global_amax), atol=0, rtol=0, ctol=0)
|
||||
test_utils.compare(quantizer2.clip.clip_value_max, torch.max(global_amax), atol=0, rtol=0, ctol=0)
|
||||
|
||||
def test_entropy_and_percentile_calib(self):
|
||||
"""Don't really have a good way to test it."""
|
||||
quant_desc1 = tensor_quant.QuantDescriptor(calib_method='histogram')
|
||||
quantizer1 = tensor_quantizer.TensorQuantizer(quant_desc1, if_calib=True, if_quant=False).cuda()
|
||||
|
||||
x_1 = torch.rand(3, 63, 7, 7).cuda()
|
||||
x_2 = torch.rand(3, 63, 7, 7).cuda()
|
||||
quantizer1(x_1)
|
||||
quantizer1(x_2)
|
||||
|
||||
quantizer1.load_calib_amax("entropy")
|
||||
test_utils.compare(quantizer1._calibrator.compute_amax("entropy"), quantizer1.amax, atol=0, rtol=0, ctol=0)
|
||||
quantizer1._calibrator.reset()
|
||||
|
||||
quantizer1(x_1)
|
||||
quantizer1(x_2)
|
||||
|
||||
quantizer1.load_calib_amax("percentile", percentile=99.99)
|
||||
test_utils.compare(quantizer1._calibrator.compute_amax("percentile", percentile=99.99),
|
||||
quantizer1.amax,
|
||||
atol=0,
|
||||
rtol=0,
|
||||
ctol=0)
|
||||
|
||||
def test_setters(self):
|
||||
quantizer = tensor_quantizer.TensorQuantizer()
|
||||
quantizer.num_bits = 7
|
||||
quantizer.unsigned = True
|
||||
|
||||
assert quantizer.num_bits == 7
|
||||
assert quantizer.unsigned
|
||||
|
||||
def test_pre_quant_scale(self):
|
||||
quant_desc = tensor_quant.QuantDescriptor(axis=1, num_bits=8, amax=127.0)
|
||||
quantizer = tensor_quantizer.TensorQuantizer(quant_desc).cuda()
|
||||
quantizer2 = tensor_quantizer.TensorQuantizer(quant_desc).cuda()
|
||||
|
||||
inputs = torch.Tensor([[0, 0.4, 1.1, 2.0]]).cuda()
|
||||
outputs_gt = torch.Tensor([[0, 0, 1, 2]]).cuda()
|
||||
assert torch.allclose(quantizer(inputs), outputs_gt)
|
||||
|
||||
quantizer.pre_quant_scale = 2.0
|
||||
outputs_gt = torch.Tensor([[0, 1, 2, 4]]).cuda()
|
||||
assert torch.allclose(quantizer(inputs), outputs_gt)
|
||||
|
||||
quantizer2.pre_quant_scale = torch.Tensor([[1.0, 2.0, 3.0, 4.0]]).cuda()
|
||||
outputs_gt = torch.Tensor([[0, 1, 3, 8]]).cuda()
|
||||
assert torch.allclose(quantizer2(inputs), outputs_gt)
|
||||
|
||||
@pytest.mark.parametrize("E, M, axis", [(5, 2, None), (4, 3, None), (4, 3, 1), (7, 3, None)])
|
||||
def test_e4m3(self, E, M, axis):
|
||||
is_error_expected = (E != 4 or M != 3)
|
||||
with (pytest.raises(TypeError)
|
||||
if is_error_expected else contextlib.nullcontext()):
|
||||
e4m3_desc = tensor_quant.QuantDescriptor(num_bits=(E, M), axis=axis)
|
||||
e4m3_quantizer = tensor_quantizer.TensorQuantizer(e4m3_desc).to("cuda")
|
||||
|
||||
x = torch.rand(3, 63, 7, 7, device="cuda")
|
||||
|
||||
e4m3_x = e4m3_quantizer(x)
|
||||
ref = tensor_quant.scaled_e4m3(x, e4m3_quantizer._get_amax(x), E, M)
|
||||
test_utils.compare(e4m3_x, ref, atol=0, rtol=0)
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#
|
||||
# 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 for ONNX export."""
|
||||
import io
|
||||
import onnxruntime
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
# ORT output correctness tests sometimes fails due to random seed.
|
||||
# It needs to be investigated closer
|
||||
torch.manual_seed(0)
|
||||
|
||||
import tests.utils as test_utils
|
||||
import torch.nn as nn
|
||||
import pytorch_quantization
|
||||
from pytorch_quantization.nn import QuantLinear
|
||||
from pytorch_quantization.tensor_quant import QuantDescriptor
|
||||
|
||||
|
||||
class MyModel(nn.Module):
|
||||
"""Test model for ONNX export."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__()
|
||||
self.net = nn.Sequential(
|
||||
QuantLinear(16, 32, **kwargs),
|
||||
nn.ReLU(),
|
||||
QuantLinear(32, 64, **kwargs),
|
||||
nn.ReLU(),
|
||||
QuantLinear(64, 16, **kwargs),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.net(x)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_bits, per_channel_quantization, constant_folding, dtype",
|
||||
[(8, True, True, torch.float32), (8, False, True, torch.float32),
|
||||
(8, True, False, torch.float32), (8, False, False, torch.float32),
|
||||
(8, False, False, torch.float16), (8, False, False, torch.bfloat16),
|
||||
((4, 3), False, True, torch.float32), ((4, 3), False, False, torch.float32),
|
||||
((4, 3), False, False, torch.float16), ((4, 3), False, False, torch.bfloat16)])
|
||||
def test_onnx_export(num_bits, per_channel_quantization, constant_folding, dtype, onnx_file_path=None):
|
||||
quant_desc_input = QuantDescriptor(num_bits=num_bits, axis=None)
|
||||
quant_desc_weight = QuantDescriptor(num_bits=num_bits, axis=0 if per_channel_quantization else None)
|
||||
|
||||
model = MyModel(quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight).cuda()
|
||||
model.eval()
|
||||
|
||||
OPSET = 17
|
||||
dummy_input = torch.randn(16, 16).cuda()
|
||||
input_names = ["input"]
|
||||
output_names = ["output"]
|
||||
|
||||
model = model.to(dtype)
|
||||
dummy_input = dummy_input.to(dtype)
|
||||
|
||||
# Calibrate model
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith('_quantizer'):
|
||||
module.enable_calib()
|
||||
module.disable_quant()
|
||||
_ = model(dummy_input)
|
||||
for name, module in model.named_modules():
|
||||
if name.endswith('_quantizer'):
|
||||
module.disable_calib()
|
||||
module.load_calib_amax()
|
||||
module.enable_quant()
|
||||
|
||||
f = io.BytesIO() if onnx_file_path is None else None
|
||||
|
||||
with pytorch_quantization.enable_onnx_export():
|
||||
torch.onnx.export(
|
||||
model,
|
||||
dummy_input,
|
||||
f=f if onnx_file_path is None else onnx_file_path,
|
||||
opset_version=OPSET,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
do_constant_folding=constant_folding,
|
||||
)
|
||||
|
||||
# TODO: ort output correctness check for fp8
|
||||
# ONNXRuntime does not seem to be supporting bf16 gemms
|
||||
if num_bits == 8 and dtype != torch.bfloat16:
|
||||
if f is not None:
|
||||
f.seek(0)
|
||||
ort_session = onnxruntime.InferenceSession(f.read() if onnx_file_path is None else onnx_file_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
|
||||
ort_result = ort_session.run([], {"input": dummy_input.cpu().numpy()})
|
||||
ort_result = torch.tensor(ort_result[0]).cuda()
|
||||
torch_result = model(dummy_input)
|
||||
test_utils.compare(ort_result, torch_result, atol=1e-2, rtol=1e-2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_onnx_export(8, False, False, torch.float16, "/tmp/test_fp16.onnx")
|
||||
test_onnx_export(8, False, False, torch.bfloat16, "/tmp/test_bf16.onnx")
|
||||
@@ -0,0 +1,129 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""Utils for testing quantization."""
|
||||
import numpy as np
|
||||
from scipy.spatial import distance
|
||||
|
||||
import torch
|
||||
|
||||
from pytorch_quantization import tensor_quant
|
||||
|
||||
def quantize_by_range(x, num_bits):
|
||||
"""Quantize torch tensor by range to num_bits with symmetric zero-mean quantizer."""
|
||||
amax = x.abs().max()
|
||||
x_q = tensor_quant.fake_tensor_quant(x, amax, num_bits)
|
||||
return x_q
|
||||
|
||||
def quantize_by_range_fused(x_tuple, num_bits):
|
||||
"""Quantize multiple torch tensors by combined range to num_bits with symmetric zero-mean quantizer."""
|
||||
# compute aggregate amax across all tensors
|
||||
amax = max([x.abs().max() for x in x_tuple])
|
||||
# quantize each tensor with the aggregate amax
|
||||
x_q_tuple = tuple(tensor_quant.fake_tensor_quant(x, amax, num_bits) for x in x_tuple)
|
||||
return x_q_tuple
|
||||
|
||||
def copy_state_and_quantize(dst, src, num_bits):
|
||||
"""Copy src to dst, quantize all 'weight' entries to num_bits."""
|
||||
src_state_dict = src.state_dict()
|
||||
dst_state_dict = dict()
|
||||
for key in src_state_dict:
|
||||
if 'weight' in key:
|
||||
dst_state_dict[key] = quantize_by_range(src_state_dict[key], num_bits)
|
||||
else:
|
||||
dst_state_dict[key] = src_state_dict[key].clone()
|
||||
|
||||
dst.load_state_dict(dst_state_dict)
|
||||
|
||||
def copy_state_and_quantize_fused(dst, src, num_bits):
|
||||
"""Copy src to dst, quantize all 'weight' entries to num_bits using the aggregate amax."""
|
||||
src_state_dict = src.state_dict()
|
||||
dst_state_dict = dict()
|
||||
|
||||
# compute aggregate amax across all weight tensors
|
||||
amax = 0
|
||||
for key in src_state_dict:
|
||||
if 'weight' in key:
|
||||
amax = max(amax, src_state_dict[key].abs().max())
|
||||
|
||||
# quantize each weight tensor with the aggregate amax
|
||||
for key in src_state_dict:
|
||||
if 'weight' in key:
|
||||
dst_state_dict[key] = tensor_quant.fake_tensor_quant(src_state_dict[key], amax, num_bits)
|
||||
else:
|
||||
dst_state_dict[key] = src_state_dict[key].clone()
|
||||
|
||||
dst.load_state_dict(dst_state_dict)
|
||||
|
||||
def compare(a, b, rtol=1e-7, atol=1e-6, ctol=1e-6):
|
||||
"""Compare two tensors and raise AssertionError if their difference is outside of tolerance."""
|
||||
if torch.isinf(a).any():
|
||||
raise ValueError("a contains infs")
|
||||
if torch.isinf(b).any():
|
||||
raise ValueError("b contains infs")
|
||||
|
||||
a = a.detach().cpu().numpy().flatten()
|
||||
b = b.detach().cpu().numpy().flatten()
|
||||
|
||||
# compare elements of a and b relative to the max value in b
|
||||
# large fp32 values may cause quantization errors that propagate to small values
|
||||
rel_diff = np.abs(a-b)/np.linalg.norm(b)
|
||||
abs_diff = np.abs(a-b)
|
||||
cos_diff = distance.cosine(a, b)
|
||||
try:
|
||||
if rel_diff.max() > rtol:
|
||||
raise AssertionError("Tensor relative error > %.2e (%.2e)" % (rtol, rel_diff.max()))
|
||||
if abs_diff.max() > atol:
|
||||
raise AssertionError("Tensor absolute error > %.2e (%.2e)" % (atol, abs_diff.max()))
|
||||
if cos_diff > ctol:
|
||||
raise AssertionError("Tensor cosine distance > %.2e (%.2e)" % (ctol, cos_diff))
|
||||
# np.testing.assert_allclose(a, b, rtol=rtol, atol=atol)
|
||||
# np.testing.assert_array_almost_equal_nulp(a, b)
|
||||
except AssertionError as e:
|
||||
print('norm(a) =', np.linalg.norm(a))
|
||||
print('norm(b) =', np.linalg.norm(b))
|
||||
print('Largest relative difference = %.2e' % rel_diff.max())
|
||||
idx = np.argmax(rel_diff)
|
||||
print('a[%d] = %.10f' % (idx, a[idx]))
|
||||
print('b[%d] = %.10f' % (idx, b[idx]))
|
||||
print('Largest absolute difference = %.2e' % abs_diff.max())
|
||||
idx = np.argmax(abs_diff)
|
||||
print('a[%d] = %.10f' % (idx, a[idx]))
|
||||
print('b[%d] = %.10f' % (idx, b[idx]))
|
||||
print('Cosine distance = %.2e' % cos_diff)
|
||||
raise e
|
||||
|
||||
def assert_min_mse(a, b, tol=1e-20):
|
||||
"""Assert that the mean squared error between a and b is at least tol."""
|
||||
a = a.detach().cpu().numpy()
|
||||
b = b.detach().cpu().numpy()
|
||||
mse = ((a-b)**2).mean()
|
||||
if mse < tol:
|
||||
raise AssertionError("MSE = %.2e < %.2e" % (mse, tol))
|
||||
|
||||
def quant_np(x, amax, num_bits=8, fake=False, narrow_range=True):
|
||||
"""Quantize x using numpy."""
|
||||
intmax = 2.0**(num_bits - 1) - 1
|
||||
intmin = -intmax if narrow_range else -intmax - 1
|
||||
scale = intmax / amax
|
||||
x_q = np.round(np.clip(x * scale, intmin, intmax))
|
||||
|
||||
if fake:
|
||||
x_q /= scale
|
||||
|
||||
return x_q
|
||||
Reference in New Issue
Block a user