Apache MXNet - Thành phần hệ thống
Tại đây, các thành phần hệ thống trong Apache MXNet được giải thích chi tiết. Đầu tiên, chúng ta sẽ nghiên cứu về công cụ thực thi trong MXNet.
Công cụ thực thi
Công cụ thực thi của Apache MXNet rất linh hoạt. Chúng ta có thể sử dụng nó cho việc học sâu cũng như bất kỳ vấn đề nào về miền cụ thể: thực thi một loạt các hàm theo sau sự phụ thuộc của chúng. Nó được thiết kế theo cách mà các hàm có phụ thuộc được tuần tự hóa trong khi các hàm không có phụ thuộc có thể được thực thi song song.
Giao diện cốt lõi
API được cung cấp bên dưới là giao diện cốt lõi cho công cụ thực thi của Apache MXNet -
virtual void PushSync(Fn exec_fun, Context exec_ctx,
std::vector<VarHandle> const& const_vars,
std::vector<VarHandle> const& mutate_vars) = 0;
API trên có những điều sau:
exec_fun - API giao diện cốt lõi của MXNet cho phép chúng ta đẩy hàm có tên là execute_fun, cùng với thông tin ngữ cảnh và các phụ thuộc của nó, tới công cụ thực thi.
exec_ctx - Thông tin ngữ cảnh trong đó hàm execute_fun được đề cập ở trên sẽ được thực thi.
const_vars - Đây là các biến mà hàm đọc từ đó.
mutate_vars - Đây là những biến sẽ được sửa đổi.
Công cụ thực thi cung cấp cho người dùng sự đảm bảo rằng việc thực thi bất kỳ hai hàm nào sửa đổi một biến chung được tuần tự hóa theo thứ tự đẩy của chúng.
Chức năng
Sau đây là loại chức năng của công cụ thực thi của Apache MXNet:
using Fn = std::function<void(RunContext)>;
Trong hàm trên, RunContextchứa thông tin thời gian chạy. Thông tin thời gian chạy phải được xác định bởi công cụ thực thi. Cú pháp củaRunContext như sau
struct RunContext {
// stream pointer which could be safely cast to
// cudaStream_t* type
void *stream;
};
Dưới đây là một số điểm quan trọng về các chức năng của công cụ thực thi:
Tất cả các chức năng được thực thi bởi các luồng nội bộ của công cụ thực thi MXNet.
Sẽ không tốt nếu đẩy chức năng chặn đến công cụ thực thi vì cùng với đó, chức năng sẽ chiếm luồng thực thi và cũng sẽ làm giảm tổng thông lượng.
Đối với MXNet này cung cấp một hàm không đồng bộ khác như sau:
using Callback = std::function<void()>;
using AsyncFn = std::function<void(RunContext, Callback)>;
Trong này AsyncFn hàm chúng ta có thể vượt qua phần nặng trong các luồng của chúng ta, nhưng bộ máy thực thi không coi hàm đã hoàn thành cho đến khi chúng ta gọi callback chức năng.
Bối cảnh
Trong Context, chúng ta có thể chỉ định ngữ cảnh của hàm sẽ được thực thi bên trong. Điều này thường bao gồm những điều sau:
Cho dù chức năng nên được chạy trên CPU hay GPU.
Nếu chúng tôi chỉ định GPU trong Ngữ cảnh, thì GPU nào sẽ sử dụng.
Có một sự khác biệt rất lớn giữa Context và RunContext. Ngữ cảnh có loại thiết bị và id thiết bị, trong khi RunContext có thông tin chỉ có thể được quyết định trong thời gian chạy.
VarHandle
VarHandle, được sử dụng để chỉ định các phụ thuộc của các hàm, giống như một mã thông báo (đặc biệt được cung cấp bởi công cụ thực thi) mà chúng ta có thể sử dụng để đại diện cho các tài nguyên bên ngoài mà hàm có thể sửa đổi hoặc sử dụng.
Nhưng câu hỏi đặt ra, tại sao chúng ta cần sử dụng VarHandle? Đó là bởi vì, công cụ Apache MXNet được thiết kế để tách khỏi các mô-đun MXNet khác.
Sau đây là một số điểm quan trọng về VarHandle -
Nó nhẹ nên để tạo, xóa hoặc sao chép một biến số sẽ phải trả ít chi phí vận hành.
Chúng ta cần chỉ định các biến bất biến, tức là các biến sẽ được sử dụng trong const_vars.
Chúng ta cần chỉ định các biến có thể thay đổi, tức là các biến sẽ được sửa đổi trong mutate_vars.
Quy tắc được sử dụng bởi công cụ thực thi để giải quyết sự phụ thuộc giữa các hàm là việc thực thi hai hàm bất kỳ khi một trong số chúng sửa đổi ít nhất một biến chung được tuần tự hóa theo thứ tự đẩy của chúng.
Để tạo một biến mới, chúng ta có thể sử dụng NewVar() API.
Để xóa một biến, chúng ta có thể sử dụng PushDelete API.
Hãy để chúng tôi hiểu cách hoạt động của nó với một ví dụ đơn giản -
Giả sử nếu ta có hai hàm là F1 và F2 và chúng đều đồng biến là V2. Trong trường hợp đó, F2 được đảm bảo thực hiện sau F1 nếu F2 được đẩy sau F1. Mặt khác, nếu F1 và F2 đều sử dụng V2 thì thứ tự thực hiện thực tế của chúng có thể là ngẫu nhiên.
Đẩy và Chờ
Push và wait là hai API hữu ích hơn của công cụ thực thi.
Sau đây là hai tính năng quan trọng của Push API:
Tất cả các API đẩy là không đồng bộ, có nghĩa là lệnh gọi API trả về ngay lập tức bất kể chức năng được đẩy đã hoàn thành hay chưa.
API đẩy không an toàn cho luồng, có nghĩa là chỉ một luồng nên thực hiện lệnh gọi API công cụ tại một thời điểm.
Bây giờ nếu chúng ta nói về Wait API, các điểm sau đây thể hiện nó:
Nếu người dùng muốn đợi một chức năng cụ thể kết thúc, họ nên bao gồm một hàm gọi lại trong bao đóng. Sau khi được bao gồm, hãy gọi hàm ở cuối hàm.
Mặt khác, nếu người dùng muốn đợi tất cả các hàm liên quan đến một biến nhất định kết thúc, họ nên sử dụng WaitForVar(var) API.
Nếu ai đó muốn đợi tất cả các chức năng được đẩy kết thúc, hãy sử dụng WaitForAll () API.
Được sử dụng để chỉ định các phụ thuộc của các hàm, giống như một mã thông báo.
Người điều hành
Toán tử trong Apache MXNet là một lớp chứa logic tính toán thực tế cũng như thông tin bổ trợ và hỗ trợ hệ thống thực hiện tối ưu hóa.
Giao diện điều hành
Forward là giao diện toán tử cốt lõi có cú pháp như sau:
virtual void Forward(const OpContext &ctx,
const std::vector<TBlob> &in_data,
const std::vector<OpReqType> &req,
const std::vector<TBlob> &out_data,
const std::vector<TBlob> &aux_states) = 0;
Cấu trúc của OpContext, được định nghĩa trong Forward() là như sau:
struct OpContext {
int is_train;
RunContext run_ctx;
std::vector<Resource> requested;
}
Các OpContextmô tả trạng thái của người vận hành (cho dù trong giai đoạn chạy tàu hay chạy thử), thiết bị mà người vận hành nên được chạy và cả các tài nguyên được yêu cầu. hai API hữu ích hơn của công cụ thực thi.
Từ trên Forward giao diện cốt lõi, chúng ta có thể hiểu các tài nguyên được yêu cầu như sau:
in_data và out_data đại diện cho đầu vào và đầu ra tensors.
req biểu thị cách kết quả tính toán được ghi vào out_data.
Các OpReqType có thể được định nghĩa là -
enum OpReqType {
kNullOp,
kWriteTo,
kWriteInplace,
kAddTo
};
Như thể là Forward , chúng tôi có thể tùy chọn triển khai Backward giao diện như sau -
virtual void Backward(const OpContext &ctx,
const std::vector<TBlob> &out_grad,
const std::vector<TBlob> &in_data,
const std::vector<TBlob> &out_data,
const std::vector<OpReqType> &req,
const std::vector<TBlob> &in_grad,
const std::vector<TBlob> &aux_states);
Các nhiệm vụ khác nhau
Operator giao diện cho phép người dùng thực hiện các tác vụ sau:
Người dùng có thể chỉ định cập nhật tại chỗ và có thể giảm chi phí phân bổ bộ nhớ
Để làm cho nó rõ ràng hơn, người dùng có thể ẩn một số đối số nội bộ khỏi Python.
Người dùng có thể xác định mối quan hệ giữa các tensor và tensors đầu ra.
Để thực hiện tính toán, người dùng có thể có thêm không gian tạm thời từ hệ thống.
Thuộc tính nhà điều hành
Như chúng ta đã biết rằng trong mạng nơ-ron Hợp pháp (CNN), một tích chập có một số cách triển khai. Để đạt được hiệu suất tốt nhất từ chúng, chúng tôi có thể muốn chuyển đổi giữa một số phức hợp đó.
Đó là lý do, Apache MXNet tách giao diện ngữ nghĩa của nhà điều hành khỏi giao diện thực thi. Sự tách biệt này được thực hiện dưới dạngOperatorProperty lớp bao gồm các lớp sau
InferShape - Giao diện InferShape có hai mục đích như dưới đây:
Mục đích đầu tiên là cho hệ thống biết kích thước của mỗi tensor đầu vào và đầu ra để không gian có thể được phân bổ trước Forward và Backward gọi.
Mục đích thứ hai là thực hiện kiểm tra kích thước để đảm bảo rằng không có lỗi trước khi chạy.
Cú pháp được đưa ra dưới đây:
virtual bool InferShape(mxnet::ShapeVector *in_shape,
mxnet::ShapeVector *out_shape,
mxnet::ShapeVector *aux_shape) const = 0;
Request Resource- Điều gì sẽ xảy ra nếu hệ thống của bạn có thể quản lý không gian làm việc tính toán cho các hoạt động như cudnnConvolutionForward? Hệ thống của bạn có thể thực hiện tối ưu hóa chẳng hạn như tái sử dụng không gian và nhiều hơn nữa. Ở đây, MXNet dễ dàng đạt được điều này với sự trợ giúp của hai giao diện sau:
virtual std::vector<ResourceRequest> ForwardResource(
const mxnet::ShapeVector &in_shape) const;
virtual std::vector<ResourceRequest> BackwardResource(
const mxnet::ShapeVector &in_shape) const;
Nhưng, điều gì sẽ xảy ra nếu ForwardResource và BackwardResourcetrả về mảng không trống? Trong trường hợp đó, hệ thống cung cấp các tài nguyên tương ứng thông quactx tham số trong Forward và Backward giao diện của Operator.
Backward dependency - Apache MXNet có hai chữ ký toán tử khác nhau sau đây để đối phó với sự phụ thuộc ngược -
void FullyConnectedForward(TBlob weight, TBlob in_data, TBlob out_data);
void FullyConnectedBackward(TBlob weight, TBlob in_data, TBlob out_grad, TBlob in_grad);
void PoolingForward(TBlob in_data, TBlob out_data);
void PoolingBackward(TBlob in_data, TBlob out_data, TBlob out_grad, TBlob in_grad);
Ở đây, hai điểm quan trọng cần lưu ý -
Dữ liệu out_data trong CompleteConnectedForward không được sử dụng bởi CompleteConnectedBackward và
PoolingBackward yêu cầu tất cả các đối số của PoolingForward.
Đó là lý do tại sao FullyConnectedForward, các out_datatensor một khi được tiêu thụ có thể được giải phóng an toàn vì hàm lùi sẽ không cần nó. Với sự trợ giúp của hệ thống này có thể thu thập một số bộ căng rác càng sớm càng tốt.
In place Option- Apache MXNet cung cấp một giao diện khác cho người dùng để tiết kiệm chi phí cấp phát bộ nhớ. Giao diện thích hợp cho các hoạt động khôn ngoan trong đó cả bộ căng đầu vào và đầu ra có hình dạng giống nhau.
Sau đây là cú pháp để chỉ định cập nhật tại chỗ:
Ví dụ để tạo một toán tử
Với sự trợ giúp của OperatorProperty, chúng ta có thể tạo một toán tử. Để làm như vậy, hãy làm theo các bước dưới đây:
virtual std::vector<std::pair<int, void*>> ElewiseOpProperty::ForwardInplaceOption(
const std::vector<int> &in_data,
const std::vector<void*> &out_data)
const {
return { {in_data[0], out_data[0]} };
}
virtual std::vector<std::pair<int, void*>> ElewiseOpProperty::BackwardInplaceOption(
const std::vector<int> &out_grad,
const std::vector<int> &in_data,
const std::vector<int> &out_data,
const std::vector<void*> &in_grad)
const {
return { {out_grad[0], in_grad[0]} }
}
Bước 1
Create Operator
Đầu tiên hãy triển khai giao diện sau trong OperatorProperty:
virtual Operator* CreateOperator(Context ctx) const = 0;
Ví dụ được đưa ra dưới đây -
class ConvolutionOp {
public:
void Forward( ... ) { ... }
void Backward( ... ) { ... }
};
class ConvolutionOpProperty : public OperatorProperty {
public:
Operator* CreateOperator(Context ctx) const {
return new ConvolutionOp;
}
};
Bước 2
Parameterize Operator
Nếu bạn định triển khai toán tử tích chập, bạn bắt buộc phải biết kích thước hạt nhân, kích thước bước sóng, kích thước đệm, v.v. Tại sao, vì các tham số này phải được chuyển cho nhà điều hành trước khi gọi bất kỳForward hoặc là backward giao diện.
Đối với điều này, chúng ta cần xác định ConvolutionParam cấu trúc như bên dưới -
#include <dmlc/parameter.h>
struct ConvolutionParam : public dmlc::Parameter<ConvolutionParam> {
mxnet::TShape kernel, stride, pad;
uint32_t num_filter, num_group, workspace;
bool no_bias;
};
Bây giờ, chúng ta cần đưa cái này vào ConvolutionOpProperty và chuyển nó cho nhà điều hành như sau:
class ConvolutionOp {
public:
ConvolutionOp(ConvolutionParam p): param_(p) {}
void Forward( ... ) { ... }
void Backward( ... ) { ... }
private:
ConvolutionParam param_;
};
class ConvolutionOpProperty : public OperatorProperty {
public:
void Init(const vector<pair<string, string>& kwargs) {
// initialize param_ using kwargs
}
Operator* CreateOperator(Context ctx) const {
return new ConvolutionOp(param_);
}
private:
ConvolutionParam param_;
};
Bước 3
Register the Operator Property Class and the Parameter Class to Apache MXNet
Cuối cùng, chúng ta cần đăng ký Lớp thuộc tính toán tử và Lớp tham số cho MXNet. Nó có thể được thực hiện với sự trợ giúp của các macro sau:
DMLC_REGISTER_PARAMETER(ConvolutionParam);
MXNET_REGISTER_OP_PROPERTY(Convolution, ConvolutionOpProperty);
Trong macro trên, đối số đầu tiên là chuỗi tên và đối số thứ hai là tên lớp thuộc tính.