Forward và backward propagation trong mạng thần kinh nhân tạo (neural network) có thể sử dụng một tập hợp các hàm như convolution và pooling. Mỗi hàm nhận một vài input tensors và áp dụng một operator để tạo output tensors. Bằng việc thể hiện mỗi operator là một node và mỗi tensor là một edge, tất cả dạng hàm tạo thành một computational graph. Với computational graph, tối ưu hoá tốc độ và bộ nhớ có thể được tiến hành bởi việc đưa vào thực hiện việc phân bổ/giải phóng bộ nhớ và thao tác một cách hợp lý. Trong SINGA, người dùng chỉ cần xác định neural network model sử dụng API của hàm Model. Graph được xây dựng và tối ưu hoá ở C++ phía sau một cách tự động.
Theo đó, một mặt người dùng thực hiện network sử dụng API của hàm Model tuân theo phong cách lập trình bắt buộc như PyTorch. Có điều khác với PyTorch phải tái tạo lại các thao tác ở mỗi vòng lặp, SINGA buffer các thao tác để tạo computational graph một cách đầy đủ (khi tính năng này được kích hoạt) sau vòng lặp đầu tiên. Do đó, mặt khác, SINGA có computational graph giống như được tạo bởi các libraries sử dụng lập trình khai báo (declarative programming), như TensorFlow. Nên nó được tối ưu hoá qua graph.
Mã code sau mô phỏng việc sử dụng API của hàm Model
.
class CNN(model.Model): def __init__(self, num_classes=10, num_channels=1): super(CNN, self).__init__() self.conv1 = layer.Conv2d(num_channels, 20, 5, padding=0, activation="RELU") self.conv2 = layer.Conv2d(20, 50, 5, padding=0, activation="RELU") self.linear1 = layer.Linear(500) self.linear2 = layer.Linear(num_classes) self.pooling1 = layer.MaxPool2d(2, 2, padding=0) self.pooling2 = layer.MaxPool2d(2, 2, padding=0) self.relu = layer.ReLU() self.flatten = layer.Flatten() self.softmax_cross_entropy = layer.SoftMaxCrossEntropy() def forward(self, x): y = self.conv1(x) y = self.pooling1(y) y = self.conv2(y) y = self.pooling2(y) y = self.flatten(y) y = self.linear1(y) y = self.relu(y) y = self.linear2(y) return y def train_one_batch(self, x, y): out = self.forward(x) loss = self.softmax_cross_entropy(out, y) self.optimizer(loss) return out, loss
model = CNN() # khởi tạo optimizer và đính nó vào model sgd = opt.SGD(lr=0.005, momentum=0.9, weight_decay=1e-5) model.set_optimizer(sgd) # khởi tạo device dev = device.create_cuda_gpu() # input và target placeholders cho model tx = tensor.Tensor((batch_size, 1, IMG_SIZE, IMG_SIZE), dev, tensor.float32) ty = tensor.Tensor((batch_size, num_classes), dev, tensor.int32) # compile model trước khi training model.compile([tx], is_train=True, use_graph=True, sequential=False)
for b in range(num_train_batch): # tạo mini-batch tiếp theo x, y = ... # Copy dữ liệu vào input tensors tx.copy_from_numpy(x) ty.copy_from_numpy(y) # Training với một batch out, loss = model(tx, ty)
Ví dụ này có trên Google Colab notebook tại đây.
Các ví dụ khác:
SINGA tạo computational graph qua 3 bước:
Sử dụng phép nhân ma trận từ dense layer của MLP model làm ví dụ. Quá trình này gọi là hàm forward
function của class MLP
class MLP(model.Model): def __init__(self, data_size=10, perceptron_size=100, num_classes=10): super(MLP, self).__init__() self.linear1 = layer.Linear(perceptron_size) ... def forward(self, inputs): y = self.linear1(inputs) ...
Layer Linear
tạo thành từ phép tính mutmul
. autograd
áp dụng phép matmul
bằng cách gọi hàm Mult
được lấy từ CPP qua SWIG.
# áp dụng matmul() singa.Mult(inputs, w)
Từ phía sau, hàm Mult
function được áp dụng bằng cách gọi GEMV
, là một hàm CBLAS. thay vì gọi hàm GEMV
trực tiếp, Mult
gửi đi GEMV
và đối số (argument) tới thiết bị (device) như sau,
// Áp dụng Mult() C->device()->Exec( [a, A, b, B, CRef](Context *ctx) mutable { GEMV<DType, Lang>(a, A, B, b, &CRef, ctx); }, read_blocks, {C->block()});
Hàm Exec
function của Device
buffer hàm này và các đối số của nó. Thêm vào đó, nó cũng có thông tin về các block (một block là một đoạn bộ nhớ cho một tensor) để đọc và viết bởi hàm này.
Sau khi Model.forward()
được thực hiện xong một lần, tất cả quá trình được buffer bởi Device
. Tiếp theo, thông tin đọc/viết của tất cả quá trình sẽ được phân tích để tạo computational graph. Ví dụ, nếu một block b
được viết bởi quá trình 01 và sau đó được đọc bởi quá trình 02 khác, chúng ta sẽ biết 02 là dựa vào 01 và có edge trực tiếp từ A sang B, thể hiện qua block b
(hoặc tensor của nó). Sau đó một graph không tuần hoàn sẽ được tạo ra như dưới đây. Graph chỉ được tạo ra một lần.
Sơ đồ 1 - Ví dụ Computational graph của MLP.
Hiện nay, các tối ưu hoá sau được thực hiện dựa trên computational graph.
Phân bổ thụ động (Lazy allocation) Khi tensor/blocks được tạo ra, các thiết bị (devices) không phân bổ bộ nhớ cho chúng ngay lập tức. Thay vào đó, khi block được tiếp cận lần đầu tiên, bộ nhớ sẽ được phân bổ.
Tự động tái sử dụng (Automatic recycling) Đếm số của mỗi tensor/block được tính dựa trên graph. Trước khi thực hiện quá trình nào, đếm số là số lượng hàm đọc block này. Trong quá trình thực hiện, khi một hàm nào được tiến hành, đếm số của mỗi block đầu vào bị trừ đi 1. Nếu đếm số của một block bằng 0, thì block này sẽ không được đọc lại nữa trong toàn bộ quá trình còn lại. Bởi vậy, bộ nhớ của nó được giải phóng một cách an toàn. Thêm vào đó, SINGA theo dõi việc sử dụng block bên ngoài graph. Nếu block được sử dụng bởi mã code Python (không phải các hàm autograd), nó sẽ không được tái sử dụng.
Chia sẻ bộ nhớ SINGA sử dụng memory pool, như là CnMem để quản lý bộ nhớ CUDA. Với Automatic recycling và memory pool, SINGA có thể chia sẻ bộ nhớ giữa các tensor. Xem xét hai hàm c = a + b
và d=2xc
. Trước khi thực hiện hàm thứ hai, theo như Lazy allocation thì bộ nhớ của d nên được sử dụng. Cũng như a
không được sử dụng ở toàn bộ quá trình còn lại. Theo Tự động sử dụng (Automatic recycling), block của a
sẽ được giải phóng sau hàm đầu tiên. Vì thế, SINGA sẽ đề xuất bốn hàm tới CUDA stream: addition, free a
, malloc b
, và multiplication. Memory pool sau đó có thể chia sẻ bộ nhớ được a
với b
giải phóng thay vì yêu cầu GPU thực hiện real malloc cho b
.
Các kĩ thuật tối ưu hoá khác, ví dụ từ compliers, như common sub-expression elimination và parallelizing operations trên CUDA streams khác nhau cũng có thể được áp dụng.
Mỗi toán tử được định nghĩa trong autograd
module áp dụng hai hàm: forward và backward, được thực hiện bằng cách gọi toán tử (operator) từ backend. Để thêm một toán tử mới vào hàm autograd
, bạn cần thêm nhiều toán tử ở backend.
Lấy toán tử Conv2d làm ví dụ, từ phía Python, hàm forward và backward được thực hiện bằng cách gọi các toán tử từ backend dựa trên loại device.
class _Conv2d(Operation): def forward(self, x, W, b=None): ...... if training: if self.handle.bias_term: self.inputs = (x, W, b) # ghi chép x, W, b else: self.inputs = (x, W) if (type(self.handle) != singa.ConvHandle): return singa.GpuConvForward(x, W, b, self.handle) else: return singa.CpuConvForward(x, W, b, self.handle) def backward(self, dy): if (type(self.handle) != singa.ConvHandle): dx = singa.GpuConvBackwardx(dy, self.inputs[1], self.inputs[0], self.handle) dW = singa.GpuConvBackwardW(dy, self.inputs[0], self.inputs[1], self.handle) db = singa.GpuConvBackwardb( dy, self.inputs[2], self.handle) if self.handle.bias_term else None else: dx = singa.CpuConvBackwardx(dy, self.inputs[1], self.inputs[0], self.handle) dW = singa.CpuConvBackwardW(dy, self.inputs[0], self.inputs[1], self.handle) db = singa.CpuConvBackwardb( dy, self.inputs[2], self.handle) if self.handle.bias_term else None if db: return dx, dW, db else: return dx, dW
Mỗi toán tử ở backend nên được thực hiện theo cách sau:
Giả dụ toán từ là foo()
; khi được thực hiện nên được gói vào trong một hàm khác, như _foo()
. foo()
chuyển _foo
cùng với các đối số như một hàm lambda tới hàm Device
's Exec
để buffer. Block để đọc và viết cũng được chuyển cho Exec
.
Tất cả đối số được sử dụng trong hàm lambda expression cần phải được thu thập dựa trên các nguyên tắc sau.
thu thập bằng giá trị
: Nếu biến đối số (argument variable) là biến local hoặc sẽ được giải phóng ngay (như intermediate tensors). Hoặc, những biến số này sẽ bị loại bỏ khi foo()
tồn tại.
thu thập theo tham khảo
:Nếu biến số được ghi lại từ phía python hoặc một biến bất biến (như tham số W và ConvHand trong Conv2d class).
mutable
: Biểu thức lambda expression nên có biến thẻ (mutable tag) nếu một biến được thu thập theo giá trị bị thay đổi trong hàm _foo()
Đây là một ví dụ về operator được áp dụng ở backend.
Tensor GpuConvBackwardx(const Tensor &dy, const Tensor &W, const Tensor &x, const CudnnConvHandle &cch) { CHECK_EQ(dy.device()->lang(), kCuda); Tensor dx; dx.ResetLike(x); dy.device()->Exec( /* * dx is a local variable so it's captured by value * dy is an intermediate tensor and isn't recorded on the python side * W is an intermediate tensor but it's recorded on the python side * chh is a variable and it's recorded on the python side */ [dx, dy, &W, &cch](Context *ctx) mutable { Block *wblock = W.block(), *dyblock = dy.block(), *dxblock = dx.block(); float alpha = 1.f, beta = 0.f; cudnnConvolutionBackwardData( ctx->cudnn_handle, &alpha, cch.filter_desc, wblock->data(), cch.y_desc, dyblock->data(), cch.conv_desc, cch.bp_data_alg, cch.workspace.block()->mutable_data(), cch.workspace_count * sizeof(float), &beta, cch.x_desc, dxblock->mutable_data()); }, {dy.block(), W.block()}, {dx.block(), cch.workspace.block()}); /* the lambda expression reads the blocks of tensor dy and w * and writes the blocks of tensor dx and chh.workspace */ return dx; }
s
:giây (second)it
: vòng lặp (iteration)Mem
:sử dụng bộ nhớ tối đa trong một GPUThroughout
:số lượng hình ảnh được xử lý mỗi giâyTime
:tổng thời gianSpeed
:vòng lặp mỗi giâyReduction
:tốc độ giảm bộ nhớ sử dụng so với sử dụng layerSpeedup
: tốc độ tăng tốc so với dev branch