latest (dev)
Copy
Latest development documentation · Updated 2026-10-15
Source code for tensorplay.ao.quantization.quantize
"""Eager-mode quantization workflow.
``prepare`` attaches observers to float modules according to their
``qconfig`` attributes and records activation qparams through forward hooks
(each quantizable leaf gets an input-side and an output-side observer, since
the framework's quantized kernels take qparams as explicit arguments).
``convert`` swaps calibrated float modules for their quantized counterparts
via each target class's ``from_float``. ``quantize`` chains the two around a
calibration callable; ``quantize_dynamic`` swaps modules for their
dynamic-quantization counterparts without calibration.
"""
from __future__ import annotations
import copy
import inspect
from tensorplay import nn
from .fake_quantize import FakeQuantize, FakeQuantizeBase
from .observer import ObserverBase, UniformQuantizationObserverBase
from .quantization_mappings import (
get_default_dynamic_quant_module_mappings,
get_default_static_quant_module_mappings,
)
from .stubs import DeQuantStub, QuantStub
__all__ = [
"prepare",
"convert",
"quantize",
"quantize_dynamic",
"propagate_qconfig_",
"add_quant_dequant",
]
def _matches(module, spec) -> bool:
if isinstance(spec, type) and issubclass(spec, nn.Module):
return isinstance(module, spec)
if callable(spec):
return spec(module)
return False
def propagate_qconfig_(module, qconfig_dict=None):
"""Attach ``qconfig`` attributes derived from ``qconfig_dict``.
Resolution per submodule: registered name first, then module type (by
name or class), then the parent's qconfig. A qconfig already set on the
child wins over the inherited one.
"""
qconfig_dict = qconfig_dict or {}
def assign(current, inherited, prefix):
config = qconfig_dict.get(type(current), qconfig_dict.get(type(current).__name__, inherited))
config = qconfig_dict.get(prefix, config)
current.qconfig = getattr(current, 'qconfig', config)
for name, child in current.named_children():
if not _is_observer_machinery(child):
assign(child, current.qconfig, prefix + '.' + name if prefix else name)
assign(module, qconfig_dict.get('', getattr(module, 'qconfig', None)), '')
# ---------------------------------------------------------------------------
# observer attachment
# ---------------------------------------------------------------------------
def _observer_forward_hook(module, input, output):
target = output[0] if isinstance(output, tuple) else output
module.activation_post_process.to(target.device)
result = module.activation_post_process(target)
if isinstance(output, tuple):
return (result,) + output[1:]
return result
def _observer_forward_pre_hook(module, input):
module.input_activation_post_process.to(input[0].device)
module.input_activation_post_process(input[0])
return input
def _is_observer_machinery(child) -> bool:
"""Whether ``child`` is an embedded calibration module rather than a
real submodule. A wrapper carrying only such modules (for example a
stub with its inner fake-quantize) still qualifies as a leaf."""
return isinstance(child, (FakeQuantizeBase, ObserverBase))
def _add_observer_(module, qconfig_spec=None):
"""Instantiate observers on each quantizable leaf module.
``activation_post_process`` records the module's output activations;
``input_activation_post_process`` records the incoming activations and
``weight_observer`` calibrates on the module's own weight.
"""
# The dequantize boundary is swapped out at conversion and carries no
# calibration of its own.
if isinstance(module, (DeQuantStub, nn.Identity)):
return
if getattr(module, "activation_post_process", None) is not None:
# Already prepared; attaching again would duplicate the hooks.
return
from tensorplay.ao.nn.intrinsic import _FusedModule
is_leaf = isinstance(module, _FusedModule) or all(_is_observer_machinery(child) for child in module.children())
if is_leaf and getattr(module, "qconfig", None) is not None:
if qconfig_spec is not None and not any(
_matches(module, spec) for spec in qconfig_spec
):
return
module.activation_post_process = module.qconfig.activation()
if getattr(module, '_qat_prepared', False) and isinstance(module.activation_post_process, UniformQuantizationObserverBase):
module.activation_post_process = FakeQuantize(observer=module.activation_post_process)
module.register_forward_hook(_observer_forward_hook, prepend=True)
module.input_activation_post_process = module.qconfig.activation()
module.register_forward_pre_hook(_observer_forward_pre_hook, prepend=True)
if getattr(module, "weight", None) is not None and module.qconfig.weight is not None and not hasattr(module, "weight_fake_quant"):
module.weight_observer = module.qconfig.weight()
module.weight_observer.to(module.weight.device)
module.weight_observer(module.weight.detach())
return
for child in module.children():
_add_observer_(child, qconfig_spec)
[docs]
def prepare(model, inplace=False, qconfig_spec=None):
"""Attach observers to float modules for post-training calibration."""
if not inplace:
model = copy.deepcopy(model)
propagate_qconfig_(model)
_add_observer_(model, qconfig_spec)
return model
# ---------------------------------------------------------------------------
# conversion
# ---------------------------------------------------------------------------
def _resolve_input_qparams(mod):
observer = getattr(mod, "input_activation_post_process", None)
if observer is None:
return None
return observer.calculate_qparams()
def _resolve_weight_qparams(mod):
observer = getattr(mod, "weight_observer", None)
if observer is None:
return None
return observer.calculate_qparams()
[docs]
def convert(module, mapping=None, inplace=False, remove_qconfig=True, use_precomputed_fake_quant=False):
"""Swap calibrated float modules for their quantized counterparts.
Each target class must expose ``from_float``; activation qparams the
quantized kernels require are resolved from the observers attached by
:func:`prepare` and exposed on the float module.
"""
if not inplace:
module = copy.deepcopy(module)
if mapping is None:
mapping = get_default_static_quant_module_mappings()
module = _convert(module, mapping, use_precomputed_fake_quant)
if remove_qconfig:
_remove_qconfig(module)
return module
def _convert(module, mapping, use_precomputed_fake_quant=False):
target_type = mapping.get(type(module))
if target_type is not None and getattr(module, 'qconfig', None) is not None:
kwargs = {}
if 'use_precomputed_fake_quant' in inspect.signature(target_type.from_float).parameters:
kwargs['use_precomputed_fake_quant'] = use_precomputed_fake_quant
result = target_type.from_float(module, **kwargs)
if hasattr(module, 'qconfig'):
result.qconfig = module.qconfig
for hook in module._forward_pre_hooks.values():
if hook is not _observer_forward_pre_hook:
result.register_forward_pre_hook(hook)
for hook in module._forward_hooks.values():
if hook is not _observer_forward_hook:
result.register_forward_hook(hook)
result.train(module.training)
return result
for name, child in list(module.named_children()):
if not _is_observer_machinery(child):
module._modules[name] = _convert(child, mapping, use_precomputed_fake_quant)
return module
def _remove_qconfig(module):
if hasattr(module, "qconfig"):
del module.qconfig
# Modules that survive conversion run in the quantized regime from here
# on; their float-era observer hooks would feed quantized tensors into
# the calibration observers, so strip the whole attachment.
for attr in ("activation_post_process", "input_activation_post_process",
"weight_observer"):
if hasattr(module, attr):
delattr(module, attr)
for hook_id, hook in list(module._forward_pre_hooks.items()):
if hook is _observer_forward_pre_hook:
del module._forward_pre_hooks[hook_id]
for hook_id, hook in list(module._forward_hooks.items()):
if hook is _observer_forward_hook:
del module._forward_hooks[hook_id]
for child in module.children():
_remove_qconfig(child)
# ---------------------------------------------------------------------------
# one-shot entry points
# ---------------------------------------------------------------------------
[docs]
def quantize(model, run_fn, run_args, mapping=None, inplace=False):
"""Prepare, calibrate through ``run_fn(*run_args)`` and convert.
Only submodules carrying a ``qconfig`` are quantized; attach qconfigs
(or pass a ``qconfig_dict`` through :func:`propagate_qconfig_`) before
calling.
"""
if not inplace:
model = copy.deepcopy(model)
model.eval()
prepared = prepare(model, inplace=True)
run_fn(prepared, *run_args)
return convert(prepared, mapping=mapping, inplace=True)
[docs]
def quantize_dynamic(model, qconfig_spec=None, mapping=None, inplace=False, dtype=None):
"""Swap modules for their dynamic-quantization counterparts.
No calibration is performed: dynamic modules quantize activations at
inference time and carry statically quantized weights.
"""
if mapping is None:
mapping = get_default_dynamic_quant_module_mappings()
if not inplace:
model = copy.deepcopy(model)
from tensorplay import qint8, quint8, quint4x2, float16, nn
from .qconfig import default_dynamic_qconfig, float16_dynamic_qconfig, float_qparams_weight_only_qconfig, float_qparams_weight_only_qconfig_4bit
if dtype is not None and dtype not in (qint8, quint8, quint4x2, float16):
raise ValueError('Dynamic conversion requires integer or half weights')
default_dynamic_qconfig = (float16_dynamic_qconfig if dtype == float16 else
float_qparams_weight_only_qconfig_4bit if dtype == quint4x2 else
float_qparams_weight_only_qconfig if dtype == quint8 else default_dynamic_qconfig)
if qconfig_spec is None:
if dtype==quint4x2:
qconfig_spec={nn.EmbeddingBag:default_dynamic_qconfig}
else:
qconfig_spec = {source: default_dynamic_qconfig for source in mapping
if (source in (nn.Embedding, nn.EmbeddingBag)) == (dtype == quint8)}
elif not isinstance(qconfig_spec, dict):
qconfig_spec = {source: default_dynamic_qconfig for source in qconfig_spec}
model.eval()
propagate_qconfig_(model, qconfig_spec)
return convert(model, mapping=mapping, inplace=True)
def add_quant_dequant(module):
"""Wrap a leaf float module with quant/dequant stubs.
The stubs simulate the quantization round trip around the module, which
surfaces quantization noise during float training.
"""
qconfig = getattr(module, "qconfig", None)
if qconfig is None:
raise ValueError(
"add_quant_dequant: the module must carry a qconfig attribute")
quant_stub = QuantStub(qconfig.activation)
dequant_stub = DeQuantStub()
holder = nn.Sequential(quant_stub, module, dequant_stub)
return holder
[docs]
def prepare_qat(model, mapping=None, inplace=False):
from .quantization_mappings import get_default_qat_module_mappings
if not model.training:
raise ValueError('QAT preparation requires training mode')
if not inplace:
model = copy.deepcopy(model)
propagate_qconfig_(model)
model = convert(model, mapping=mapping or get_default_qat_module_mappings(),
inplace=True, remove_qconfig=False)
for module in model.modules():
if not _is_observer_machinery(module):
module._qat_prepared = True
return prepare(model, inplace=True)
[docs]
def quantize_qat(model, run_fn, run_args, inplace=False):
if not inplace:
model = copy.deepcopy(model)
model.train()
model = prepare_qat(model, inplace=True)
run_fn(model, *run_args)
model.eval()
return convert(model, inplace=True)
__all__ += ['prepare_qat', 'quantize_qat']
__all__ += ['get_default_custom_config_dict', 'swap_module', 'get_qparam_dict', 'has_no_children_ignoring_parametrizations']
def get_default_custom_config_dict():
"""Return the default empty custom configuration dictionary."""
return {}
def _get_unique_devices_(module):
return {p.device for p in module.parameters()} | {
p.device for p in module.buffers()
}
def swap_module(mod, mapping, custom_module_class_mapping, use_precomputed_fake_quant=False):
"""Swap ``mod`` for its quantized counterpart when one exists.
``mapping`` maps float module types to quantized module classes;
``custom_module_class_mapping`` maps float module types to classes with a
``from_observed`` factory. Forward hooks are preserved across the swap.
"""
from tensorplay.nn.utils.parametrize import type_before_parametrizations
from .utils import get_qparam_dict
new_mod = mod
if hasattr(mod, "qconfig") and mod.qconfig is not None:
swapped = False
if type_before_parametrizations(mod) in custom_module_class_mapping:
new_mod = custom_module_class_mapping[
type_before_parametrizations(mod)
].from_observed(mod)
swapped = True
elif type_before_parametrizations(mod) in mapping:
qmod = mapping[type_before_parametrizations(mod)]
if getattr(qmod, "_IS_REFERENCE", False):
if mod.qconfig is None:
raise AssertionError(
"module qconfig must not be None when swapping to reference module"
)
weight_post_process = mod.qconfig.weight()
weight_post_process(mod.weight)
weight_qparams = get_qparam_dict(weight_post_process)
new_mod = qmod.from_float(mod, weight_qparams)
else:
sig = inspect.signature(qmod.from_float)
if "use_precomputed_fake_quant" in sig.parameters:
new_mod = qmod.from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
else:
new_mod = qmod.from_float(mod)
swapped = True
if swapped:
for pre_hook_fn in mod._forward_pre_hooks.values():
new_mod.register_forward_pre_hook(pre_hook_fn)
for hook_fn in mod._forward_hooks.values():
if hook_fn is not _observer_forward_hook:
new_mod.register_forward_hook(hook_fn)
devices = _get_unique_devices_(mod)
if len(devices) > 1:
raise AssertionError(
f"swap_module only works with cpu or single-device CUDA modules, but got devices {devices}"
)
device = next(iter(devices)) if len(devices) > 0 else None
if device:
new_mod.to(device)
return new_modHelp improve this page
Found an error, an unclear step, or a missing example?
Was this page helpful?

