Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Define wait(req) to use threadcall #452

Closed
wants to merge 2 commits into from
Closed

Conversation

simonbyrne
Copy link
Member

@simonbyrne simonbyrne commented Feb 24, 2021

Prompted by this discussion I tried to see if I could use @threadcall to wait on nonblocking MPI operations inside tasks.

And it seems to work! I've defined this to be a method of Base.wait, in that it acts mostly the same way (i.e. will yield until it is complete). Obviously one problem is that it is limited by the size of the libuv thread pool, but I don't think that would be a big issue.

If people are in favor, I can define analogous waitany/waitall/waitsome operations.

cc: @giordano @stevengj @vchuravy @fverdugo

@stevengj
Copy link
Contributor

Couldn’t waitall be defined as a wait method that takes an array of requests, maybe with a flag argument for waitall vs waitany?

@vchuravy vchuravy self-requested a review February 25, 2021 01:47
@simonbyrne simonbyrne closed this Feb 25, 2021
@simonbyrne simonbyrne reopened this Feb 25, 2021
Comment on lines +417 to +419
errcode = @threadcall((:MPI_Wait, libmpi), Cint,
(Ptr{MPI_Request}, Ptr{Status}),
req, stat_ref)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue is that on 32-bit windows we need to use the stdcall convention: this is usually handled by the @mpicall/@mpichk macros, but @threadcall doesn't seem to support nonstandard calling conventions.

On the other hand, it passed tests so 🤷 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(alternatively, we could disable this on 32-bit Windows, like we do the custom operator stuff)

@vchuravy
Copy link
Member

vchuravy commented Feb 25, 2021 via email

@fverdugo
Copy link

Nice to see this!

Is this approach composable? In some situations, we need to wait for a precondition (Request 1) in order to start a non-bloking communication that will generate Request 2. It would be nice to be able to compose these two requests in a asynchronous Task (or whatever object) that waits for Request 1, starts the non-blocking communication and waits for Request 2.

@simonbyrne
Copy link
Member Author

It would be good to have this be configurable, I have to think a bit more about this, but MPI implementation are free to not be threadsafe (or use libraries under the hood that are not threadsafe, I suspect that half the UCX builds are built without support for threads...)

Yes, the user would be required to initialize using MPI.Init_thread(MPI.THREAD_SERIALIZED) or MPI.Init_thread(MPI.THREAD_MULTIPLE).

Is this approach composable? In some situations, we need to wait for a precondition (Request 1) in order to start a non-bloking communication that will generate Request 2. It would be nice to be able to compose these two requests in a asynchronous Task (or whatever object) that waits for Request 1, starts the non-blocking communication and waits for Request 2.

Yes, you can do something like:

request1 = MPI.Irecv!(...)

commtask = @async begin
    wait(request1)
    request2 = MPI.Send(...)
    wait(request2)
end
# do other work
wait(commtask)

Alternatively, it might be better to just use Julia threading for this:

request1 = MPI.Irecv!(...)

commtask = Threads.@spawn begin
    MPI.Wait!(request1)
    request2 = MPI.Send(...)
    MPI.Wait!(request2)
end
# do other work
wait(commtask)

The main difference is that the first is blocking a libuv thread, the second is blocking a Julia thread. I'm not sure what other implications of this are.

@simonbyrne
Copy link
Member Author

After further thought, I don't think this is a good idea.

I think it would be better to define wait as a simple busy loop with yield, e.g.

function Base.wait(req::MPI.Request)
   while !MPI.Test(req)
       yield()
   end
end

This should work, since

If an MPI_TEST that completes a receive is repeatedly called with the same arguments,
and a matching send has been started, then the call will eventually return flag = true, unless
the send is satisfied by another receive. If an MPI_TEST that completes a send is repeatedly
called with the same arguments, and a matching receive has been started, then the call will
eventually return flag = true, unless the receive is satisfied by another send

@simonbyrne simonbyrne closed this Dec 31, 2022
@giordano giordano deleted the sb/waitthread branch January 9, 2023 14:34
@fverdugo
Copy link

@simonbyrne thanks for the new idea.

In my code I am finally doing something like

t = @async begin
   while !MPI.Test(req)
       yield()
   end
end

and then one can consume task t as usual in Julia. Perhaps we don't even need to define the new method for wait. Just add some comment on the docstrings of Test and TestAll regarding on how to use them in combination with @async.

@simonbyrne
Copy link
Member Author

Just add some comment on the docstrings of Test and TestAll regarding on how to use them in combination with @async.

That's a good idea, would you mind opening a draft PR?

@fverdugo
Copy link

That's a good idea, would you mind opening a draft PR?

Yes! I'll do that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants