Built-in low-precision types¶
Pychop ships four ready-made classes that automatically chop to the
desired format after each public operation boundary:
pychop.builtin.CPFloat– scalar (Pythonfloat-like)pychop.builtin.CPArray–numpy.ndarraysubclasspychop.builtin.CPJaxArray– JAX array wrapperpychop.builtin.CPTensor–torch.Tensorsubclass
All four work with any Chop (or FaultChop) instance and keep the
result inside the low-precision type when the wrapped operation returns a
floating-point or complex value. Integer and Boolean outputs, such as pivot
indices and comparison masks, are left as ordinary backend values.
Note
The built-in types chop after Python/NumPy/PyTorch/JAX operation calls. They do not chop every internal multiply/add inside fused BLAS, LAPACK, XLA, or PyTorch kernels. For strict per-arithmetic-step simulation, write the algorithm steps explicitly with the built-in types or use a specialized chopped kernel.
Quick import¶
import pychop
from pychop import Chop
from pychop.builtin import CPFloat, CPArray, CPJaxArray, CPTensor, cast_precision
pychop.backend('torch') # use 'numpy', 'jax', or 'torch' for the matching container
Common set-up¶
# Half-precision (IEEE-754 binary16) – subnormals enabled
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
# Under-flow-free half (tiny numbers become zero)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
Scalar – CPFloat¶
- class pychop.builtin.CPFloat(value: int | float | complex | number, chopper: Any)[source]¶
Bases:
objectChopped-precision scalar.
Parameters¶
- valueint, float, complex, or numpy scalar
Input scalar value. It is chopped immediately at construction time.
- choppercallable
A Chop-like object. It must be callable and accept a backend-appropriate scalar container: - NumPy backend: expects a NumPy array/scalar - Torch backend: expects a torch.Tensor - JAX backend: expects a jax.Array
Attributes¶
- valuefloat
The chopped value stored as a Python scalar (typically float, but can be complex depending on backend and operation).
- choppercallable
The chopping/quantization operator.
Examples¶
Basic arithmetic stays chopped:
from pychop import Chop from pychop.builtin import CPFloat half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1) a = CPFloat(1.234567, half) b = CPFloat(0.987654, half) c = a + b d = a * b / 2.0 - 0.1
NumPy ufunc interoperability:
import numpy as np x = CPFloat(1.234, half) y = np.sin(x) # -> CPFloat z = np.sqrt(x + 1.0) # -> CPFloat
Notes¶
Mixed-chopper operations are disallowed for CPFloat binary arithmetic and NumPy ufuncs.
For torch/jax specific functions (e.g., torch.sin), CPFloat will be coerced to a Python float unless you build explicit wrappers.
- astype_precision(chopper: Any) CPFloat[source]¶
Cast this scalar to another chopped precision.
The returned scalar is chopped immediately with
chopper.
- chopper: Any¶
- item()[source]¶
Return the chopped value as a Python scalar.
Returns¶
- scalar
The stored chopped scalar.
- to_numpy(dtype=None) ndarray[source]¶
Convert to a NumPy 0-d array.
Parameters¶
- dtypenumpy dtype, default=None
Optional dtype to cast to.
Returns¶
- numpy.ndarray
A 0-d NumPy array containing the chopped value.
- value: Any¶
Example
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
a = CPFloat(1.234567, half)
b = CPFloat(0.987654, half)
print(a) # CPFloat(1.23438, prec=half)
c = a + b # stays a CPFloat, chopped
print(c) # CPFloat(2.22203, prec=half)
d = a * b / 2.0
print(d) # CPFloat(0.609863, prec=half)
# mixed with a normal Python float
e = a + 3.14
print(e) # CPFloat(4.37438, prec=half)
PyTorch – CPTensor¶
Example
import torch
from pychop.builtin import CPTensor
pychop.backend('torch') # Switch to Torch backend
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
x = CPTensor(torch.tensor([1.1, 2.2, 3.3]), half)
y = CPTensor(torch.tensor([0.5, 1.5, 2.5]), half)
print(x) # CPTensor(tensor([1.1, 2.2, 3.3]), device=cpu, prec=half)
z = x + y
print(z) # CPTensor(tensor([1.6, 3.7, 5.8]), device=cpu, prec=half)
# broadcasting with a plain tensor
reg = torch.tensor([10.0, 20.0, 30.0])
w = x * reg
print(w) # CPTensor(tensor([11.0, 44.0, 99.0]), device=cpu, prec=half)
# matrix multiplication
A = CPTensor(torch.randn(4, 3), half)
B = CPTensor(torch.randn(3, 5), half)
C = A @ B
print(C.shape) # torch.Size([4, 5])
# GPU works out-of-the-box
if torch.cuda.is_available():
A = A.to('cuda')
B = B.to('cuda')
C = A @ B
print(C.device) # cuda:0
NumPy – CPArray¶
- class pychop.builtin.CPArray(input_array, chopper=None)[source]¶
Bases:
ndarrayA NumPy array subclass that maintains chopped precision after arithmetic ops.
Key behaviors: - Construction chops the input immediately. - NumPy ufuncs (+, -, *, /, etc.) are intercepted via __array_ufunc__:
compute on base ndarrays -> chop result -> wrap as CPArray.
- Matrix multiplication (@) is intercepted via __matmul__/__rmatmul__:
compute with np.matmul on base ndarrays -> wrap (constructor chops).
NumPy high-level functions (including np.linalg.*) are intercepted via __array_function__:
unwrap CPArray inputs to base ndarrays -> call func -> wrap numeric ndarray outputs back to CPArray and chop numeric scalar outputs.
Important safety notes: - __array_function__ MUST be conservative to avoid breaking NumPy internals
(printing/formatting, string/object dtypes, etc.).
We only chop/wrap numeric ndarrays (dtype.kind in “biufc”) and numeric scalars.
Example
import numpy as np
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
p = CPArray(np.array([10.0, 20.0, 30.0]), half)
q = CPArray(np.array([1.0, 2.0, 3.0]), half)
print(p) # CPArray([10. 20. 30.], prec=half)
r = p - q
print(r) # CPArray([ 9. 18. 27.], prec=half)
# element-wise with a normal ndarray
plain = np.array([0.5, 1.5, 2.5])
s = p * plain
print(s) # CPArray([ 5. 30. 75.], prec=half)
# linear-algebra (still chopped)
M = CPArray(np.random.rand(3, 4), half)
N = CPArray(np.random.rand(4, 2), half)
P = M @ N
print(P.shape) # (3, 2)
JAX – CPJaxArray¶
Example
from pychop.builtin import CPJaxArray
pychop.backend('jax') # Switch to JAX backend
half = Chop(exp_bits=5, sig_bits=10, subnormal=True, rmode=1)
ufhalf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
x = CPJaxArray(jnp.array([1.1, 2.2, 3.3]), half)
y = CPJaxArray(jnp.array([0.5, 1.5, 2.5]), half)
print(x) # CPJaxArray([1.1, 2.2, 3.3], prec=half)
z = x + y
print(z) # CPJaxArray([1.6, 3.7, 5.8], prec=half)
# broadcasting with a plain array
reg = jnp.array([10.0, 20.0, 30.0])
w = x * reg
print(w) # CPJaxArray([11.0, 44.0, 99.0], prec=half)
# matrix multiplication
gpu_devices = jax.devices('gpu')
if gpu_devices:
with jax.default_device(gpu_devices[0]):
A = CPJaxArray(jax.random.normal(jax.random.PRNGKey(0), (4, 3)), half)
B = CPJaxArray(jax.random.normal(jax.random.PRNGKey(1), (3, 5)), half)
C = A @ B
print(C.shape) # (4, 5)
# GPU works out-of-the-box (JAX auto-dispatches)
print(C.to_regular().devices()) # {CpuDevice()} or {CudaDevice()}
Under-flow-free (UF) formats¶
Just create a Chop with subnormal=False and pass it to any of the
four types:
uf = Chop(exp_bits=5, sig_bits=10, subnormal=False, rmode=1)
tiny = CPFloat(1e-40, uf) # becomes 0.0 (flushed)
print(tiny) # CPFloat(0.0, prec=uf)
huge = CPTensor(torch.tensor([1e30, 1e35]), uf)
print(huge) # CPTensor(tensor([1.0000e+30, inf]), ...)
Supported operations¶
All Python arithmetic operators (+ – * / // % **) and many library
functions are dispatched through the wrapper/subclass machinery:
NumPy – any ufunc (
np.sin,np.exp,np.linalg.norm…)JAX – operators implemented by
CPJaxArray; usepychop.builtin.linalgorchopwrapfor wrapped high-level JAX outputsPyTorch – any
torch.*function (torch.nn.functional.relu,torch.matmul,torch.conv2d…)
Floating-point and complex results are chopped and returned as the matching built-in type. Non-floating outputs are returned as ordinary backend values.
Note
Scalar-returning wrappers in pychop.builtin.linalg return
CPFloat where scalar chaining is useful. Backend-native reductions
may return backend scalars or arrays depending on the library.
Switching precision¶
Use pychop.builtin.cast_precision() to switch a value to a new Chop
configuration. Casting chops immediately and preserves the container family.
- pychop.builtin.cast_precision(x: Any, chopper: Any) Any[source]¶
Cast a value to a chopped-precision container using
chopper.Existing
CPFloat,CPArray,CPJaxArray, andCPTensorinputs keep their container family. Native arrays/tensors are wrapped according to the active backend, or by auto-detecting the input type when the backend is"auto". The cast chops immediately.
fp8 = Chop(exp_bits=4, sig_bits=3, subnormal=True, rmode=1)
x_fp8 = cast_precision(x, fp8)
Pickling / serialization¶
The NumPy and PyTorch built-in containers implement __reduce_ex__ and can be pickled/unpickled
with the usual pickle module.
import pickle, io
buf = io.BytesIO()
pickle.dump(a, buf) # a is a CPFloat
buf.seek(0)
a2 = pickle.load(buf)
print(a2) # same value & chopper
Performance tip¶
Use the PyTorch backend (
pychop.backend('torch')) for GPU-accelerated chopping.Use the TensorFlow backend (
pychop.backend('tensorflow')) for TensorFlow/Keras workflows with STE-based gradient support. TensorFlow does not currently have a built-inCP*wrapper type.Use the NumPy backend (default) for pure-CPU workloads.
That’s it, simply drop the imports into your code and you get type-preserving low-precision arithmetic for scalars, NumPy arrays, JAX arrays, and PyTorch tensors.