import numpy as np
# Linear Min-Max Quantization: Quantize float64 to int8 (-128 to 127)
def quantize_to_int8(arr):
min_val, max_val = arr.min(), arr.max()
scale = (max_val - min_val) / 255.0
zero_point = min_val
q_arr = np.round((arr - zero_point) / scale - 128).astype(np.int8)
return q_arr, scale, zero_point
raw_matrix = np.random.uniform(-10.0, 10.0, size=(1000, 1000))
q_matrix, scale, zero_point = quantize_to_int8(raw_matrix)
print("Original Memory:", raw_matrix.nbytes, "bytes")
print("Quantized Memory:", q_matrix.nbytes, "bytes")
assert q_matrix.nbytes == raw_matrix.nbytes // 8
This tutorial covers NumPy Array Quantization and Precision Reduction. By default, NumPy creates 64-bit double-precision floating-point arrays (float64), which use 8 bytes per number. In large-scale numerical models, neural network weight matrices, and signal processing pipelines, using 64-bit precision exhausts CPU L1/L2 cache lines and memory bandwidth. Downcasting arrays to 32-bit floats, 16-bit floats, or 8-bit integer bins reduces memory usage by 50% to 87.5% while keeping precision high and throughput fast.
Ensure you have NumPy version 1.20 or later installed in your Python environment. Array precision reduction relies on built-in NumPy type routines (astype, np.finfo, np.iinfo), so no external libraries are needed:
pip install numpy
Choosing the correct floating-point precision involves a trade-off between memory footprint, numerical range, and floating-point precision (mantissa bits).
| NumPy Dtype | Bytes / Elem | Exponent Bits | Mantissa (Precision) | Approx. Dynamic Range | Memory Reduction vs float64 |
|---|---|---|---|---|---|
float64 (double) |
8 Bytes | 11 bits | 52 bits (~15-17 digits) | 10-308 to 10308 | 0% (Baseline standard) |
float32 (single) |
4 Bytes | 8 bits | 23 bits (~6-9 digits) | 10-38 to 1038 | 50% Reduction |
float16 (half) |
2 Bytes | 5 bits | 10 bits (~3-4 digits) | 6.10 × 10-5 to 65,504 | 75% Reduction |
int8 (quantized) |
1 Byte | N/A | 8 bits (-128 to 127) | 256 Discrete Bins | 87.5% Reduction |
When downcasting floating-point arrays, values that exceed the target type's maximum range turn into inf (overflow), while numbers smaller than the minimum float resolution turn into 0.0 (underflow). Inspect limits using np.finfo() before downcasting.
import numpy as np
# Inspect float16 limits
f16_info = np.finfo(np.float16)
print("float16 Max:", f16_info.max)
print("float16 Smallest Normal:", f16_info.tiny)
# Sample array in float64
arr_64 = np.array([0.000123456789, 12345.6789, 60000.0], dtype=np.float64)
# Downcast safely to float32 and float16
arr_32 = arr_64.astype(np.float32)
arr_16 = arr_64.astype(np.float16)
print("float64 bytes:", arr_64.nbytes)
print("float32 values:", arr_32)
print("float16 values:", arr_16)
assert arr_32.nbytes == arr_64.nbytes // 2
assert arr_16.nbytes == arr_64.nbytes // 4
Linear Min-Max quantization maps a continuous range of floating-point values [min_val, max_val] into discrete 8-bit signed integer bins [-128, 127] using a scale factor and zero-point offset.
import numpy as np
def quantize_tensor(tensor):
min_val = tensor.min()
max_val = tensor.max()
# Calculate scaling parameter and offset
scale = (max_val - min_val) / 255.0
zero_point = min_val
# Quantize floating points to int8
q_tensor = np.round((tensor - zero_point) / scale - 128).astype(np.int8)
return q_tensor, scale, zero_point
def dequantize_tensor(q_tensor, scale, zero_point):
# Reconstruct continuous float representation
return (q_tensor.astype(np.float32) + 128) * scale + zero_point
# Original continuous dataset
original_data = np.array([-[span style="color:#808000;">15.2Extreme outlier values distort the min-max range, stretching scale steps and causing resolution loss in typical values. Use np.clip() or percentile clipping to cap extreme values before quantization.
import numpy as np
# Data stream with an extreme outlier spike (9999.0)
raw_signals = np.array([1.2, 1.5, 1.8, 2.1, 9999.0, 1.4, 1.6], dtype=np.float64)
# Step 1: Clip extreme values to 1st and 95th percentiles
p_low, p_high = np.percentile(raw_signals, [0, 95])
clipped_signals = np.clip(raw_signals, p_low, p_high)
# Step 2: Quantize clipped signal to uint8
scale = (p_high - p_low) / 255.0
q_signals = np.round((clipped_signals - p_low) / scale).astype(np.uint8)
print("Clipped Signals: ", clipped_signals)
print("Quantized uint8: ", q_signals)
assert q_signals.max() == 255
assert q_signals.dtype == np.uint8
Downcasting array precision directly improves CPU L1/L2 cache efficiency and increases matrix computation throughput by reducing RAM-to-CPU data transfers.
import numpy as np
import time
# Allocate 2000x2000 matrices in float64 and float32
N = 2000
A_64 = np.random.randn(N, N).astype(np.float64)
B_64 = np.random.randn(N, N).astype(np.float64)
A_32 = A_64.astype(np.float32)
B_32 = B_64.astype(np.float32)
# Measure float64 matrix multiplication speed
t0 = time.perf_counter()
C_64 = np.dot(A_64, B_64)
t_64 = time.perf_counter() - t0
# Measure float32 matrix multiplication speed
t0 = time.perf_counter()
C_32 = np.dot(A_32, B_32)
t_32 = time.perf_counter() - t0
print(f"float64 Execution Time: {t_64:.4f} seconds")
print(f"float32 Execution Time: {t_32:.4f} seconds")
print(f"Speedup Factor: {t_64 / t_32:.2f}x")
assert A_32.nbytes == A_64.nbytes // 2
| Issue / Error | Cause | Recommended Solution |
|---|---|---|
| Silent Overflow / Inf Spikes | Casting values larger than 65,504 directly to float16 converts them to inf. |
Use np.clip() to bound values within np.finfo(np.float16).min and max before casting. |
| Integer Underflow / Wrap-Around | Performing operations on int8 arrays that exceed 127 causes silent integer wrap-around. |
Promote array dtypes to int32 before executing additions or multiplications. |
| Loss of Small Gradients (Underflow) | Small floating-point numbers below 1e-7 become 0.0 when cast to float32 or float16. |
Normalize or scale arrays by a constant factor prior to quantization. |
float32) provide more than enough precision without any noticeable drop in accuracy while saving 50% memory bandwidth.
Explore interactive code cells, dynamically generate classes, test type vs isinstance comparisons, and run self-validating assertions.
View on GitHubFurther Reading & Related Tutorials: NumPy dtype Reference, Performance Optimization, Memory Mapping (np.memmap), and NumPy Overview.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.