Numpy and Torch interop#
Quadrants provides interop with both numpy and PyTorch. There are two mechanisms:
Copy-based: convert data between quadrants fields/ndarrays and numpy arrays or torch tensors
Direct pass-through: pass torch tensors directly into kernels as ndarray arguments (zero-copy)
Copy-based interop#
Fields#
Fields support to_numpy(), from_numpy(), to_torch(), and from_torch():
import numpy as np
import quadrants as qd
qd.init(arch=qd.gpu)
f = qd.field(qd.f32, shape=(4, 4))
# numpy -> field
arr = np.ones((4, 4), dtype=np.float32) * 3.0
f.from_numpy(arr)
# field -> numpy
result = f.to_numpy()
print(result[0, 0]) # 3.0
With torch:
import torch
f = qd.field(qd.f32, shape=(4, 4))
# torch -> field
t = torch.ones(4, 4, dtype=torch.float32) * 5.0
f.from_torch(t)
# field -> torch
result = f.to_torch(device="cpu")
print(result[0, 0]) # 5.0
Ndarrays#
Ndarrays support to_numpy() and from_numpy():
a = qd.ndarray(qd.i32, shape=(10,))
# numpy -> ndarray
arr = np.arange(10, dtype=np.int32)
a.from_numpy(arr)
# ndarray -> numpy
result = a.to_numpy()
Shape requirements#
The shape of the numpy array or torch tensor must match the shape of the field or ndarray exactly. For matrix and vector fields, the element dimensions are appended to the shape:
# A field of 3x2 matrices with shape (4,) has numpy shape (4, 3, 2)
m = qd.Matrix.field(3, 2, qd.f32, shape=(4,))
arr = np.zeros((4, 3, 2), dtype=np.float32)
m.from_numpy(arr)
Direct torch tensor pass-through#
Torch tensors can be passed directly into kernels where qd.types.ndarray() parameters are expected. The kernel reads from and writes to the torch tensor directly:
import torch
import quadrants as qd
qd.init(arch=qd.gpu)
@qd.kernel
def square(inp: qd.types.ndarray(), out: qd.types.ndarray()) -> None:
for i in range(32):
out[i] = inp[i] * inp[i]
x = torch.ones(32, dtype=torch.float32) * 3.0
y = torch.zeros(32, dtype=torch.float32)
square(x, y)
print(y[0]) # 9.0
This also works with CUDA tensors when running on a CUDA backend:
x = torch.ones(32, dtype=torch.float32, device="cuda:0") * 3.0
y = torch.zeros(32, dtype=torch.float32, device="cuda:0")
square(x, y)
Integration with torch.autograd#
Since torch tensors can be passed directly into kernels, you can integrate Quadrants kernels into PyTorch’s autograd system by wrapping them in a torch.autograd.Function:
@qd.kernel
def forward_kernel(t: qd.types.ndarray(), o: qd.types.ndarray()) -> None:
for i in range(32):
o[i] = t[i] * t[i]
@qd.kernel
def backward_kernel(t_grad: qd.types.ndarray(), t: qd.types.ndarray(), o_grad: qd.types.ndarray()) -> None:
for i in range(32):
t_grad[i] = 2 * t[i] * o_grad[i]
class Sqr(torch.autograd.Function):
@staticmethod
def forward(ctx, inp):
outp = torch.zeros_like(inp)
ctx.save_for_backward(inp)
forward_kernel(inp, outp)
return outp
@staticmethod
def backward(ctx, outp_grad):
outp_grad = outp_grad.contiguous()
inp_grad = torch.zeros_like(outp_grad)
(inp,) = ctx.saved_tensors
backward_kernel(inp_grad, inp, outp_grad)
return inp_grad
x = torch.tensor([2.0] * 32, requires_grad=True)
loss = Sqr.apply(x).sum()
loss.backward()
print(x.grad[0]) # 4.0
Summary#
Method |
Copies data? |
Works with fields? |
Works with ndarrays? |
|---|---|---|---|
|
yes |
yes |
yes |
|
yes |
yes |
no |
Direct pass-through |
no |
no |
yes (as kernel arg) |