Search code examples
cudapycuda

CUDA contexts, streams, and events on multiple GPUs


TL;DR version: "What's the best way to round-robin kernel calls to multiple GPUs with Python/PyCUDA such that CPU and GPU work can happen in parallel?" with a side of "I can't have been the first person to ask this; anything I should read up on?"

Full version:

I would like to know the best way to design context, etc. handling in an application that uses CUDA on a system with multiple GPUs. I've been trying to find literature that talks about guidelines for when context reuse vs. recreation is appropriate, but so far haven't found anything that outlines best practices, rules of thumb, etc.

The general overview of what we're needing to do is:

  • Requests come in to a central process.
  • That process forks to handle a single request.
  • Data is loaded from the DB (relatively expensive).

The the following is repeated an arbitrary number of times based on the request (dozens):

  • A few quick kernel calls to compute data that is needed for later kernels.
  • One slow kernel call (10 sec).

Finally:

  • Results from the kernel calls are collected and processed on the CPU, then stored.

At the moment, each kernel call creates and then destroys a context, which seems wasteful. Setup is taking about 0.1 sec per context and kernel load, and while that's not huge, it is precluding us from moving other quicker tasks to the GPU.

I am trying to figure out the best way to manage contexts, etc. so that we can use the machine efficiently. I think that in the single-gpu case, it's relatively simple:

  • Create a context before starting any of the GPU work.
  • Launch the kernels for the first set of data.
  • Record an event for after the final kernel call in the series.
  • Prepare the second set of data on the CPU while the first is computing on the GPU.
  • Launch the second set, repeat.
  • Insure that each event gets synchronized before collecting the results and storing them.

That seems like it should do the trick, assuming proper use of overlapped memory copies.

However, I'm unsure what I should do when wanting to round-robin each of the dozens of items to process over multiple GPUs.

The host program is Python 2.7, using PyCUDA to access the GPU. Currently it's not multi-threaded, and while I'd rather keep it that way ("now you have two problems" etc.), if the answer means threads, it means threads. Similarly, it would be nice to just be able to call event.synchronize() in the main thread when it's time to block on data, but for our needs efficient use of the hardware is more important. Since we'll potentially be servicing multiple requests at a time, letting other processes use the GPU when this process isn't using it is important.

I don't think that we have any explicit reason to use Exclusive compute modes (ie. we're not filling up the memory of the card with one work item), so I don't think that solutions that involve long-standing contexts are off the table.

Note that answers in the form of links to other content that covers my questions are completely acceptable (encouraged, even), provided they go into enough detail about the why, not just the API. Thanks for reading!


Solution

  • Caveat: I'm not a PyCUDA user (yet).

    With CUDA 4.0+ you don't even need an explicit context per GPU. You can just call cudaSetDevice (or the PyCUDA equivalent) before doing per-device stuff (cudaMalloc, cudaMemcpy, launch kernels, etc.).

    If you need to synchronize between GPUs, you will need to potentially create streams and/or events and use cudaEventSynchronize (or the PyCUDA equivalent). You can even have one stream wait on an event inserted in another stream to do sophisticated dependencies.

    So I suspect the answer to day is quite a lot simpler than talonmies' excellent pre-CUDA-4.0 answer.

    You might also find this answer useful.

    (Re)Edit by OP: Per my understanding, PyCUDA supports versions of CUDA prior to 4.0, and so still uses the old API/semantics (the driver API?), so talonmies' answer is still relevant.