Turi Create
4.0
|
The Unity FFI layer describes the basic C++ <–> Python interface layer. Using this layer directly requires a decent breadth of knowledge of the underlying architecture. It is recommended that you first take a look at the serialization documentation, the CPPIPC documentation.
The unity system basically is a client-server architecture which uses the CPPIPC library to share objects on the server, to the client. The server is implemented in C++ toolkits/unity, and the client is implemented mostly in Cython in src/cython_interface. Much of the common code is in src/sframe/unity. This in theory allows the C++ and Python processes to be located in different processes, or even different machines / systems across the network. This is less efficient than sharing pointers between C++ and Python directly, but this provides a more generic interface for which the Python end can be easily swapped out to, for instance, support new languages.
To allow effective communication between the server and the client (where the client is Python), we need to be able to translate between Python types and C++ types. However, since Python types are dynamic types, and C++ does not quite have an equivalent, we need to build a solution for this.
These two are the two key dynamic object types:
To expose a type X
For an example of the above convention, see unity_global.
For the unity subsystem, the following key types are exposed from the server to the client. We follow the following file naming convention for any exposed type.
While The Extension interface (turicreate_extension_interface) should be sufficient for most purposes, on occasion it might be necessary to expose a new fundamental type to Python. This is unfortunately, a slightly involving process, but we do not expect this to happen too frequently (You should really use the extension interface).
On a high level, you first implement an object exposed via the CPPIPC interface. Then you need to persuade Cython to pick up the proxy object, and then further wrapping the proxy object in a Python class if necessary.
Lets walk through a simple example. Say we are going to expose a new type called "counter" which implements a simple integer counter with two functions, increment(int), and int get(). The tasks involved are essentially:
First, we create a new header for the CPPIPC base and proxy classes. Going with the convention, the file is called counter_interface.hpp
The implementation header is in in counter.hpp. (it must inherit from counter_base)
And the implementation itself is in counter.cpp. While the hpp/cpp split is not strictly necessary (especially for such a simple class, it is still good practice.
counter.cpp has to be added to the UNITY_FILES list in the unity CMakeLists.txt
Finally, the type has to be registered with CPPIPC on the server side. See unity/server/unity_server.cpp (which implements the main() function for the unity_server) and look for a sequence of calls to
cppipc::comm_server::register_type, and add the lines:
Remember you have to include src/model_server/lib/counter.hpp at the top of unity_server.cpp.
Done!. Thats it to create a counter object and expose it via CPPIPC. Next, we need Python to be able to see the object type. And that is the tricky part.
The counter_proxy object must now be exposed to python via cython. This is can be a mildly annoying process due to Cython oddities, especially if your functions take interesting types (like maps and vectors).
All the Cython classes are implemented in src/python/turicreate/cython. To expose the counter object, first create a counter.pxd file (definition).
Now, this just makes Cython aware of the counter_proxy type. We further wrap the counter_proxy object time with a Cython class which can ensure correct allocation and deletion behavior. This wrapper can also further wrap functions to provide translators from Python types to C++ types. It is also at this point, we can switch from C++ conventions to Python conventions.
This new Counter class needs an implementation in counter.pyx
Now, in Python, once a a connection has been established, a counter object can be created with
Note that in some situations, you may in fact want to rename the Counter class as CounterProxy, and further wrap it with a Counter class fully implemented in Python. For instance, this is done with the Graph object to provide a fully Pythonic interface (The Cython cdef limitations can make it very difficult to implement interesting functions).
This section describes the old toolkit interface, a deprecated method for exporting objects and methods to Python. The new SDK method via turicreate_extension_interface is preferred.
A toolkit is very simply stated, a collection of functions, all with a common input/output interface. That is:
Both toolkit_invocation and toolkit_response are highly generic, and mainly contain a special map datastructure called turi::variant_map_type which can contain within it, a graph, a dataframe, a model or a flexible_type, and has automatic translators to and from Python.
A toolkit may contain many functions of the above form, and to expose it to the unity_server requires the writing of a "registration" function of the type:
which basically lists all the functions to expose.
Lets try this out with a simple example. We are going to implement a toolkit which simply adds one to an input integer/float. This is actually fully implemented in unity/server/toolkits/demo_addone.* files.
The invoke datastructure basically contains within it a member called "params" which is really a map from string to a turi::variant_type variant type. This type is special because it is wrappers are implemented so that it is correctly recognized by Python and can be correctly converted to and from a Python dictionary.
We first implement the toolkit. Since the toolkit essentially takes a map as an input, and a map as an output, it is up to us to specify the actual argument interface (i.e. what the contents of the map should be). Here, we state that the input map should have a field "x" which contains either an integer, or a float. The return map should also have a field "x" which is of the same type as the input, but contains the value incremented by one.
Thats it!
Now, to implement the registration function. This is quite straightforward. Basically, it just associates the C++ function with a name.
A demo_addone.hpp should also be created which exposes the get_registration() function. Finally, the unity_server binary (in src/server/unity_server.cpp) must register the toolkit. (Search for a collection of register_toolkit calls)
The demo_addone.hpp must of course be included at the top of unity_server.cpp
Thats it! To run from Python:
While the above is a simple demonstration of the toolkit interface, it covers all the key concepts of toolkit implementation. Much of the rest of the work of implementing a toolkit is in making the interface friendly.
It might be useful to see the pagerank toolkit to see how this is done.