.. |cbox| raw:: html Exercise 1: simple RPC and RDMA using Thallium ============================================== .. note:: The instructions in these exercises have a checkbox |cbox| that you can click on to help you keep track of your progress. These checkboxes are not connected to any action, they are just there for you to mark your progress. The code for this exercise has been cloned in :code:`thallium-tutorial-exercises` in your development environment. In a terminal connected to your docker container, make sure you are in the appropriate directory. .. code-block:: console cd margo-tutorial/margo-tutorial-exercises |cbox| The :code:`src` directory provides a :code:`client.cpp` client code, a :code:`server.cpp` server code, a :code:`types.hpp` header defining some types, and a :code:`phonebook.hpp` file containing an implementation of a phonebook using an :code:`std::unordered_map`. In this exercise we will make the server manage a phonebook and service two kinds of RPCs: adding a new entry, and looking up a phone number associated with a name. |cbox| Let's start by setting up the spack environment and building the existing code: .. code-block:: console spack env create thallium-tuto-env spack.yaml spack env activate thallium-tuto-env spack install mkdir build cd build cmake .. make This will create the client and server programs. |cbox| You can test your client and server programs by opening two terminals (make sure you have run :code:`spack env activate thallium-tuto-env` in them to activate your spack environment) and running the following from the :code:`build` directory. For the server: .. code-block:: console src/server na+sm This will start the server and print its address. :code:`na+sm` (the shared memory transport) may be changed to tcp if you run this code on multiple machines connected via an Ethernet network. For the client: .. code-block:: console src/client na+sm Copying :code:`` from the standard output of the server command. The server is setup to run indefinitely. You may kill it with Ctrl-C. |cbox| Looking at the API in :code:`phonebook.hpp`, edit :code:`server.cpp` to instanciate a phonebook in :code:`main()` (see comment **(1)**). |cbox| Our two RPCs, which we will call *"insert"* and *"lookup"*, will need argument and return types. While you could pass an :code:`std::string` and an :code:`uint64_t` directly to your RPC, in this tutorial we will define a class encapsulating them to showcase custom serialization. Edit the :code:`types.hpp` file to add an :code:`entry` class, which will be used as input argument for the *insert* RPC, ontaining a :code:`std::string name` field and a :code:`uint64_t number` field (see comment **(2)** in the file). Look at the :code:`vector3d` class as an example and define a :code:`serialize` template function in your own classes following this model. .. note:: You may need to include :code:`thallium/serialization/stl/string.hpp` so that Thallium knows how to serialize strings. To summarize: * The *insert* RPC will take an :code:`entry` as input and respond with an :code:`uint32_t` error code (0 representing a successful operation). * The *lookup* RPC will take an :code:`std::string` as input and respond with a :code:`uint64_t` (we will assume as a phone number of 0 represents a failed lookup). |cbox| Edit :code:`server.cpp` to add the definitions and declarations of the lambda functions for our two RPCs. Feel free to copy/paste and modify the existing :code:`sum` RPC (comments **(3)** and **(4)**). .. important:: Thallium relies on templates and type deduction to know what to serialize and how when sending RPC arguments and responses. If you write :code:`req.respond(0)`, C++ will infer that you want to send an :code:`int`. If your client expects an :code:`uint64_t` as a response, this will cause serialization issues. It is always recommanded to explicitely define the variable that will be returned, e.g. :code:`uint64_t ret = 0; req.respond(ret)`. |cbox| Edit :code:`client.cpp` and use the existing code as an example to register the two RPCs here as well (comment **(5)**). *Make sure that the client uses the same types as the server for RPC inputs and output. Failing to do so will cause serialization issues.* |cbox| Try out your code by calling these insert and lookup functions a few times in the client. Bonus: using RDMA to transfer larger amounts of data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Do this bonus part only if you have time, or as an exercise later. In this part, we will add a :code:`lookup_multi` RPC that uses RDMA to send multiple names at once and return the array of associated phone numbers (in practice this would be too little data to call for the use of RDMA, but we will just pretent). For this, you may use the example on :ref:`ThalliumBulk`. We assume that the names to lookup are in a :code:`std::vector` on the client. |cbox| You will need to create two bulk handles (:code:`tl::bulk`) on the client and two on the server. On the client, the first will expose the names as read-only (remember that :code:`engine::expose` can take a vector of non-contiguous segments, but you will need to use :code:`name.size()+1` as the size of each segment to keep the null terminator of each name), and the second will expose the output array as write only. The :code:`engine::expose` function can be used to create these bulk handles. It takes an :code:`std::vector>` of segments (represented by their address and size). The address of the memory of an :code:`std::string` str can be obtained using :code:`str.data()` (which should then be cast to :code:`void*`). |cbox| You will need to transfer the two bulk handles in the RPC arguments, and since names can have a varying size, you will have to also transfer the total size of the bulk handle wrapping them, so that the server knows how much memory to allocate for its local buffer. |cbox| On the server side, you will need to allocate two buffers; one to receive the names (you can use an :code:`std::vector` which you resize to the size required to receive all the names; they will end up in this contiguous buffer, separated by null characters) via a *pull* operation, the other to send the phone numbers via a *push* (you can use an :code:`std::vector` for this one). |cbox| You will need to create two bulk instances to expose these buffers. |cbox| After having transferred the names (:code:`remote_names_bulk >> local_names_bulk`), they will be in the server's contiguous buffers. You can rely on the null-terminators to know where one name ends and the next starts, lookup each name in the phonebook, fill the :code:`std::vector` buffer allocated for the phone numbers, then transfer the content of this local buffer to the client (:code:`remote_numbers_bulk << local_numbers_bulk`).