Skip to content

Commit d93193c

Browse files
committed
More Python functions for convenience, improved mnist cuda script with better shuffling and less memcopies to GPU and back
1 parent 71d4c90 commit d93193c

3 files changed

Lines changed: 45 additions & 32 deletions

File tree

examples/mnist_cuda.py

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
sys.path.insert(0, str(Path(__file__).parent.parent / "python_lib"))
55

66
import math
7+
import random
78
import numpy as np
89
from sklearn.datasets import fetch_openml
910
from sklearn.model_selection import train_test_split
@@ -32,14 +33,10 @@ def to_one_hot(y, n_classes=10):
3233
return one_hot
3334

3435

35-
def make_batches(x, y, batch_size, shuffle=True):
36-
n = x.shape[0]
37-
indices = np.arange(n)
38-
if shuffle:
39-
np.random.shuffle(indices)
40-
for start in range(0, n, batch_size):
41-
batch_idx = indices[start : start + batch_size]
42-
yield x[batch_idx], y[batch_idx]
36+
def to_gpu(np_arr):
37+
t = fromNumpy(np_arr)
38+
t.device = Device.CUDA
39+
return t
4340

4441

4542
# ─── network ─────────────────────────────────────────────────────────────────
@@ -56,19 +53,25 @@ def make_net():
5653

5754
# ─── training ────────────────────────────────────────────────────────────────
5855

59-
def train_epoch(net, loss_fn, optim, x, y, batch_size=64):
56+
def train_epoch(net, loss_fn, optim, x_gpu, y_gpu, batch_size=64):
57+
n = x_gpu.dims[0]
58+
59+
# shuffle both tensors identically on the GPU via a random permutation of row indices
60+
indices = list(range(n))
61+
random.shuffle(indices)
62+
x_shuf = x_gpu.slice(indices)
63+
y_shuf = y_gpu.slice(indices)
64+
6065
total_loss = 0.0
6166
n_batches = 0
62-
max_batches = math.ceil(x.shape[0] / batch_size)
63-
for xb, yb in make_batches(x, y, batch_size):
64-
xTensor = fromNumpy(xb)
65-
xTensor.device = Device.CUDA
66-
67-
yTensor = fromNumpy(yb)
68-
yTensor.device = Device.CUDA
67+
max_batches = math.ceil(n / batch_size)
68+
for start in range(0, n, batch_size):
69+
end = min(start + batch_size, n)
70+
xb = x_shuf.slice(start, end)
71+
yb = y_shuf.slice(start, end)
6972

70-
pred = net.forward(xTensor)
71-
loss = loss_fn(yTensor, pred)
73+
pred = net.forward(xb)
74+
loss = loss_fn(yb, pred)
7275
loss.backward()
7376

7477
optim.clipGradients(1.0)
@@ -83,21 +86,17 @@ def train_epoch(net, loss_fn, optim, x, y, batch_size=64):
8386
return total_loss / n_batches
8487

8588

86-
def evaluate(net, x, y, batch_size=256):
89+
def evaluate(net, x_gpu, y_np, batch_size=256):
90+
n = x_gpu.dims[0]
8791
correct = 0
88-
total = 0
89-
for xb, yb in make_batches(x, y, batch_size, shuffle=False):
90-
xTensor = fromNumpy(xb)
91-
xTensor.device = Device.CUDA
92-
93-
pred = net.forward(xTensor)
92+
for start in range(0, n, batch_size):
93+
end = min(start + batch_size, n)
94+
pred = net.forward(x_gpu.slice(start, end))
9495
pred.device = Device.CPU
9596
pred_np = toNumpy(pred)
96-
9797
predicted = np.argmax(pred_np, axis=1)
98-
correct += np.sum(predicted == np.argmax(yb, axis=1))
99-
total += len(yb)
100-
return correct / total
98+
correct += np.sum(predicted == np.argmax(y_np[start:end], axis=1))
99+
return correct / n
101100

102101

103102
# ─── main ────────────────────────────────────────────────────────────────────
@@ -112,14 +111,19 @@ def evaluate(net, x, y, batch_size=256):
112111

113112
print(f"Train: {x_train.shape}, Val: {x_val.shape}")
114113

114+
# upload once; all batching and shuffling happens on the GPU
115+
x_train_gpu = to_gpu(x_train)
116+
y_train_gpu = to_gpu(y_train)
117+
x_val_gpu = to_gpu(x_val)
118+
115119
net = make_net()
116120
loss_fn = CrossEntropyWithSoftmax()
117121
optim = RmsProp(net.parameters(), 0.0001, 0.999)
118122

119123
n_epochs = 5
120124
for epoch in range(n_epochs):
121-
train_loss = train_epoch(net, loss_fn, optim, x_train, y_train)
122-
val_acc = evaluate(net, x_val, y_val)
125+
train_loss = train_epoch(net, loss_fn, optim, x_train_gpu, y_train_gpu)
126+
val_acc = evaluate(net, x_val_gpu, y_val)
123127
print(
124128
f"Epoch {epoch+1}/{n_epochs} "
125129
f"loss={train_loss:.4f} "

src/python/py_core/py_core.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ BOOST_PYTHON_MODULE(_core)
135135

136136
// classes
137137
class_<Dimension>("Dimension", no_init)
138-
.add_property("list", &Dimension::get)
138+
.add_property("list", &Dimension::toVector)
139+
.def("__getitem__", &Dimension::get)
140+
.def("__len__", &Dimension::nDims)
139141
.def("__str__", &Py_Util::toString<Dimension>)
140142
.def("__eq__", Py_DataModeling::dimEquals1)
141143
.def("__eq__", Py_DataModeling::dimEquals2)
@@ -183,6 +185,7 @@ BOOST_PYTHON_MODULE(_core)
183185

184186
// properties
185187
.add_property("device", &Tensor::getDevice, &Tensor::setDevice)
188+
.def("to", WRAP_FREE_FUNC_4(&Py_DataModeling::toDevice, Device))
186189
.add_property("dims", make_function(&Tensor::getDims, return_internal_reference<>()))
187190
.add_property("grads", &Tensor::getGrads)
188191
.add_property("requiresGrad", &Tensor::getRequiresGrad, &Tensor::setRequiresGrad)

src/python/py_core/py_core_util.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ namespace Py_DataModeling
151151
const std::vector<tensorDim_t>& indices) {
152152
return std::make_shared<Tensor>(t->getSlice(std::span<const tensorDim_t>(indices)));
153153
}
154+
155+
// device
156+
inline std::shared_ptr<Tensor> toDevice(const std::shared_ptr<Tensor>& t, Device d) {
157+
t->setDevice(d);
158+
return t;
159+
}
154160
}
155161

156162

0 commit comments

Comments
 (0)