Custom Backend
We have found that a major problem in business is that the preset backends (computational backend/scheduling backend/RPC backend/cross-process backend, etc.) cannot cover all requirements. Typically, extending the backend is not the task of the user
, but rather the task of the library developer
. Torchpipe
holds a different perspective, believing that the backend itself is also an API oriented towards the user
. Therefore, the backend must be designed to be simple enough. Drawing on the design of frameworks such as GStreamer and FFmpeg, and targeting modern C++ and Python, Torchpipe
hopes that the backend:
- Is granular enough
- Does not require any special input/output data types, with the backend author responsible for type validation and management
- As an API, the
user
only needs to pay attention to the usage rules of basic types (dict) and the optional logging component, both of which can be re-implemented and globally overridden. No other C++ elements are required. Although this approach can easily lead to code confusion, it greatly gives theuser
freedom to extend. - Provides AOT compilation, especially in Python environments.
Basic Types
any
Similar to std::any
in C++17, we have defined a type-erased container, ipipe::any
, with an almost identical interface. It can accept any type of data except char*
and unsigned char*
. Data of the any
type can be retrieved using any_cast
to obtain the actual data.
T data = any_cast<T>(any_data);
// or
auto* pdata = any_cast<T>(&any_data);
dict
As a data carrier, similar to Python's dict
, we have also defined the following dict
in C++:
#ifndef CUSTOM_DICT
using dict = std::shared_ptr<std::unordered_map<std::string, ipipe::any>>;
#else
// todo : custom dict with bidirectional binding between C++ and Python
#endif
Backend
Torchpipe limits the basic elements of the backend to:
- Initialization: parameter configuration
- Forward: input/output interface
- Forward: batch range of data
The backend interface is defined as follows:
class Backend {
public:
// The configuration will be passed through to the first parameter of `init`
virtual bool init(const std::unordered_map<std::string, std::string>& config, dict dict_config) {
return true;
};
virtual void forward(const std::vector<dict>& input_dicts) = 0;
virtual uint32_t max() const { return 1; };
virtual uint32_t min() const { return 1; };
... //Irrelevant Functions
};
You can customize the backend, for example, the internal Identity
can be defined equivalently in the following CustomIdentity.cpp
file:
#include "torchpipe/extension.h"
namespace ipipe {
class CustomIdentity : public Backend {
public:
void forward(const std::vector<dict>& input_dicts) override {
(*input_dicts[0])[TASK_RESULT_KEY] = input_dicts[0]->at(TASK_DATA_KEY);
}
uint32_t max() const override final { return 1; }; // or directly inherit from SingleBackend
};
// Register with the system
IPIPE_REGISTER(Backend, CustomIdentity, "CustomIdentity");
} // namespace ipipe
Compared to the triton inference server, the backend here has fewer requirements. Users generally only need to extend init/forward
. If batching functionality is required, then max/min
needs to be modified. In our practice, providing these elements in the backend is sufficient as long as the basic assumptions are met. Torchpipe implements all the functions through the composite of backends. In fact, torchpipe
itself is a composite of backends.
Compilation
For convenient compilation, we use the torch.utils.cpp_extension
AOT compilation facility to complete the rapid custom extension of the backend in the Python environment.
from torchpipe.utils.cpp_extension import load
from torchpipe import pipe
## Load C++
load(sources=["CustomIdentity.cpp"])
## Initialization
model = pipe({"backend":"CustomIdentity", "instance_num":"2"})
## Forward
input = {"data":"123"}
model(input)
## Check result
assert(input["result"] == b"123")
Or you can:
tp.utils.cpp_extension.load_filter(
name = 'Skip',
sources='status forward(dict data){return status::Skip;}',
sources_header="")
tp.utils.cpp_extension.load_backend(
name = 'identity',
sources='void forward(dict data){(*data)["result"] = (*data)["data"];}',
sources_header="")
model = tp.pipe({"backend":'identity'})
input = {"data":2}
model(input)
assert input["result"] == 2
Binding with Python
When using Python as the front-end language, the back-end is called from Python and the results are returned to Python, requiring type conversion.
From Python Types to Any
Assuming the input data is
input = {"data":py_object}
The framework converts the Python object py_object
to a C++ object according to certain rules;
For the Python dictionary input
, the corresponding C++ code is:
dict input_cpp = std::make_shared<std::unordered_map<std::string, ipipe::any>>();
(*input_cpp)["data"] = PY2CPP(py_object);
After the conversion, the data will be sent to the back-end for processing as a dict
. Users can specify the data type T
in the back-end and obtain the real data through the following API:
T data = any_cast<T>((*input_cpp)["data"]);
// or
auto* pdata = any_cast<T>(&(*input_cpp)["data"]);
If T
is inconsistent with the actual C++
type, an exception will be thrown or a null pointer will be returned.
From any
to Python Types
When the back-end writes the results and other key values to input_cpp
, if the Python interface is needed, the framework will automatically transmit the data corresponding to all key values except data
to the Python end according to certain rules.