Messages¶
RAJA::messages provides a portable interface and type-safe way to store messages that can
be handled at a later time. In this context, a message will store function arguments and handled
typically means passed to a function. For example, one use case could be logging error messages
from the GPU to file. In this case, arguments from the GPU can be stored and passed
to function that prints to a file on the CPU.
Warning
This capability is new. We would like users to try it out and give feedback to improve it.
How to manage messages?¶
All messages are handled via the message_manager, which is responsible for
storing callbacks and a list of messages. For the purposes of RAJA::messages,
a single message can be thought of:
MsgHeader: helper data internal toRAJAMsgArgs: a tuple of arguments needed to pass to the function
To create the message_manager:
auto host_allocator = RAJA::ResourceAllocator<char, decltype(res_host)>{res_host,
RAJA::resources::MemoryAccess::Pinned};
auto msg_manager = RAJA::make_message_manager(buf_sz, res_host, host_allocator);
buf_sz is the size of the buffer that stores messages. res_host is the resource of the
execution policy, which determines the memory space in which messages will be stored. For GPU resources, for example,
this is PINNED memory.
Subscribing callbacks¶
To create a specific message type, callbacks must subscribe first. This can be done in two ways:
subscribe(Callable): Subscribing with just a callable will create a new type of message with the type depending on the parameters.subscribe(msg_queue_id, Callable): Subscribing withmsg_queue_idand a callable will append the new callback to the already existing callback list.
As an example for subscribing with both methods:
auto cpu_msg_queue = msg_manager.subscribe<RAJA::mpsc_queue>(
[](const my_string<128>& str, int* ptr, int idx, int value) {
std::cout << "\n " << str.c_str() << " " << ptr << " a[" << idx << "] = " << value << "\n";
}
);
auto err_callback = [] (const my_string<128>& str, int* ptr, int idx, int value) {
std::cerr << "\n " << str.c_str() << " " << ptr << " a[" << idx << "] = " << value << "\n";
};
msg_manager.subscribe(cpu_msg_queue.get_id(), err_callback);
Unsubscribing callbacks¶
If a particular callback no longer needs to be subscribed to a message type, then the callback can be unsubcribed. This can be achieved in three ways:
unsubscribe(msg_queue_id, Callable): Looks for a specific callback that is subscribed to a particular message. If the callback is subscribed, remove from callback list. Otherwise, throws an exception.unsubscribe_all(msg_queue_id): Removes all callbacks subscribed to a particular messageunsubscribe_all(): Removes all callbacks from map.erase_all(msg_queue_id): Erases all callbacks and the map entry.
An example for unsubscribing a callback:
msg_manager.unsubscribe(cpu_msg_queue.get_id(), err_callback);
Publishing messages¶
Messages can be published/stored in a MessageQueue. These are non-owning adapters to the MessageBus, which is
responsible for storing all messages. The MessageQueue will contain additional type information as well as
the msg_queue_id. A queue is created once a callback is subscribed to a new message type. Since MessageQueue is
non-owning, these can be copied.
Here is how the MessageQueue can be used to publish messages:
RAJA::forall<RAJA::seq_exec>(res_host, RAJA::RangeSegment(0, N), [=] (int i) {
if (a[i] < 0) {
cpu_msg_queue.try_post_message("message from RAJA seq_exec loop", a, i, a[i]);
}
c[i] = a[i] + b[i];
});
Handling messages¶
Lastly, there needs to be a way to direct messages to the corresponding callback(s).
This is handled with the message_manager, which forces a synchronize on the resource provided.
msg_manager.wait_all();
Handling messages with a GPU¶
Here is a complete example using the RAJA::messages with a GPU kernel.
auto gpu_allocator = RAJA::ResourceAllocator<char, decltype(res)>{res,
RAJA::resources::MemoryAccess::Pinned};
auto msg_manager = RAJA::make_message_manager<gpu_policy>(message_sz*10, gpu_allocator);
auto log = [](const my_string<32>& str, int idx, int value) {
std::cout << "[INFO]: " << str.c_str() << "[" << idx << "] = " << value << "\n";
};
// Create two types of messages:
// queue1 stores one message type and prints with one callback
// queue2 stores the other types of messages and forwards the message to multiple callbacks
auto msg_queue1 = msg_manager.subscribe<RAJA::mpsc_queue>(log);
msg_manager.subscribe(msg_queue1.get_id(),
[](const my_string<32>& str, int idx, int value) {
std::cout << "echo msg: " << str.c_str() << "[" << idx << "] = " << value << "\n";
}
);
auto msg_queue2 = msg_manager.subscribe<RAJA::mpsc_queue>(log);
RAJA::forall<gpu_policy>(RAJA::RangeSegment(0, N),
[=] RAJA_DEVICE (int i) {
if (d_a1[i] < 0 && i == 1) {
msg_queue1.try_post_message("d_a1", i, d_a1[i]);
}
if (d_b1[i] > 0 && i == 1) {
msg_queue2.try_post_message("d_b1", i, d_b1[i]);
}
d_c1[i] = d_a1[i] + d_b1[i];
});
msg_manager.wait_all();
Note
In this example, gpu_policy depends on the build (i.e., CUDA, HIP), res is the default resource for
gpu_policy, and d_* arrays are allocated for the device. These are removed above from the example to just
show the RAJA::messages interface.
Handling messages across multiple streams¶
Here is a complete example using the RAJA::messages with multiple resources.
auto allocator1 = RAJA::ResourceAllocator<char, decltype(res_gpu1)>{res_gpu1,
RAJA::resources::MemoryAccess::Pinned};
auto gpu_logger1 = RAJA::make_message_manager(buf_sz, res_gpu1, allocator1);
auto gpu_msg_queue1 = gpu_logger1.subscribe<RAJA::mpsc_queue>(
[](int* ptr, int idx, int value) {
std::cout << "\n gpu stream 1: pointer (" << ptr << ") d_array1[" << idx << "] = " << value << "\n";
}
);
auto allocator2 = RAJA::ResourceAllocator<char, decltype(res_gpu2)>{res_gpu2,
RAJA::resources::MemoryAccess::Pinned};
auto gpu_logger2 = RAJA::make_message_manager(buf_sz, res_gpu2, allocator2);
auto gpu_msg_queue2 = gpu_logger2.subscribe<RAJA::mpsc_queue>(
[](int* ptr, int idx, int value) {
std::cout << "\n gpu stream 2: pointer (" << ptr << ") d_array2[" << idx << "] = " << value << "\n";
}
);
RAJA::forall<EXEC_POLICY>(res_gpu1, RAJA::RangeSegment(0,N),
[=] RAJA_HOST_DEVICE (int i) {
d_array1[i] = i;
gpu_msg_queue1.try_post_message(d_array1, i, d_array1[i]);
}
);
// Log message for stream 1
gpu_logger1.wait_all();
RAJA::forall<EXEC_POLICY>(res_gpu2, RAJA::RangeSegment(0,N),
[=] RAJA_HOST_DEVICE (int i) {
d_array2[i] = -1;
gpu_msg_queue2.try_post_message(d_array2, i, d_array2[i]);
}
);
RAJA::forall<EXEC_POLICY>(res_gpu1, RAJA::RangeSegment(0,N),
[=] RAJA_HOST_DEVICE (int i) {
d_array1[i] *= -1;
gpu_msg_queue1.try_post_message(d_array1, i, d_array1[i]);
}
);
// Log message for stream 2
gpu_logger2.wait_all();
// Log message for stream 1
gpu_logger1.wait_all();
Note
In this example, res_gpu1 and res_gpu2 depend on the build (i.e., CUDA, HIP), EXEC_POLICY the
exeuction policy for the loops (also depends on the build), and d_* arrays are allocated for the device
while h_* are allocated for the host. These are removed from the example above to just show the
RAJA::messages interface.
Message queue policies¶
Message queues can support various policies depending on the requirements of that queue, such as the number of producers/consumers or the type of atomic operations.
Message queue Policies
Brief description
spsc
Supports a single producer, single consumer; i.e., no atomic operations
mpsc
Supports multiple producers, a single consumer; i.e., requires atomic operations. Automatically determines which atomic operations to use.
Note
Producers and consumers can not operate at the same time
Building and running the example¶
The example examples/messages-forall.cpp is built from the RAJA source tree when:
ENABLE_EXAMPLES=On
To run the example:
./bin/messages-forall
This example will show how callbacks can be subscribed to various types of messages as
well as how to publish messages on multiple different platforms. For the purposes of
this example, output for messages will be printed using std::cout.
Application considerations¶
There are several things to consider when using RAJA::messages in an application.
The
MessageQueuewith the correct argument types is created when a callback subscribes. Certain patterns will cause this storage to slowly grow overtime. For example, creating a newMessageQueueevery function call within a loop. Therefore, applications that use this pattern will want to unsubscribe at some point to avoid running out of memory.Upon creation of the
MessageManager, theMessageBuswill be allocated with some size. This can be resized; however, resizing will force a synchronize and will loss any messages currently stored. Also, by default, the allocation is done through the resource, which can be less performant depending on the resource.Since the
MessageQueueis a fixed size, there is a chance of lossing messages. Thetry_post_messagefunction will return aboolean. This will betrueif the message is successfully added to the queue; otherwise, this isfalse.
Allocators¶
During the creation of the MessageManager, a custom
allocator needs to be provided. This allocator should follow the C++ standard
requirements for an allocator and will be used to allocate/deallocate
the message bus. The message bus will always read messages on the host
to forward the message to the respect callback(s). However, depending on the build
and where messages are stored, messages may be written on either the host
or the device. Therefore, in cases where messages are stored on the device, the allocator
needs to allocate memory that is accessible on both the host and device.
Note
When using the move assignment operator, the MessageManager
assumes that there are no currently pending messages.