Added schedule_exchange() and schedule_wait() to communication_object#190
Added schedule_exchange() and schedule_wait() to communication_object#190philip-paul-mueller wants to merge 88 commits intoghex-org:masterfrom
schedule_exchange() and schedule_wait() to communication_object#190Conversation
…t compiles though.
…here is another error.
|
I have now run everything on Säntis and with the exception of the "inplace test" in the concepts the unit tests pass. |
|
The failing in the Python bindings in CPU mode is because the library falls back to normal exchange, transparently. |
include/ghex/device/event_pool.hpp
Outdated
| struct event_pool | ||
| { | ||
| public: // constructors | ||
| event_pool(std::size_t) {}; |
There was a problem hiding this comment.
| event_pool(std::size_t) {}; | |
| event_pool(std::size_t) = default; |
include/ghex/device/event_pool.hpp
Outdated
| event_pool(event_pool&& other) noexcept = default; | ||
| event_pool& operator=(event_pool&&) noexcept = default; | ||
|
|
||
| void rewind() {}; |
There was a problem hiding this comment.
| void rewind() {}; | |
| void rewind() {} |
include/ghex/device/event.hpp
Outdated
| { | ||
| struct cuda_event | ||
| { | ||
| cuda_event() {}; |
There was a problem hiding this comment.
| cuda_event() {}; | |
| cuda_event() = default; |
include/ghex/device/cuda/stream.hpp
Outdated
| //We do not use `reserve()` to ensure that the events are initialized now | ||
| // and not in the hot path when they are actually queried. |
There was a problem hiding this comment.
I like your current version, thanks!
include/ghex/device/cuda/stream.hpp
Outdated
| * and recreating them. It requires however, that a user can guarantee | ||
| * that the events are no longer in use. | ||
| */ | ||
| void rewind_pool() |
There was a problem hiding this comment.
Yep, that sounds reasonable. So:
rewind_pool->rewindreset_pool->clear
?
| initialize_data(d, field, levels, levels_first); | ||
| data_descriptor_cpu_int_type data{d, field, levels, levels_first}; | ||
|
|
||
| cudaDeviceSynchronize(); |
msimberg
left a comment
There was a problem hiding this comment.
A few more minor comments.
| if (!m_moved) { GHEX_CHECK_CUDA_RESULT_NO_THROW(cudaEventDestroy(m_event)) } | ||
| } | ||
|
|
||
| operator bool() const noexcept { return m_moved; } |
There was a problem hiding this comment.
Hmm, this seems backwards. I would expect:
| operator bool() const noexcept { return m_moved; } | |
| operator bool() const noexcept { return !m_moved; } |
i.e. bool() is true when the event is valid, false otherwise?
I see stream has the same "backwards" semantics, so this probably comes from there.
What do you think?
There was a problem hiding this comment.
Now that I think about it, I would say that it should return true when it is valid, thus it should be !m_moved.
However, I think it should follow the same logic as stream, because everything else is just confusing.
So I would keep it that way but add a comment that explains what true means.
In any case we should check with Fabian as he is the one who added that function, thus we would have to change all of GHEX (which is not an argument against it).
There was a problem hiding this comment.
Yep, it'd be great to hear if @boeschf had other semantics in mind for it originally. Otherwise, it simply smells a bit like a bug that should be changed for both stream and event.
| while (!(m_next_event < m_events.size())) { m_events.emplace_back(cuda_event()); }; | ||
|
|
||
| const std::size_t event_to_use = m_next_event; | ||
| assert(!bool(m_events[event_to_use])); |
There was a problem hiding this comment.
Related to semantics of operator bool() for event and stream:
| assert(!bool(m_events[event_to_use])); | |
| assert(bool(m_events[event_to_use])); |
There was a problem hiding this comment.
Skipped for now, see discussion above.
| if (!m_moved) { GHEX_CHECK_CUDA_RESULT_NO_THROW(cudaStreamDestroy(m_stream)) } | ||
| } | ||
|
|
||
| operator bool() const noexcept { return m_moved; } |
There was a problem hiding this comment.
See comment about operator bool() for event. I think this is also backwards.
There was a problem hiding this comment.
Skipped for now, see discussion above.
| * | ||
| * TODO: Should the handle expose this function? | ||
| */ | ||
| void complete_schedule_exchange() |
There was a problem hiding this comment.
What do you think about making this private and if a caller needs to synchronize after a schedule_wait, just make them call wait instead?
schedule_exchange(stream);
schedule_wait(stream);
// now I really want to synchronize
wait();
IMO this would simplify the API in that wait can always be called as a wait-for-everything-to-finish regardless of how the exchange or wait was done previously. wait may of course need to call complete_schedule_exchange internally (or parts of it) for this approach to work.
There was a problem hiding this comment.
We should consider it.
There was a problem hiding this comment.
I have though about it again.
Regarding your example, while I can understand why it should work (in fact I also think it must work), I do not think that it is the right (what ever that means) way to achieve it.
The scheduled functions essentially introduce a kind of stream semantic into GHEX.
So, I would argue that to check if the transmission has finished one should synchronize with the stream passed to schedule_exchange().
The only thing that GHEX must do is, not start a new exchange before the old has finished or delete something that is still in use by a transfer that is still ongoing, which is what complete_schedule_exchange() does.
So I think that you are right when you say that it should become private as it should probably never be called directly by a user.
Instead all exchange*() functions call it to make sure that the exchange has happened and it is safe to start a new one.
Because wait() deallocate memory it must also call it to make sure that it does not delete something that is still in use.
As a side effect your example code will work and do what you want, i.e. the full synchronization.
But, as I outlined above, in my opinion, it is not the right way of doing it, but it must work.
Does this make sense?
I think that it should be private but currently the test needs them and currently I have no good idea to solve it.
| #ifdef GHEX_CUDACC | ||
| assert(has_scheduled_exchange()); | ||
| #endif |
There was a problem hiding this comment.
This doesn't belong here, or the logic is wrong elsewhere. clear() is called by complete_scheduled_exchange after the event has been reset, meaning this always fails. Should this be assert(!has_scheduled_exchange())?
There was a problem hiding this comment.
You are right, it should be assert(!...), I am just wondering why the CI "passes".
Probably because asserts are disabled?
| * exchange. A user will never have to call it directly. If there was no such | ||
| * exchange or GPU support was disabled, the function does nothing. | ||
| * | ||
| * \note This should be a private function, but the tests need them. |
There was a problem hiding this comment.
Why do the tests need this function?
There was a problem hiding this comment.
Some test calls it (https://github.com/ghex-org/GHEX/pull/190/changes#diff-c9ca950ae3be0eda232ecea6c5e5224c87bdee091f1e3a7c9efa081badda3fc2R364) but I think that this is more like a bad test.
This PR adds the the
schedule_exchange()andschedule_wait()function to the communication object.They behave similar to the regular
exchange()andwait()but they accepts an additional CUDA stream as argument.schedule_exchange()will wait with packing until all work that has been scheduled on the passed stream has finished, which removes the need for an external synchronization.It is important to note, that the function will only return after all data have been send, which is the same behaviour than
exchange().schedule_wait()is similar towait(), it will launch unpacking but will make synchronize it with the passed stream.This means that every work that is submitted to the stream after the function has returned, will not start until all unpacking has been completed.
The PR also extends the Python bindings.
The bindings are able to interpret the following Python objects:
Noneis interpreted as default stream, i..enullptr.__cuda_stream__method it is assumed to follow Nvidia's stream protocol..ptrattribute it is assumed that it follows CuPy's Stream implementation.Note:
For CPU memory
schedule_exchange()andschedule_wait()behave the exact same way asexchange()andwait(), i.e.exchange()starts to pack immediately andwait()only returns after the unpacking has finished.If CPU and GPU memory is exchanged in the same transaction then the behaviour is a mix of both, the CPU parts are immediately packed but the packing of the GPU memory synchronizes with the stream.
The same holds for
schedule_wait(), which will only return if the CPU memory has been unpacked, but will return after the unpacking of the GPU memory has been initiated.What happens exactly also depends on if the CPU memory is processed before the GPU memory.
Thus it is safe to mix the two but it is not recommended.
NOTE:
include/ghex/communication_object.hpp: Adding of theschedule_*()functions.include/ghex/device/cuda/stream.hpp: Adding of a (very simple) event pool.bindings/python/src/_pyghex/unstructured/communication_object.cpp: Updating the bindings.TODO: