Dual Backend Architecture
PyHDC supports two backends for its array operations: NumPy (the default) and PyTorch. Every encoding, operation, and hypervector works with either backend and the API surface is identical.
Design rationale
NumPy is the universal default because it has no install dependencies beyond Python and works on every platform. PyTorch is supported for three reasons:
GPU acceleration: CUDA tensors enable large-scale HDC (50,000+ dimensions, millions of codebook entries) to run in seconds instead of minutes.
Integration with deep learning pipelines: if you are using HDC as a layer in a neural network or alongside PyTorch models, keeping everything in the same tensor ecosystem avoids data copies.
Vectorised batch operations: PyTorch’s broadcasting and GPU-native matmul make batched bundling and similarity faster for large batches; GPU generation of 10,000 x 10,000 codebooks takes under a second versus several minutes on CPU.
The TORCH_AVAILABLE flag
At import time, PyHDC tries to import torch. If it succeeds,
pyhdc.TORCH_AVAILABLE is True. If it fails, it is False and all
backend="torch" requests raise ImportError.
import pyhdc
print(pyhdc.TORCH_AVAILABLE) # True or False
The BackendManager
The internal BackendManager class is a static utility that
dispatches array operations to the correct backend. Advanced users who are
extending PyHDC (writing custom encodings or components) may use it directly:
BackendManager.to_numpy(array): convert to NumPyBackendManager.to_torch(array, device): convert to PyTorch tensorBackendManager.get_device(hypervector): return the device string
Normal users never need to call BackendManager: the encoding and
Hypervector methods handle dispatch automatically.
Backend selection
Set the backend at encoding construction time:
enc_np = pyhdc.MAP_C(dimension=10_000) # numpy
enc_cpu = pyhdc.MAP_C(dimension=10_000, backend="torch") # torch CPU
enc_gpu = pyhdc.MAP_C(dimension=10_000, backend="torch",
device="cuda") # torch GPU
All operations on hypervectors generated by enc_gpu run on the GPU.
Global backend and device defaults
Rather than pass backend= and device= to every encoding, you can set
process-wide defaults. Any encoding constructed without an explicit
backend/device argument inherits these:
import pyhdc
pyhdc.prefer_torch(device=None) # default backend -> torch (CPU unless device given)
pyhdc.prefer_cuda(index=None) # default backend -> torch on CUDA (optional device index)
pyhdc.prefer_numpy() # default backend -> numpy
pyhdc.prefer_cpu() # default device -> CPU
pyhdc.get_default_backend() # current default backend ("numpy" or "torch")
pyhdc.get_default_device() # current default device (None or a device string)
enc = pyhdc.MAP_C(dimension=10_000) # inherits the current defaults
prefer_torch and prefer_cuda raise exceptions if PyTorch (or CUDA) is unavailable.
What changes between backends
From the user’s perspective, almost nothing changes. The only observable differences are:
hv.datareturnsnumpy.ndarray(NumPy) ortorch.Tensor(PyTorch)hv.devicereturnsNone(NumPy) or a device string (PyTorch)hv.backendreturns"numpy"or"torch"PyTorch similarity on a GPU tensor returns a
torch.Tensor, not a Python float. Wrap withfloat()when needed.
PyTorch batching
PyTorch enables larger and faster batched operations:
Batched generation
enc = pyhdc.MAP_C(dimension=10_000, backend="torch", device="cuda")
batch = enc.generate(size=(10_000, 1000)) # shape (10000, 1000) tensor
Batched similarity
A = enc.generate(size=(10_000, 500)) # shape (10000, 500)
B = enc.generate(size=(10_000, 500)) # shape (10000, 500)
sims = enc.similarity(A, B) # shape (500,) # one score per column pair
Batched bundling
batch = enc.generate(size=(10_000, 3)) # 3 hypervectors as columns,
# shape is (10000, 3)
result = enc.bundle(batch)
print(result.shape) # (10000,): one bundled prototype
Memory layout and data movement
PyHDC generators always produce CPU data first (they are Python-level sequences that get converted to arrays). When you create a GPU encoding, the generated floats are first assembled into a NumPy array and then transferred to the GPU. This means generation is not purely on-GPU.
The data-movement methods on Hypervector make transfers explicit:
.to_torch(device=None): NumPy -> PyTorch CPU (or specified device).to_numpy(): PyTorch -> NumPy.cuda(device=None): move to CUDA (shortcut for.to("cuda")).cpu(): move to CPU.to(device): move to any device string
These all copy the data; the original hypervector is unchanged.
Limitations
No automatic gradient tracking : HDC operations do not participate in PyTorch autograd. Gradients do not flow through bind, bundle, or similarity.
Mixed backends raise ValueError : you cannot mix NumPy and PyTorch hypervectors in the same operation; convert explicitly first.
Generator output is always CPU-first : even for GPU encodings, generation goes through NumPy before being sent to the GPU device.