目录
前言
代码库:
https://github.com/mercury-hpc/mercury
用户手册:
用户手册:https://mercury-hpc.github.io/user/overview/
概述Overview
现有的RPC框架大多采用基于套接字的网络接口(通常构建在TCP/IP协议之上),但这种方式并不适合HPC(高性能计算)系统。因为这类API往往无法很好地适配HPC系统上使用的本机网络传输机制(RDMA等),从而导致网络性能不佳。此外,现有的RPC框架通常无法有效处理大数据参数,比如大数据传输。
为此提出一种专为HPC系统设计的异步RPC接口:Mercury 。该接口允许参数和执行请求进行异步传输,并且直接支持大数据参数的传输。此接口具有通用性,可以传送任何函数调用。同时,网络实现部分是抽象的,使得该接口能够轻松移植到未来的系统中,并有效利用现有的本地传输机制
Mercury 由三个主要层组成:
网络抽象层(network abstraction layer),它在低级别的网络结构之上提供高性能通信接口。
RPC 层( RPC layer),它为用户提供发送和接收 RPC 元数据(小消息)的必要组件。这包括函数参数的序列化和反序列化;
批量层( bulk layer),它为处理大参数提供了必要的组件——这意味着通过 RMA 传输大量数据;
(可选)高层 RPC 层( high-level RPC layer),旨在提供方便的 API,构建在较低层之上,并提供用于生成 RPC 存根以及序列化和反序列化功能的宏。
这三个主要层可以总结在下图中:
如上图所示,RPC 接口遵循客户端/服务器架构。 发出远程调用会根据关联的数据大小进入不同的步骤。 我们区分两种类型的传输:包含典型函数参数的传输,通常很小,称为元数据(metadata),以及参数包含大数据的函数参数的传输,称为批量数据(bulk)。
通过接口发送的每个 RPC 调用都会导致函数参数的序列化 进入内存缓冲区(其大小通常限制为 1 KB,具体取决于互连),然后使用网络抽象层接口将其发送到服务器。 关键要求之一是在传输的任何阶段限制内存副本,尤其是在传输大量数据时。 因此,如果发送的数据很小,它会被序列化并使用小消息发送。否则将在同一条小消息中将要传输的数据所在内存地址的描述信息发送到服务器,然后服务器可以开始(DMA)拉取数据。 限制对服务器的初始 RPC 请求的大小也有助于可伸缩性,避免大量客户端同时访问同一服务器的情况下不必要的服务器资源消耗。
根据定义,一个 RPC 调用由一个进程发起,称为源(origin),然后转发到另一个进程,该进程将执行该调用,称为目标(target)。
每一方,origin和target,都使用一个 RPC proc来序列化和反序列化 (通过接口发送的)参数。调用函数时,如果使用的参数相对较小时 网络抽象层会使用短消息机制,如果参数包含大数据,则使用远程内存访问 (RMA) 机制。请注意,当批量数据足够小时,Mercury 会自动将其与元数据一起嵌入(如果它适合的话)。
网络抽象层Network Abstraction Layer
网络抽象(NA)层是对下层的传输网络协议进行封装抽象,向用户提供一致的接口,在RPC层和批量数据层内部都被使用。NA层采用插件机制,从而实现可以通过插件系统集成实现对添加和选择对各种网络协议的支持。
注意:
如果您打算使用水星(Mercury)的RPC层(HG调用),则不应直接使用NA接口。在这种情况下,请直接跳转到可用插件部分,查看在初始化水星时可以使用的插件列表——水星的初始化过程将在RPC层部分进一步描述。
NA Interface
该接口本身必须定义三种主要的数据传输机制:意外消息传递(unexpect)、预期消息传递(expected)和远程内存访问(RMA)。此外,它还需包含客户端和服务器之间动态建立连接所需的额外设置(尽管动态连接并不总是可行,这取决于底层网络实现的具体情况)。
- 意外消息传递(unexpect)、预期消息传递(expected)传递仅限于短消息的传输,并采用双向通信方式。出于性能考虑,最大消息大小由互连决定,可能小至几千字节。意外消息传递(unexpect)的概念在其他通信协议(如BMI 10)中也有应用。通过网络抽象层发送意外消息时,无需在消息完成之前发布匹配的接收操作。这种机制使得客户端不会被阻塞,服务器可以在每次收到意外消息时获取已发布的新消息。
- 预期消息传递(expected)和意外消息之间的另一个区别在于,意外消息可以从任何远程源到达,而预期消息则需要知道具体的远程源。
- 远程内存访问(RMA)接口允许访问远程内存块(包括连续和非连续的内存块)。在大多数单向接口和RDMA协议中,内存必须先注册到网络接口控制器(NIC)才能使用。
在网络抽象层中定义接口的目的是创建一级抽象,并定义与大多数RMA协议兼容的API。将内存段注册到NIC时,通常会为该段内存创建一个句柄,其中包含虚拟地址等信息。这个本地句柄需要传达给远程节点,以便远程节点可以开始执行放置或获取操作。网络抽象层负责确保这些内存句柄可以被序列化并通过网络传输。交换句柄后,可以启动非阻塞的放置或获取操作。
在大多数互连上,put和get操作会映射到互连提供的特定API所提供的put和get操作。网络抽象接口旨在允许在仅支持双向消息传递方法的网络协议(如TCP/IP)之上模拟单向传输。有了这个网络抽象层,MeCurry可以很容易地移植以支持新的互连。网络抽象层提供的相对有限的功能(例如,不支持无限大小的双向消息)确保了接近本机的性能表现。
初始化Mercury
(https://mochi.readthedocs.io/en/latest/mercury/01_init.html)
在本教程中,您将学习如何将 Mercury 初始化为客户端和服务器。
初始化为客户端
以下代码举例说明了如何将 Mercury 初始化为客户端。我们首先需要调用 HG_Init 创建一个 hg_class 实例,然后调用 HG_Context_create 创建一个上下文。然后,在终止之前分别立即调用 HG_Context_destroy 和 HG_Finalize 来销毁上下文并和指定的Mercury。
client.c
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
static hg_class_t* hg_class = NULL; /* Pointer to the Mercury class */
static hg_context_t* hg_context = NULL; /* Pointer to the Mercury context */
int main(int argc, char** argv)
{
hg_return_t ret;
/*
* Initialize an hg_class.
* Here we only specify the protocal since this is a client
* (no need for an address and a port). HG_FALSE indicates that
* the client will not listen for incoming requests.
*/
hg_class = HG_Init("tcp", HG_FALSE);
assert(hg_class != NULL);
/* Creates a context for the hg_class. */
hg_context = HG_Context_create(hg_class);
assert(hg_context != NULL);
/* Destroy the context. */
ret = HG_Context_destroy(hg_context);
assert(ret == HG_SUCCESS);
/* Finalize the hg_class. */
hg_return_t err = HG_Finalize(hg_class);
assert(err == HG_SUCCESS);
return 0;
}
初始化为服务器
以下代码举例说明了如何将 Mercury 初始化为服务器。和客户端一样,我们调用 HG_Init 和 HG_Context_create,但这次我们将 HG_TRUE 传递给 HG_Init 以指示该进程将侦听到来的请求。
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
static hg_class_t* hg_class = NULL; /* the mercury class */
static hg_context_t* hg_context = NULL; /* the mercury context */
int main(int argc, char** argv)
{
hg_return_t ret;
/* Initialize Mercury and get an hg_class handle.
* bmi+tcp is the protocol to use.
* localhost is the address of the server (not useful at the server itself).
* HG_TRUE is here to specify that mercury will listen for incoming requests.
* (HG_TRUE on servers, HG_FALSE on clients).
*/
hg_class = HG_Init("tcp", HG_TRUE);
assert(hg_class != NULL);
/* Get the address of the server */
char hostname[128];
hg_size_t hostname_size;
hg_addr_t self_addr;
HG_Addr_self(hg_class, &self_addr);
HG_Addr_to_string(hg_class, hostname, &hostname_size, self_addr);
printf("Server running at address %s\n",hostname);
HG_Addr_free(hg_class, self_addr);
/* Creates a Mercury context from the Mercury class. */
hg_context = HG_Context_create(hg_class);
assert(hg_context != NULL);
/* Progress loop */
do
{
/* count will keep track of how many RPCs were treated in a given
* call to HG_Trigger.
*/
unsigned int count;
do {
/* Executes callbacks.
* 0 = no timeout, the function just returns if there is nothing to process.
* 1 = the max number of callbacks to execute before returning.
* After the call, count will hold the number of callbacks executed.
*/
ret = HG_Trigger(hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count);
/* Exit the loop if no event has been processed. */
/* Make progress on receiving/sending data.
* 100 is the timeout in milliseconds, for which to wait for network events. */
HG_Progress(hg_context, 100);
} while(1); /* another condition should be put here for the loop to terminate */
/* Destroys the Mercury context. */
ret = HG_Context_destroy(hg_context);
assert(ret == HG_SUCCESS);
/* Finalize Mercury. */
ret = HG_Finalize(hg_class);
assert(ret == HG_SUCCESS);
return 0;
}
此代码还举例说明了典型的 Mercury progress 循环。这个progress 循环交替使用 HG_Progress(它处理网络事件(发送和接收数据)<which makes progress on network events (sending and receiving data)>)和 HG_Trigger(它根据 HG_Progress 中发生的事件调用注册的回调函数。)
HG_Progress:接收面有点类似于poll
HG_Trigger:从context->completion_queue中取出entry,调用回调函数处理。
HG_Forward: RPC 函数和参数封装成handle,调用HG_Forward将handle发送到对端请求RPC。
注意:由于这个服务器还没有提供任何 RPC,它会一直运行直到你杀死它。
RPC 简单 Hello World例子
(https://mochi.readthedocs.io/en/latest/mercury/02_hello.html)
在本教程中,我们将注册一个简单地在服务器的标准输出上打印“Hello World”的 RPC。
服务器代码
以下代码显示了如何在服务器上注册 RPC
server.c
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
static hg_class_t* hg_class = NULL; /* the mercury class */
static hg_context_t* hg_context = NULL; /* the mercury context */
/* after serving this number of rpcs, the server will shut down. */
static const int TOTAL_RPCS = 10;
/* number of RPCS already received. */
static int num_rpcs = 0;
/*
* hello_world function to expose as an RPC.
* This function just prints "Hello World"
* and increment the num_rpcs variable.
*
* All Mercury RPCs must have a signature
* hg_return_t f(hg_handle_t h)
*/
hg_return_t hello_world(hg_handle_t h);
/*
* main function.
*/
int main(int argc, char** argv)
{
hg_return_t ret;
if(argc != 2) {
printf("Usage: %s <protocol>\n", argv[0]);
exit(0);
}
hg_class = HG_Init(argv[1], HG_TRUE);
assert(hg_class != NULL);
char hostname[128];
hg_size_t hostname_size;
hg_addr_t self_addr;
HG_Addr_self(hg_class, &self_addr);
HG_Addr_to_string(hg_class, hostname, &hostname_size, self_addr);
printf("Server running at address %s\n",hostname);
HG_Addr_free(hg_class, self_addr);
hg_context = HG_Context_create(hg_class);
assert(hg_context != NULL);
/* Register the RPC by its name ("hello").
* The two NULL arguments correspond to the functions user to
* serialize/deserialize the input and output parameters
* (hello_world doesn't have parameters and doesn't return anything, hence NULL).
*/
hg_id_t rpc_id = HG_Register_name(hg_class, "hello", NULL, NULL, hello_world);
/* We call this function to tell Mercury that hello_world will not
* send any response back to the client.
*/
HG_Registered_disable_response(hg_class, rpc_id, HG_TRUE);
do
{
unsigned int count;
do {
ret = HG_Trigger(hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count);
HG_Progress(hg_context, 100);
} while(num_rpcs < TOTAL_RPCS);
/* Exit the loop if we have reached the given number of RPCs. */
ret = HG_Context_destroy(hg_context);
assert(ret == HG_SUCCESS);
ret = HG_Finalize(hg_class);
assert(ret == HG_SUCCESS);
return 0;
}
/* Implementation of the hello_world RPC. */
hg_return_t hello_world(hg_handle_t h)
{
hg_return_t ret;
printf("Hello World!\n");
num_rpcs += 1;
/* We are not going to use the handle anymore, so we should destroy it. */
ret = HG_Destroy(h);
assert(ret == HG_SUCCESS);
return HG_SUCCESS;
}
要将函数注册为 RPC,它必须将 hg_handle_t 作为参数并返回 hg_return_t 类型的值(如果处理程序正确执行,则通常为 HG_SUCCESS)。此函数(在我们的例子中为 hello_workd)使用 HG_Register_name 注册为 RPC。它返回 RPC 的标识符。我们还调用 HG_Registered_disable_response 来指示此 RPC 不会将任何响应发送回客户端。在 hello_world 的定义中,我们只需在标准输出上打印“Hello World”,然后调用 HG_Destroy 来销毁传递给函数的 RPC 句柄。
客户端代码
下面是对应的客户端代码。
client.c
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
static hg_class_t* hg_class = NULL; /* Pointer to the Mercury class */
static hg_context_t* hg_context = NULL; /* Pointer to the Mercury context */
static hg_id_t hello_rpc_id; /* ID of the RPC */
static int completed = 0; /* Variable indicating if the call has completed */
/*
* This callback will be called after looking up for the server's address.
* This is the function that will also send the RPC to the servers, then
* set the completed variable to 1.
*/
hg_return_t lookup_callback(const struct hg_cb_info *callback_info);
int main(int argc, char** argv)
{
hg_return_t ret;
if(argc != 3) {
printf("Usage: %s <protocol> <server_address>\n",argv[0]);
printf("Example: %s tcp ofi+tcp://1.2.3.4:1234\n",argv[0]);
exit(0);
}
char* protocol = argv[1];
char* server_address = argv[2];
hg_class = HG_Init(protocol, HG_FALSE);
assert(hg_class != NULL);
hg_context = HG_Context_create(hg_class);
assert(hg_context != NULL);
/* Register a RPC function.
* The first two NULL correspond to what would be pointers to
* serialization/deserialization functions for input and output datatypes
* (not used in this example).
* The third NULL is the pointer to the function (which is on the server,
* so NULL here on the client).
*/
hello_rpc_id = HG_Register_name(hg_class, "hello", NULL, NULL, NULL);
/* Indicate Mercury that we shouldn't expect a response from the server
* when calling this RPC.
*/
HG_Registered_disable_response(hg_class, hello_rpc_id, HG_TRUE);
/* Lookup the address of the server, this is asynchronous and
* the result will be handled by lookup_callback once we start the progress loop.
* NULL correspond to a pointer to user data to pass to lookup_callback (we don't use
* any here). The 4th argument is the address of the server.
* The 5th argument is a pointer a variable of type hg_op_id_t, which identifies the operation.
* It can be useful to get this identifier if we want to be able to cancel it using
* HG_Cancel. Here we don't use it so we pass HG_OP_ID_IGNORE.
*/
ret = HG_Addr_lookup(hg_context, lookup_callback, NULL, server_address, HG_OP_ID_IGNORE);
/* Main event loop: we do some progress until completed becomes TRUE. */
while(!completed)
{
unsigned int count;
do {
ret = HG_Trigger(hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count && !completed);
HG_Progress(hg_context, 100);
}
ret = HG_Context_destroy(hg_context);
assert(ret == HG_SUCCESS);
/* Finalize the hg_class. */
hg_return_t err = HG_Finalize(hg_class);
assert(err == HG_SUCCESS);
return 0;
}
/*
* This function is called when the address lookup operation has completed.
*/
hg_return_t lookup_callback(const struct hg_cb_info *callback_info)
{
hg_return_t ret;
/* First, check that the lookup went fine. */
assert(callback_info->ret == 0);
/* Get the address of the server. */
hg_addr_t addr = callback_info->info.lookup.addr;
/* Create a call to the hello_world RPC. */
hg_handle_t handle;
ret = HG_Create(hg_context, addr, hello_rpc_id, &handle);
assert(ret == HG_SUCCESS);
/* Send the RPC. The first NULL correspond to the callback
* function to call when receiving the response from the server
* (we don't expect a response, hence NULL here).
* The second NULL is a pointer to user-specified data that will
* be passed to the response callback.
* The third NULL is a pointer to the RPC's argument (we don't
* use any here).
*/
ret = HG_Forward(handle, NULL, NULL, NULL);
assert(ret == HG_SUCCESS);
/* Free the handle */
ret = HG_Destroy(handle);
assert(ret == HG_SUCCESS);
/* Set completed to 1 so we terminate the loop. */
completed = 1;
return HG_SUCCESS;
}
服务端注册函数名hello,指派对应的RPC函数hello_word,客户端注册函数hello。
就像服务器端一样,我们使用 HG_Register_name 来注册 RPC,这次传递 NULL 而不是函数指针作为最后一个参数。我们还调用 HG_Registered_disable_response 表示服务器不会发回响应。
HG_Addr_lookup 用于查找服务器的地址。此函数将回调作为其第二个参数。此回调必须是一个接受 const struct hg_cb_info* 并返回 hg_return_t 类型值的函数。当地址查找完成时将调用它。
接下来,我们进入一个类似于服务器的进度循环。这是因为我们正在等待 HG_Addr_lookup 完成。提供的回调将从 HG_Trigger 内部执行。在lookup_callback 函数中,我们可以从callback_info->info.lookup.addr 获取服务器的地址。此地址可用于使用 HG_Create 创建 RPC 实例,并使用 HG_Forward 转发它。
由于我们不期望任何响应,我们可以立即调用 HG_Destroy 来销毁我们刚刚转发的 RPC 句柄。我们将完成设置为 1 以退出 main 中的进度循环。
传递上下文
上一个教程使用全局静态变量来使诸如 hg_context 和 hg_class 之类的资源可以在回调中访问。但任何优秀的开发人员都会禁止这种做法,因此我们将使用局部变量来代替前面的做法。
Client
在客户端,我们将 一个上下文封装在client_data_t 结构中。通过将指向该结构的指针作为 HG_Addr_lookup 的第三个参数传递,我们可以在回调中将其恢复为 callback_info->arg。这让我们可以将 hg_class、hg_context 和 hello_rpc_id 从 main 传递到 lookup_callback 函数。
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
typedef struct {
hg_class_t* hg_class;
hg_context_t* hg_context;
hg_id_t hello_rpc_id;
int completed;
} client_data_t;
hg_return_t lookup_callback(const struct hg_cb_info *callback_info);
int main(int argc, char** argv)
{
hg_return_t ret;
if(argc != 3) {
printf("Usage: %s <protocol> <server_address>\n",argv[0]);
printf("Example: %s tcp ofi+tcp://1.2.3.4:1234\n",argv[0]);
exit(0);
}
client_data_t client_data = {
.hg_class = NULL,
.hg_context = NULL,
.hello_rpc_id = 0,
.completed = 0
};
char* protocol = argv[1];
char* server_address = argv[2];
client_data.hg_class = HG_Init(protocol, HG_FALSE);
assert(client_data.hg_class != NULL);
client_data.hg_context = HG_Context_create(client_data.hg_class);
assert(client_data.hg_context != NULL);
client_data.hello_rpc_id = HG_Register_name(client_data.hg_class, "hello", NULL, NULL, NULL);
HG_Registered_disable_response(client_data.hg_class, client_data.hello_rpc_id, HG_TRUE);
/* We pass a pointer to the client's data as 3rd argument */
ret = HG_Addr_lookup(client_data.hg_context, lookup_callback, &client_data, server_address, HG_OP_ID_IGNORE);
while(!client_data.completed)
{
unsigned int count;
do {
ret = HG_Trigger(client_data.hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count && !client_data.completed);
HG_Progress(client_data.hg_context, 100);
}
ret = HG_Context_destroy(client_data.hg_context);
assert(ret == HG_SUCCESS);
hg_return_t err = HG_Finalize(client_data.hg_class);
assert(err == HG_SUCCESS);
return 0;
}
hg_return_t lookup_callback(const struct hg_cb_info *callback_info)
{
hg_return_t ret;
assert(callback_info->ret == 0);
/* Get the client's data */
client_data_t* client_data = (client_data_t*)(callback_info->arg);
hg_addr_t addr = callback_info->info.lookup.addr;
hg_handle_t handle;
ret = HG_Create(client_data->hg_context, addr, client_data->hello_rpc_id, &handle);
assert(ret == HG_SUCCESS);
ret = HG_Forward(handle, NULL, NULL, NULL);
assert(ret == HG_SUCCESS);
ret = HG_Destroy(handle);
assert(ret == HG_SUCCESS);
client_data->completed = 1;
return HG_SUCCESS;
}
Server
在服务器端,我们将我们的信息封装在一个 server_data_t 结构中。我们使用 HG_Register_data 将指向该结构的指针附加到 RPC 处理程序(第四个参数 NULL,对应于在 RPC 处理程序注销时调用以释放指针的函数。由于我们的结构在堆栈上,所以我们不需要提供任何此类功能)。在 hello_world 处理程序中,我们使用 HG_Get_info 和 HG_Registered_data 恢复指向 server_data_t 结构的指针。
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
typedef struct {
hg_class_t* hg_class;
hg_context_t* hg_context;
int max_rpcs;
int num_rpcs;
} server_data_t;
hg_return_t hello_world(hg_handle_t h);
int main(int argc, char** argv)
{
hg_return_t ret;
if(argc != 2) {
printf("Usage: %s <protocol>\n", argv[0]);
exit(0);
}
server_data_t server_data = {
.hg_class = NULL,
.hg_context = NULL,
.max_rpcs = 4,
.num_rpcs = 0
};
server_data.hg_class = HG_Init(argv[1], HG_TRUE);
assert(server_data.hg_class != NULL);
char hostname[128];
hg_size_t hostname_size;
hg_addr_t self_addr;
HG_Addr_self(server_data.hg_class, &self_addr);
HG_Addr_to_string(server_data.hg_class, hostname, &hostname_size, self_addr);
printf("Server running at address %s\n",hostname);
HG_Addr_free(server_data.hg_class, self_addr);
server_data.hg_context = HG_Context_create(server_data.hg_class);
assert(server_data.hg_context != NULL);
hg_id_t rpc_id = HG_Register_name(server_data.hg_class, "hello", NULL, NULL, hello_world);
/* Register data with the RPC handler */
HG_Register_data(server_data.hg_class, rpc_id, &server_data, NULL);
HG_Registered_disable_response(server_data.hg_class, rpc_id, HG_TRUE);
do
{
unsigned int count;
do {
ret = HG_Trigger(server_data.hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count);
HG_Progress(server_data.hg_context, 100);
} while(server_data.num_rpcs < server_data.max_rpcs);
ret = HG_Context_destroy(server_data.hg_context);
assert(ret == HG_SUCCESS);
ret = HG_Finalize(server_data.hg_class);
assert(ret == HG_SUCCESS);
return 0;
}
/* Implementation of the hello_world RPC. */
hg_return_t hello_world(hg_handle_t h)
{
hg_return_t ret;
/* Get the hg_class_t instance from the handle */
const struct hg_info *info = HG_Get_info(h);
hg_class_t* hg_class = info->hg_class;
hg_id_t rpc_id = info->id;
/* Get the data attached to the RPC handle */
server_data_t* server_data = (server_data_t*)HG_Registered_data(hg_class, rpc_id);
printf("Hello World!\n");
server_data->num_rpcs += 1;
ret = HG_Destroy(h);
assert(ret == HG_SUCCESS);
return HG_SUCCESS;
}
RPC 参数和返回值
在之前的教程中,我们没有向 RPC 处理程序传递或返回任何数据。在本教程中,我们将了解如何将数据作为参数发送到 RPC,并从 RPC 返回数据。我们将以计算客户端发送的两个数字之和的 RPC 为例。
输入/输出结构
首先,我们需要声明 RPC 参数和返回值的类型。这是使用mercury 宏在mercury_macros.h 头文件中完成的,如下所示。
types.h
#ifndef PARAM_H
#define PARAM_H
#include <mercury.h>
#include <mercury_macros.h>
MERCURY_GEN_PROC(sum_in_t,
((int32_t)(x))\
((int32_t)(y)))
MERCURY_GEN_PROC(sum_out_t, ((int32_t)(ret)))
#endif
Client code
以下代码查找服务器的地址,然后向服务器发送一个 RPC。
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
#include "types.h"
typedef struct {
hg_class_t* hg_class;
hg_context_t* hg_context;
hg_id_t sum_rpc_id;
int completed;
} client_state_t;
hg_return_t lookup_callback(const struct hg_cb_info *callback_info);
hg_return_t sum_completed(const struct hg_cb_info *info);
int main(int argc, char** argv)
{
hg_return_t ret;
if(argc != 3) {
printf("Usage: %s <protocol> <server_address>\n",argv[0]);
printf("Example: %s tcp tcp://1.2.3.4:1234\n",argv[0]);
exit(0);
}
char* protocol = argv[1];
char* server_address = argv[2];
client_state_t state;
state.completed = 0;
state.hg_class = HG_Init(protocol, HG_FALSE);
assert(state.hg_class != NULL);
state.hg_context = HG_Context_create(state.hg_class);
assert(state.hg_context != NULL);
state.sum_rpc_id = MERCURY_REGISTER(state.hg_class, "sum", sum_in_t, sum_out_t, NULL);
ret = HG_Addr_lookup(state.hg_context, lookup_callback, &state, server_address, HG_OP_ID_IGNORE);
while(!state.completed)
{
unsigned int count;
do {
ret = HG_Trigger(state.hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count && !state.completed);
HG_Progress(state.hg_context, 100);
}
ret = HG_Context_destroy(state.hg_context);
assert(ret == HG_SUCCESS);
hg_return_t err = HG_Finalize(state.hg_class);
assert(err == HG_SUCCESS);
return 0;
}
hg_return_t lookup_callback(const struct hg_cb_info *callback_info)
{
hg_return_t ret;
/* We get the pointer to the engine_state here. */
client_state_t* state = (client_state_t*)(callback_info->arg);
assert(callback_info->ret == 0);
hg_addr_t addr = callback_info->info.lookup.addr;
hg_handle_t handle;
ret = HG_Create(state->hg_context, addr, state->sum_rpc_id, &handle);
assert(ret == HG_SUCCESS);
sum_in_t in;
in.x = 42;
in.y = 23;
ret = HG_Forward(handle, sum_completed, state, &in);
assert(ret == HG_SUCCESS);
ret = HG_Addr_free(state->hg_class, addr);
assert(ret == HG_SUCCESS);
return HG_SUCCESS;
}
hg_return_t sum_completed(const struct hg_cb_info *info)
{
hg_return_t ret;
client_state_t* state = (client_state_t*)(info->arg);
sum_out_t out;
assert(info->ret == HG_SUCCESS);
ret = HG_Get_output(info->info.forward.handle, &out);
assert(ret == HG_SUCCESS);
printf("Got response: %d\n", out.ret);
ret = HG_Free_output(info->info.forward.handle, &out);
assert(ret == HG_SUCCESS);
ret = HG_Destroy(info->info.forward.handle);
assert(ret == HG_SUCCESS);
state->completed = 1;
return HG_SUCCESS;
}
与上一个教程相比的主要区别在于,我们将指向 sum_in_t 结构的指针传递以及完成回调 sum_completed给 HG_Forward,当服务器响应时,将调用此完成回调。在此回调中,HG_Get_output 用于取回服务器发送的output数据。我们需要调用 HG_Free_output 来释放使用后的output 。另请注意,HG_Destroy 现在在完成回调中使用,而不是在 HG_Forward 之后使用。
Server code
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
#include "types.h"
typedef struct {
hg_class_t* hg_class;
hg_context_t* hg_context;
int num_rpcs;
} server_state;
static const int TOTAL_RPCS = 10;
hg_return_t sum(hg_handle_t h);
int main(int argc, char** argv)
{
hg_return_t ret;
if(argc != 2) {
printf("Usage: %s <server address>\n", argv[0]);
exit(0);
}
const char* server_address = argv[1];
server_state state; // Instance of the server's state
state.num_rpcs = 0;
state.hg_class = HG_Init(server_address, HG_TRUE);
assert(state.hg_class != NULL);
char hostname[128];
hg_size_t hostname_size;
hg_addr_t self_addr;
HG_Addr_self(state.hg_class, &self_addr);
HG_Addr_to_string(state.hg_class, hostname, &hostname_size, self_addr);
printf("Server running at address %s\n",hostname);
HG_Addr_free(state.hg_class, self_addr);
state.hg_context = HG_Context_create(state.hg_class);
assert(state.hg_context != NULL);
hg_id_t rpc_id = MERCURY_REGISTER(state.hg_class, "sum", sum_in_t, sum_out_t, sum);
/* Attach the local server_state to the RPC so we can get a pointer to it when
* the RPC is invoked. */
ret = HG_Register_data(state.hg_class, rpc_id, &state, NULL);
do
{
unsigned int count;
do {
ret = HG_Trigger(state.hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count);
HG_Progress(state.hg_context, 100);
} while(state.num_rpcs < TOTAL_RPCS);
ret = HG_Context_destroy(state.hg_context);
assert(ret == HG_SUCCESS);
ret = HG_Finalize(state.hg_class);
assert(ret == HG_SUCCESS);
return 0;
}
hg_return_t sum(hg_handle_t handle)
{
hg_return_t ret;
sum_in_t in;
sum_out_t out;
const struct hg_info* info = HG_Get_info(handle);
server_state* state = HG_Registered_data(info->hg_class, info->id);
ret = HG_Get_input(handle, &in);
assert(ret == HG_SUCCESS);
out.ret = in.x + in.y;
printf("%d + %d = %d\n",in.x,in.y,in.x+in.y);
state->num_rpcs += 1;
ret = HG_Respond(handle,NULL,NULL,&out);
assert(ret == HG_SUCCESS);
ret = HG_Free_input(handle, &in);
assert(ret == HG_SUCCESS);
ret = HG_Destroy(handle);
assert(ret == HG_SUCCESS);
return HG_SUCCESS;
}
在服务器端,我们使用 HG_Get_input 将input数据反序列化为 sum_in_t 结构。用完input数据后,我们使用 HG_Free_input。然后HG_Respond将一个指向 sum_out_t 对象的指针返回给客户端。
RDMA 传输
Mercury 可以使用 RDMA 传输大量数据。在本教程中,我们将通过将文件内容从客户端传输到服务器来演示如何使用此功能。
输入/输出结构
就像我们前面的例子一样,我们需要定义用于 RPC 输入和输出的结构。这些如下。
types.h
#ifndef PARAM_H
#define PARAM_H
#include <mercury.h>
#include <mercury_bulk.h>
#include <mercury_proc_string.h>
#include <mercury_macros.h>
MERCURY_GEN_PROC(save_in_t,
((hg_string_t)(filename))\
((hg_size_t)(size))\
((hg_bulk_t)(bulk_handle)))
MERCURY_GEN_PROC(save_out_t, ((int32_t)(ret)))
#endif
客户端将发送文件名 (hg_string_t)、文件大小 (hg_size_t) 和表示客户端公开的内存区域并包含文件内容的bulk handle。服务器将简单地响应一个整数,指示操作是否成功。
Client code
客户端代码如下
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <mercury.h>
#include "types.h"
typedef struct {
hg_class_t* hg_class;
hg_context_t* hg_context;
hg_id_t save_rpc_id;
int completed;
} client_state;
typedef struct {
client_state* state;
hg_bulk_t bulk_handle;
void* buffer;
size_t size;
char* filename;
} save_operation;
hg_return_t lookup_callback(const struct hg_cb_info *callback_info);
hg_return_t save_completed(const struct hg_cb_info *info);
int main(int argc, char** argv)
{
if(argc != 4) {
fprintf(stderr,"Usage: %s <protocol> <server address> <filename>\n", argv[0]);
exit(0);
}
hg_return_t ret;
const char* protocol = argv[1];
/* Local instance of the client_state. */
client_state state;
state.completed = 0;
// Initialize an hg_class.
state.hg_class = HG_Init(protocol, HG_FALSE);
assert(state.hg_class != NULL);
// Creates a context for the hg_class.
state.hg_context = HG_Context_create(state.hg_class);
assert(state.hg_context != NULL);
// Register a RPC function
state.save_rpc_id = MERCURY_REGISTER(state.hg_class, "save", save_in_t, save_out_t, NULL);
// Create the save_operation structure
save_operation save_op;
save_op.state = &state;
save_op.filename = argv[3];
if(access(save_op.filename, F_OK) == -1) {
fprintf(stderr,"File %s doesn't exist or cannot be accessed.\n",save_op.filename);
exit(-1);
}
char* server_address = argv[2];
ret = HG_Addr_lookup(state.hg_context, lookup_callback, &save_op, server_address, HG_OP_ID_IGNORE);
// Main event loop
while(!state.completed)
{
unsigned int count;
do {
ret = HG_Trigger(state.hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count && !state.completed);
HG_Progress(state.hg_context, 100);
}
// Destroy the context
ret = HG_Context_destroy(state.hg_context);
assert(ret == HG_SUCCESS);
// Finalize the hg_class.
hg_return_t err = HG_Finalize(state.hg_class);
assert(err == HG_SUCCESS);
return 0;
}
hg_return_t lookup_callback(const struct hg_cb_info *callback_info)
{
hg_return_t ret;
assert(callback_info->ret == 0);
/* We get the pointer to the client_state here. */
save_operation* save_op = (save_operation*)(callback_info->arg);
client_state* state = save_op->state;
/* Check file size to allocate buffer. */
FILE* file = fopen(save_op->filename,"r");
fseek(file, 0L, SEEK_END);
save_op->size = ftell(file);
fseek(file, 0L, SEEK_SET);
save_op->buffer = calloc(1, save_op->size);
size_t bytes_read = fread(save_op->buffer,1,save_op->size,file);
fclose(file);
hg_addr_t addr = callback_info->info.lookup.addr;
hg_handle_t handle;
ret = HG_Create(state->hg_context, addr, state->save_rpc_id, &handle);
assert(ret == HG_SUCCESS);
save_in_t in;
in.filename = save_op->filename;
in.size = save_op->size;
ret = HG_Bulk_create(state->hg_class, 1, (void**) &(save_op->buffer), &(save_op->size),
HG_BULK_READ_ONLY, &(save_op->bulk_handle));
assert(ret == HG_SUCCESS);
in.bulk_handle = save_op->bulk_handle;
/* The state pointer is passed along as user argument. */
ret = HG_Forward(handle, save_completed, save_op, &in);
assert(ret == HG_SUCCESS);
/* Free the address. */
ret = HG_Addr_free(state->hg_class, addr);
assert(ret == HG_SUCCESS);
return HG_SUCCESS;
}
hg_return_t save_completed(const struct hg_cb_info *info)
{
hg_return_t ret;
/* Get the state pointer from the user-provided arguments. */
save_operation* save_op = (save_operation*)(info->arg);
client_state* state = (client_state*)(save_op->state);
save_out_t out;
assert(info->ret == HG_SUCCESS);
ret = HG_Get_output(info->info.forward.handle, &out);
assert(ret == HG_SUCCESS);
printf("Got response: %d\n", out.ret);
ret = HG_Bulk_free(save_op->bulk_handle);
assert(ret == HG_SUCCESS);
ret = HG_Free_output(info->info.forward.handle, &out);
assert(ret == HG_SUCCESS);
ret = HG_Destroy(info->info.forward.handle);
assert(ret == HG_SUCCESS);
state->completed = 1;
return HG_SUCCESS;
}
我们定义了一个 save_operation 结构来保存有关正在进行的操作的信息。该结构将通过指针作为用户提供的参数传递给回调。在lookup回调中,我们打开文件并将其内容读入缓冲区。然后我们使用 HG_Bulk_create 为 RDMA 操作公开缓冲区。这给了我们一个可以通过 RPC 发送到服务器的 hg_bulk_t 对象。RPC 完成并收到响应后,使用 HG_Bulk_free 释放 hg_bulkt_t 对象。
Server code
以下代码对应服务器。
Server.c
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <mercury.h>
#include "types.h"
/* This structure will encapsulate data about the server. */
typedef struct {
hg_class_t* hg_class;
hg_context_t* hg_context;
} server_state;
typedef struct {
char* filename;
hg_size_t size;
void* buffer;
hg_bulk_t bulk_handle;
hg_handle_t handle;
} rpc_state;
static hg_return_t save_bulk_completed(const struct hg_cb_info *info);
static hg_return_t save(hg_handle_t h);
int main(int argc, char** argv)
{
hg_return_t ret;
if(argc != 2) {
printf("Usage: %s <server address>\n", argv[0]);
exit(0);
}
const char* server_address = argv[1];
server_state state; // Instance of the server's state
state.hg_class = HG_Init(server_address, HG_TRUE);
assert(state.hg_class != NULL);
/* Get the address of the server */
char hostname[128];
hg_size_t hostname_size;
hg_addr_t self_addr;
HG_Addr_self(state.hg_class,&self_addr);
HG_Addr_to_string(state.hg_class, hostname, &hostname_size, self_addr);
printf("Server running at address %s\n",hostname);
state.hg_context = HG_Context_create(state.hg_class);
assert(state.hg_context != NULL);
hg_id_t rpc_id = MERCURY_REGISTER(state.hg_class, "save", save_in_t, save_out_t, save);
/* Attach the local server_state to the RPC so we can get a pointer to it when
* the RPC is invoked. */
ret = HG_Register_data(state.hg_class, rpc_id, &state, NULL);
do
{
unsigned int count;
do {
ret = HG_Trigger(state.hg_context, 0, 1, &count);
} while((ret == HG_SUCCESS) && count);
HG_Progress(state.hg_context, 100);
} while(1);
ret = HG_Context_destroy(state.hg_context);
assert(ret == HG_SUCCESS);
ret = HG_Finalize(state.hg_class);
assert(ret == HG_SUCCESS);
return 0;
}
hg_return_t save(hg_handle_t handle)
{
hg_return_t ret;
save_in_t in;
// Get the server_state attached to the RPC.
const struct hg_info* info = HG_Get_info(handle);
server_state* stt = HG_Registered_data(info->hg_class, info->id);
ret = HG_Get_input(handle, &in);
assert(ret == HG_SUCCESS);
rpc_state* my_rpc_state = (rpc_state*)calloc(1,sizeof(rpc_state));
my_rpc_state->handle = handle;
my_rpc_state->filename = strdup(in.filename);
my_rpc_state->size = in.size;
my_rpc_state->buffer = calloc(1,in.size);
ret = HG_Bulk_create(stt->hg_class, 1, &(my_rpc_state->buffer),
&(my_rpc_state->size), HG_BULK_WRITE_ONLY, &(my_rpc_state->bulk_handle));
assert(ret == HG_SUCCESS);
/* initiate bulk transfer from client to server */
ret = HG_Bulk_transfer(stt->hg_context, save_bulk_completed,
my_rpc_state, HG_BULK_PULL, info->addr, in.bulk_handle, 0,
my_rpc_state->bulk_handle, 0, my_rpc_state->size, HG_OP_ID_IGNORE);
assert(ret == HG_SUCCESS);
ret = HG_Free_input(handle, &in);
assert(ret == HG_SUCCESS);
return HG_SUCCESS;
}
hg_return_t save_bulk_completed(const struct hg_cb_info *info)
{
assert(info->ret == 0);
rpc_state* my_rpc_state = info->arg;
hg_return_t ret;
FILE* f = fopen(my_rpc_state->filename,"w+");
fwrite(my_rpc_state->buffer, 1, my_rpc_state->size, f);
fclose(f);
printf("Writing file %s\n", my_rpc_state->filename);
save_out_t out;
out.ret = 0;
ret = HG_Respond(my_rpc_state->handle, NULL, NULL, &out);
assert(ret == HG_SUCCESS);
(void)ret;
HG_Bulk_free(my_rpc_state->bulk_handle);
HG_Destroy(my_rpc_state->handle);
free(my_rpc_state->filename);
free(my_rpc_state->buffer);
free(my_rpc_state);
return HG_SUCCESS;
}
在服务器上,rpc_state 结构将用于跟踪有关正在进行的operation的信息。特别是,它包含正在进行的 RPC 的 hg_handle_t 对象,以及为接收数据而暴露的本地缓冲区的 hg_bulk_t 对象。
收到 RPC 后,我们进入save
回调。此函数分配一个本地缓冲区来接收数据并使用 HG_Bulk_create 公开它。我们使用 HG_Bulk_transfer 发出 RDMA 操作,指定 HG_BULK_PULL 类型的操作,并将 save_bulk_completed 作为回调,以便在 RDMA 操作完成后调用。
需要注意的是,此函数立即返回,此时 RDMA 操作尚未完成。save
回调将返回,Mercury progress 循环将继续运行,最终在 RDMA 操作完成时调用 save_bulk_completed。
请注意,我们不会在save
回调中响应客户端,而是在 save_bulk_completed 回调中响应,因此save
回调不会破坏 RPC 的 hg_handle_t 对象。该对象在 save_bulk_completed 中保留和释放。
啊,回调……(现在你明白了 Margo 和 Thallium 是多么容易)。
序列化复杂的数据结构
让我们回到序列化/反序列化数据结构。在之前的教程中,我们一直使用可以使用 Mercury 的 MERCURY_GEN_PROC 宏定义的结构。如果结构包含指针,事情会变得更加复杂。假设我们有一个 int_list_t 类型,它表示一个指向整数链表的指针。
typedef struct int_list {
int32_t value;
struct int_list* next;
} *int_list_t;
我们需要定义一个函数 hg_return_t hg_proc_int_list_t(hg_proc_t proc, void *data)。更一般地,对于我们想要发送或接收的任何自定义类型 X,并且还没有使用 Mercury 宏创建,我们需要一个形式为 hg_return_t hg_proc_X(hg_proc_t proc, void *data) 的函数。在我们的例子中,这个函数如下所示。
types.h
#ifndef __TYPES_H
#define __TYPES_H
#include <mercury.h>
typedef struct int_list {
int32_t value;
struct int_list* next;
} *int_list_t;
static inline hg_return_t hg_proc_int_list_t(hg_proc_t proc, void* data)
{
hg_return_t ret;
int_list_t* list = (int_list_t*)data;
hg_size_t length = 0;
int_list_t tmp = NULL;
int_list_t prev = NULL;
switch(hg_proc_get_op(proc)) {
case HG_ENCODE:
tmp = *list;
// find out the length of the list
while(tmp != NULL) {
tmp = tmp->next;
length += 1;
}
// write the length
ret = hg_proc_hg_size_t(proc, &length);
if(ret != HG_SUCCESS)
break;
// write the list
tmp = *list;
while(tmp != NULL) {
ret = hg_proc_int32_t(proc, &tmp->value);
if(ret != HG_SUCCESS)
break;
tmp = tmp->next;
}
break;
case HG_DECODE:
// find out the length of the list
ret = hg_proc_hg_size_t(proc, &length);
if(ret != HG_SUCCESS)
break;
// loop and create list elements
*list = NULL;
while(length > 0) {
tmp = (int_list_t)calloc(1, sizeof(*tmp));
if(*list == NULL) {
*list = tmp;
}
if(prev != NULL) {
prev->next = tmp;
}
ret = hg_proc_int32_t(proc, &tmp->value);
if(ret != HG_SUCCESS)
break;
prev = tmp;
length -= 1;
}
break;
case HG_FREE:
tmp = *list;
while(tmp != NULL) {
prev = tmp;
tmp = prev->next;
free(prev);
}
ret = HG_SUCCESS;
}
return ret;
}
#endif
任何 proc 函数都必须包含三个部分,由开关分隔。当 proc 句柄将现有对象序列化到缓冲区中时,使用 HG_ENCODE 部分。当 proc 句柄从其缓冲区的内容创建新对象时,使用 HG_DECODE 部分。释放对象时使用 HG_FREE 部分,例如调用 HG_Free_input 或 HG_Free_output 时。注意这里我们处理的类型是 int_list_t,所以 void* 数据参数实际上是一个指向 int_list_t 的指针,它本身就是一个指向结构的指针。我们使用 hg_proc_int32_t 和 hg_proc_hg_size_t 函数分别序列化/反序列化 int32_t 和 hg_size_t。大多数基本数据类型都在 Mercury 中定义了这样的函数。要序列化/反序列化原始内存,可以使用 hg_proc_raw(hg_proc_t proc, void* data, hg_size_t size),它将复制 data 指向的内存内容的 size 个字节。
编译水星库
mercury-hpc/mercury: Mercury is a C library for implementing RPC, optimized for HPC. (github.com)
其中的一些说明:
git clone --recursive https://github.com/mercury-hpc/mercury.git
下载下来后,cd 进入目录,然后执行:
cd mercury-X
mkdir build
cd build
ccmake ..
然后会出现一个UI界面,按C,让cmake生成cache,然后ccmake就会读到cache,并显示下面的列表,这就是ccmake 将cmake的配置项都列了出来,让用户直观的看到了项目有哪些配置项:
BUILD_SHARED_LIBS ON (or OFF if the library you link
against requires static libraries)
BUILD_TESTING ON/OFF
Boost_INCLUDE_DIR /path/to/include/directory
CMAKE_INSTALL_PREFIX /path/to/install/directory
MERCURY_ENABLE_DEBUG ON/OFF
MERCURY_TESTING_ENABLE_PARALLEL ON/OFF
MERCURY_USE_BOOST_PP ON
MERCURY_USE_CHECKSUMS ON/OFF
MERCURY_USE_SYSTEM_BOOST ON/OFF
MERCURY_USE_SYSTEM_MCHECKSUM ON/OFF
MERCURY_USE_XDR OFF
NA_USE_BMI ON/OFF
NA_USE_MPI ON/OFF
NA_USE_OFI ON/OFF
NA_USE_PSM ON/OFF
NA_USE_PSM2 ON/OFF
NA_USE_SM ON/OFF
NA_USE_UCX ON/OFF
按enter可以选择开关选项,按t可以显示更多的选项,如依赖库路径,头文件路径等。
然后按g 生成makefile,然后就可以make编译和make install 安装了。
了解 RPC 和 ULT 模型
注:
- Margo 是一个 C 库,帮助开发基于 RPC 和 RDMA 的分布式服务。(在mercury层上面)https://mochi.readthedocs.io/en/latest/margo.html
- Mochi 一个软件定义存储的项目,( A Software Defined Storage Approach to Exascale Storage Services)
在使用 Margo 或 Thallium 开发 Mochi 服务时,记住 RPC 在到达服务器时如何转换为用户级线程 (ULT) 会很有用。
下图总结了当客户端向服务器发送 RPC 时会发生什么,服务器端的 RPC 处理程序包括一些 RDMA 操作。在此图中,我们仅显示客户端的一个执行流,假设它已在没有 Mercury progress thread的情况下初始化了 Margo(或 Thallium)。在客户端上使用 Mercury progress thread的情况类似,因为progress thread只是代表调用者线程(caller thread)处理网络活动。
此图显示了一个使用 margo_iforward 的客户端,它以非阻塞方式向服务器发送 RPC。margo_forward 的情况可以看作是相同的场景,但是margo_wait 在margo_iforward 之后立即被调用。在Thallium中,等效代码将使用 callable_remote_procedure 对象的异步成员函数。

解释
(https://mochi.readthedocs.io/en/latest/general/03_rpc_model.html)
margo_forward 和 margo_iforward 首先调用序列化函数(由用户在使用 MARGO_REGISTER 注册 RPC 时提供)将 RPC 参数序列化到输入缓冲区中。Mercury 然后向服务器发送一个请求,包括这个缓冲区。
在服务器中,Mercury progress 循环(可能在专用执行流上执行)最终看到请求并调用相应的回调(黄色)。此回调已由用户代码中的 DEFINE_MARGO_RPC_HANDLER 自动生成。此回调 (1) 查找应该在其中执行 RPC 的 Argobots 池,并且 (2) 在该池中创建一个 ULT。此 ULT 将运行用户的 处理RPC 的程序。
处理RPC程序的 ULT (RPC handler ULTs)发布在一个池中,可能使用不同的执行流 (ES)(处理),该池在Mercury progress 循环中使用。例如,当调用 margo_init(..., 1, 8) 时,将创建 8 个 执行流 (ES) 以及一个共享池, 处理RPC程序的 ULT (RPC handler ULTs) 将被放置(到这个共享池中)。当其中一个 执行流 (ES) 空闲时,它会从池中拉出一个 ULT 并执行它。
通常,RPC 处理程序(RPC handler )将通过调用 margo_get_input 反序列化 RPC 的参数开始。这会调用用户提供的序列化函数将 Mercury 缓冲区的内容反序列化为用户的输入数据结构。
使用 margo_bulk_transfer 执行bulk transfer (RDMA)时,ULT 让 Mercury progress 循环执行传输。同时,这个 ULT 线程抑制/让步/让出时间片,以便它所在的 执行流 (ES) 可以执行其他 ULT(例如其他 RPC 请求)。
Mercury progress 循环最终执行 RDMA 操作并通知calling ULT。calling ULT被标记为就绪,最终将恢复。
当 RPC 处理程序(RPC handler)调用 margo_respond 向客户端发送响应时,它首先调用用户提供的序列化函数将repson encode到 Mercury 的缓冲区中,然后让出时间片,等待 Mercury progress 循环发送repson 并允许 执行流 (ES)潜在地同时执行其他 RPC。
一旦 Mercury 发送repson ,Mercury progress 循环就会通知 RPC handler ULT,该处理程序最终会恢复并完成。
最后,margo_wait 在客户端完成。然后客户端可以在 RPC handler上调用 margo_get_output 以使用用户提供的反序列化函数反序列化 RPC 的输出。
注意
无论 Mercury 进度循环(progress loop)是否在单独的 ULT 上运行,此模型仍然有效。确实你可能会注意到,从单个 RPC 操作的角度来看,这里显示的服务器上的两个 ES 可以合并为一个。将 ES 专用于 Mercury progress 的优点是多个并发的 RPC 处理程序可以以最小的干扰依赖它。如果 Mercury 进度循环(progress loop)与 RPC 处理程序在相同的 ES 中运行,则在处理程序中调用 margo_respond 可能会让给另一个(可能长时间运行的)RPC handle而不是进度循环,从而延迟第一个 RPC handle的完成。
DAOS中的ULT https://download.csdn.net/download/bandaoyu/86400579
OFI Intel® MPI Library 2019 Over Libfabric* https://www.intel.cn/content/www/cn/zh/developer/articles/technical/mpi-library-2019-over-libfabric.html
A. 流水线批量数据传输
在一个常见的用例中,服务器可能会在执行请求的调用之前等待接收到全部数据。 然而,通过流水线传输,实际上可以在数据传输时开始处理数据,避免为整个 RMA 传输支付延迟成本。 请注意,尽管我们在下面的示例中重点关注这一点,但如果 RPC 服务器没有足够的内存来处理需要发送的所有数据,使用此技术也可能特别有用,在这种情况下,它还需要在处理时传输数据
RPC 客户端代码的简化版本如下所示
代码语言:c
复制
#define BULK_NX 16
#define BULK_NY 128
int main(int argc, char *argv[])
{
hg_id_t rpc_id;
write_in_t in_struct;
write_out_t out_struct;
hg_request_t rpc_request;
int buf[BULK_NX][BULK_NY];
hg_bulk_segment_t segments[BULK_NX];
hg_bulk_t bulk_handle = HG_BULK_NULL;
/* Initialize the interface */
[...]
/* Register RPC call */
rpc_id = HG_REGISTER("write",
write_in_t, write_out_t);
/* Provide data layout information */
for (i = 0; i < BULK_NX ; i++) {
segments[i].address = buf[i];
segments[i].size = BULK_NY * sizeof(int);
}
/* Create bulk handle with segment info */
HG_Bulk_handle_create_segments(segments,
BULK_NX, HG_BULK_READ_ONLY, &bulk_handle);
/* Attach bulk handle to input parameters */
[...]
in_struct.bulk_handle = bulk_handle;
/* Send RPC request */
HG_Forward(server_addr, rpc_id,
&in_struct, &out_struct, &rpc_request);
/* Wait for RPC completion and response */
HG_Wait(rpc_request, HG_MAX_IDLE_TIME,
HG_STATUS_IGNORE);
/* Get output parameters */
[...]
ret = out_struct.ret;
/* Free bulk handle */
HG_Bulk_handle_free(bulk_handle);
/* Finalize the interface */
[...]
}
客户初始化时,它会注册RPC调用它想要发送。 因为此调用涉及非连续的批量数据转换器,所以记忆段描述了创建和注册的内存区域。 由此产生的Bulk_handle Isthen与其他CallParameter一起传递给HG_Forward调用。 然后,可以在请求完成后等待响应并免费的thebulk句柄(将来也会发送通知可能允许较早的散装句柄,因此可以取消内存的内存)。管道上的机制发生在服务器上。 ,要照顾批量转移。 管道本身具有HEREA固定管道尺寸和管道缓冲区大小。 RPC服务器代码简化了
代码语言:c
复制
#define PIPELINE_BUFFER_SIZE 256
#define PIPELINE_SIZE 4
int rpc_write(hg_handle_t handle)
{
write_in_t in_struct;
write_out_t out_struct;
hg_bulk_t bulk_handle;
hg_bulk_block_t bulk_block_handle;
hg_bulk_request_t bulk_request[PIPELINE_SIZE];
void *buf;
size_t nbytes, nbytes_read = 0;
size_t start_offset = 0;
/* Get input parameters and bulk handle */
HG_Handler_get_input(handle, &in_struct);
[...]
bulk_handle = in_struct.bulk_handle;
/* Get size of data and allocate buffer */
nbytes = HG_Bulk_handle_get_size(bulk_handle);
buf = malloc(nbytes);
/* Create block handle to read data */
HG_Bulk_block_handle_create(buf, nbytes,
HG_BULK_READWRITE, &bulk_block_handle);
/* Initialize pipeline and start reads */
for (p = 0; p < PIPELINE_SIZE; p++) {
size_t offset = p * PIPELINE_BUFFER_SIZE;
/* Start read of data chunk */
HG_Bulk_read(client_addr, bulk_handle,
offset, bulk_block_handle, offset,
PIPELINE_BUFFER_SIZE, &bulk_request[p]);
}
while (nbytes_read != nbytes) {
for (p = 0; p < PIPELINE_SIZE; p++) {
size_t offset = start_offset +
p * PIPELINE_BUFFER_SIZE;
/* Wait for data chunk */
HG_Bulk_wait(bulk_request[p],
HG_MAX_IDLE_TIME, HG_STATUS_IGNORE);
nbytes_read += PIPELINE_BUFFER_SIZE;
/* Do work (write data chunk) */
write(buf + offset, PIPELINE_BUFFER_SIZE);
/* Start another read */
offset += PIPELINE_BUFFER_SIZE * 51 PIPELINE_SIZE;
if (offset < nbytes) {
HG_Bulk_read(client_addr,
bulk_handle, offset,
bulk_block_handle, offset,
PIPELINE_BUFFER_SIZE,
&bulk_request[p]);
} else {
/* Start read with remaining piece */
}
}
start_offset += PIPELINE_BUFFER_SIZE
* PIPELINE_SIZE;
}
/* Free block handle */
HG_Bulk_block_handle_free(bulk_block_handle);
free(buf);
/* Start sending response back */
[...]
out_struct.ret = ret;
HG_Handler_start_output(handle, &out_struct);
}
int main(int argc, char *argv[])
{
/* Initialize the interface */
[...]
/* Register RPC call */
HG_HANDLER_REGISTER("write", rpc_write,
write_in_t, write_out_t);
while (!finalized) {
/* Process RPC requests (non-blocking) */
HG_Handler_process(0, HG_STATUS_IGNORE);
}
/* Finalize the interface */
[...]
}
代码语言:txt
复制
每个RPC服务器初始化后,都必须绕过HG_HANDLER_PROCESS调用,该调用将等待新的RPCRequests并执行相应的注册回调(在同一线程或新线程中取决于用户需求)。 必经请求的请求,用于获取要传输的数据的总尺寸的Bulk_handle参数可以分配适当大小的缓冲区并启动批量的DataTransfers。 在此示例中,管道尺寸设置为4,并且Pipeline缓冲区大小设置为256,这意味着启动了4个256个字节的RmareQuests。 然后,可以等待第一个256个字节到达并进行处理。 当它处理时,其他零件可能会到达。 一旦一件被处理了一件,就开始了iSAT阶段4的新的RMA转移,并且可以等待下一个件,然后对其进行处理。 请注意,虽然在客户端上注册的内存区域是不合格的,但hg_bulk_read呼叫theserver将其显示为连续区域,简化了服务器代码。 此外,可以给出逻辑偏移(相对于数据的开头)单独移动数据片,而大量数据接口将映射从连续逻辑偏移到非连接的客户端内存区域。我们继续此过程,直到所有过程 数据已被读取 /处理,响应(即功能调用的结果)可以发送回。 同样,我们仅通过调用HG_HANDLER_START_OUTPUT调用来开始发送响应,并且仅通过调用HG_HANDLER_PROCESS来测试其完成,在这种情况下,与响应相关的资源将受到影响。 请注意,所有函数都支持异步执行,如果需要,可以在事件驱动的代码中使用Mercury(HG)
B.网络插件和测试环境
代码语言:txt
复制
截至本文撰写的日期已经开发了两个插件,以说明网络抽象层的功能。 此时,尚未优化插件的性能。 一个建立在BMI顶部[10]。 但是,AS我们已经在第二节中指出的是,BMI并未有效利用已定义的网络行动层和单方面的批量数据传输结构。 另一个建立在MPI [17]的顶部,该[17]仅提供完整的RMA语义[18]最近的MPI3 [19]。 许多MPI实现,特别是已经安装的机器的Thosedelas,尚无ProvideAll MPI3功能。 由于尚未将BMI移植到室内HPC系统,以说明功能和测量性能结果,因此我们仅考虑MPI插件本文。 该插件能够在现有的HPCSystems上运行,仅限于MPI-2功能(例如Cray Systems),在两面消息的顶部实现批量数据传输。在实践中,这意味着对于每个批量数据传输, 需要将控制消息发送到theclient,以请求发送或接收数据。 然后,可以使用进度线程输入进度功能来实现转移的进度。对于测试,我们使用两个不同的HPC系统。 onis是一个Infiniband QDR 4X群集,带有mvapich [20] 1.8.1,另一个是带有Cray MPT的Cray Xe6 [21] 5.6.0