Here’s a minimal example that shows the behaviour. This launches four cooperative grids on four parallel streams. Each grid has (#SM / 3 + 2) threadblocks. Each threadblock occupies a full SM due to claiming the maximum amount of dynamic shared memory. So only two full grids will fit on the GPU at once.
The test kernel simply records, per threadblock, the time (%globaltimer) at launch, after a first grid sync, after waiting for a given number of milliseconds after launch, and after a second grid sync.
// cooperative_launch.cu
// Build and run with:
// $ nvcc -o cooperative_launch cooperative_launch.cu -arch=all
// $ CUDA_VISIBLE_DEVICES=0 ./cooperative_launch
#include <cooperative_groups.h>
#include <cuda.h>
#include <cstdint>
#include <cstdio>
struct Result {
uint64_t t0;
uint32_t t1, t2, t3;
uint32_t sm, tb;
int grid;
};
__device__ uint64_t globaltimer() {
uint64_t t;
asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)::"memory");
return t;
}
__global__ void __launch_bounds__(1) test_kernel(Result *out, int grid, int offset, int ms) {
uint64_t t0 = globaltimer();
cooperative_groups::this_grid().sync();
uint32_t t1 = globaltimer() - t0;
uint32_t t2 = t1;
while (t2 < ms * 1000000) {
t2 = globaltimer() - t0;
}
cooperative_groups::this_grid().sync();
uint32_t t3 = globaltimer() - t0;
uint32_t smid;
asm volatile("mov.u32 %0, %%smid;" : "=r"(smid));
out[offset + blockIdx.x] = Result{t0, t1, t2, t3, smid, blockIdx.x, grid};
}
__global__ void print_results(Result *in, int nresults) {
if (threadIdx.x + blockIdx.x == 0) {
uint64_t t_min = in[0].t0;
for (int i = 1; i < nresults; ++i) {
t_min = min(t_min, in[i].t0);
}
for (int i = 0; i < nresults; ++i) {
auto &r = in[i];
printf("%3d Grid %2d, TB %2d, SM %3d - Launched at %lluns, grid sync after %uns, done "
"waiting after %uns, grid sync after %uns\n",
i, r.grid, r.tb, r.sm, r.t0 - t_min, r.t1, r.t2, r.t3);
}
}
}
int main() {
constexpr int ngrids = 4;
cudaStream_t stream[ngrids];
for (int i = 0; i < ngrids; ++i) {
cudaStreamCreateWithFlags(&stream[i], cudaStreamNonBlocking);
}
int device = 0, can_coop = 0, max_smem = 0, sm_count = 0;
cudaGetDevice(&device);
cudaDeviceGetAttribute(&can_coop, cudaDevAttrCooperativeLaunch, device);
cudaDeviceGetAttribute(&max_smem, cudaDevAttrMaxSharedMemoryPerBlockOptin, device);
cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount, device);
// Use all smem to ensure a single TB per SM
cudaFuncSetAttribute(test_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, max_smem);
// Choose grid size so only two full grids will fit on the GPU, with some SMs idle.
int tbs = (sm_count / 3) + 2;
printf("SM count %d, can coop %d, max smem %d. Grids %d x %d\n", sm_count, can_coop, max_smem,
ngrids, tbs);
Result *bfr;
cudaMalloc(&bfr, 4096 * sizeof(Result));
for (int i = 0; i < ngrids; ++i) {
int offset = i * tbs;
int ms = (i == 0) ? 9 : 1; // First grid runs for longer
void *args[] = {&bfr, &i, &offset, &ms};
cudaLaunchCooperativeKernel(test_kernel, dim3(tbs, 1, 1), dim3(1, 1, 1), args, max_smem,
stream[i]);
}
cudaDeviceSynchronize();
print_results<<<1, 1, 0, stream[0]>>>(bfr, ngrids * tbs);
cudaDeviceSynchronize();
return 0;
}
For every threadblock this prints the grid it belongs to, the blockIdx.x, the %smid, and the times as described above.
On H100, RTX5090, and RTX Pro 6000 (i.e. sm_90 and sm_120) this consistently shows all threadblocks in the first two grids launching at ~0ms, all blocks in the the third grid launching at ~1ms, and the fourth grid at ~2ms. This is exactly what I would expect (note the first grid runs for 9ms, the others for 1ms).
On RTX 5000 Ada, RTX A6000, and L40S I consistently see grids launched in a “split” fashion, with some threadblocks taking the remaining SMs while others have to wait for a previous grid to finish:
147 Grid 3, TB 0, SM 100 - Launched at 1004544ns, grid sync after 1001472ns, done waiting after 1001472ns, grid sync after 2000896ns
[42 lines omitted ...]
190 Grid 3, TB 43, SM 53 - Launched at 1004544ns, grid sync after 1001472ns, done waiting after 1001472ns, grid sync after 2000896ns
191 Grid 3, TB 44, SM 69 - Launched at 2004992ns, grid sync after 0ns, done waiting after 1000448ns, grid sync after 1000448ns
[3 lines omitted...]
195 Grid 3, TB 48, SM 123 - Launched at 2004992ns, grid sync after 0ns, done waiting after 1000448ns, grid sync after 1000448ns