Why dummy cudaMalloc speeds up interval_gather in moderngpu?

Viewed 112

I'm using moderngpu CUDA library (https://github.com/moderngpu/moderngpu)

There's a strange speed up of interval_gather function in moderngpu if there are some bytes allocated by cudaMalloc before calling it.

I call a method mgpu::interval_gather(m0.data(), 1, m1.data(), 1, m2.data(), m3.data(), context); Each of m0, m1, m2, m3 is a mgpu::mem_t array of size of 1 000 000 filled with 0s.

I do it 10 000 times and it takes about 3 seconds.

However, if I allocate some dummy memory before it:

cudaMalloc((void **)(&tmpPtr), sizeof(int));

It speeds up around 10x, taking 0.3 seconds. Why would that be?

I tried allocating the mem_t memory before, or after allocating the dummy byte, same result. I experimented with different size of allocated dummy memory, calling function multiple times, or disabling -O2 compiling flag - nothing seems to change.

However, when I tried to reduce the size of arrays I pass to interval_gather, the effect stops. It then takes 0.3 seconds regardless of whether there was a dummy alloc or not.

I ran all of this on GTX 980.

Here's a full code snippet. If magic variable is set to true, it will speed up.

#include <iostream>
#include <chrono>
#include <string>

#include <moderngpu/kernel_intervalmove.hxx>

void print_ms(std::clock_t start, std::clock_t end, std::string desc)
{
    double ms = 1000.0 * (end - start) / CLOCKS_PER_SEC;
    std::cout << desc << ": " << ms << " ms." << std::endl;
}

void fun(bool magic, mgpu::context_t &context)
{
    int n = 10000000;
    mgpu::mem_t<int> m0(n, context);
    mgpu::mem_t<int> m1(n, context);
    mgpu::mem_t<int> m2(n, context);
    mgpu::mem_t<int> m3(n, context);

    void *tmpPtr;
    cudaMalloc((void **)(&tmpPtr), sizeof(int));
    if (!magic)
        cudaFree(tmpPtr);

    for (int aa = 0; aa < 10000; aa++)
        mgpu::interval_gather(m0.data(), 1,
                              m1.data(), 1,
                              m2.data(), 
                              m3.data(),
                              context);

    if (magic)
        cudaFree(tmpPtr);
}

int main(int argc, char *argv[])
{
    mgpu::standard_context_t context(false);
    std::clock_t c0 = std::clock();

    fun(false, context);
    context.synchronize();
    std::clock_t c1 = std::clock();
    print_ms(c0, c1, "1st");

    fun(false, context);
    context.synchronize();
    std::clock_t c2 = std::clock();
    print_ms(c1, c2, "2nd");

    fun(true, context);
    context.synchronize();
    std::clock_t c3 = std::clock();
    print_ms(c2, c3, "3rd");

    return 0;
}
1 Answers

Preface: I can tell you what is happening, but I can't explain why.

TLDR: The cudaMalloc call before the mgpu::interval_gather is changing the performance of internal cudaMalloc and cudaFree calls within the interval_gather operation by about a factor of 10, and that is the performance difference you see. The critical thing is that the memory which is allocated before the mgpu calls must not be freed until after the calls.

An example of typical API timing of one call to mgpu::interval_gather in your code without the magic cudaMalloc call:

API trace:

262.99ms  80.334us  cudaMalloc
263.07ms  11.912us  cudaLaunchKernel (_ZN4mgpu16launch_box_cta_kINS_15launch_params_tILi128ELi1ELi1ELi0EEENS_6detail11transform_fIS2_EEJZNS_21merge_path_partitionsILNS_8bounds_tE1ENS_19counting_iterator_tIiiEEPiNS_6less_tIiEEEENS_5mem_tIiEET0_lT1_llT2_RNS_9context_tEEUliE_mE
263.08ms  7.3310us  cudaLaunchKernel (_ZN4mgpu16launch_box_cta_kINS_12launch_box_tIJNS_7arch_20INS_12launch_cta_tILi128ELi11ELi9ELi0EEENS_7empty_tEEENS_7arch_35INS3_ILi128ELi7ELi5ELi0EEES5_EENS_7arch_52IS4_S5_EEEEEZNS_13transform_lbsIS5_ZNS_15interval_gatherIS5_PiSF_SF_SF_EE
263.09ms  51.357us  cudaFree

GPU trace:

273.40ms  1.4400us              (1 1 1)       (128 1 1)        11        0B        0B  GeForce GTX 970         1         7  _ZN4mgpu16launch_box_cta_kINS_15launch_params_tILi128ELi1ELi1ELi0EEENS_6detail11transform_fIS2_EEJZNS_21merge_path_partitionsILNS_8bounds_tE1ENS_19counting_iterator_tIiiEEPiNS_6less_tIiEEEENS_5mem_tIiEET0_lT1_llT2_RNS_9context_tEEUliE_mEEEvSF_iDpSG_ [126]
273.40ms  5.7600us              (1 1 1)       (128 1 1)        56  5.5156KB        0B  GeForce GTX 970         1         7  _ZN4mgpu16launch_box_cta_kINS_12launch_box_tIJNS_7arch_20INS_12launch_cta_tILi128ELi11ELi9ELi0EEENS_7empty_tEEENS_7arch_35INS3_ILi128ELi7ELi5ELi0EEES5_EENS_7arch_52IS4_S5_EEEEEZNS_13transform_lbsIS5_ZNS_15interval_gatherIS5_PiSF_SF_SF_EEvT0_iT1_iT2_T3_RNS_9context_tEEUliiiNS_5tupleIJiEEESF_SF_E_SF_NSM_IJSF_EEEJSF_SF_EEEvSG_iSH_iSI_SL_DpSJ_EUliiSF_SF_E_JSF_SF_EEEvSG_iDpSH_ [127]

compared to when the magic cudaMalloc is used:

API trace:

2.32306s  4.7240us  cudaMalloc
2.32307s  7.9970us  cudaLaunchKernel (_ZN4mgpu16launch_box_cta_kINS_15launch_params_tILi128ELi1ELi1ELi0EEENS_6detail11transform_fIS2_EEJZNS_21merge_path_partitionsILNS_8bounds_tE1ENS_19counting_iterator_tIiiEEPiNS_6less_tIiEEEENS_5mem_tIiEET0_lT1_llT2_RNS_9context_tEEUliE_mE
2.32308s  6.7660us  cudaLaunchKernel (_ZN4mgpu16launch_box_cta_kINS_12launch_box_tIJNS_7arch_20INS_12launch_cta_tILi128ELi11ELi9ELi0EEENS_7empty_tEEENS_7arch_35INS3_ILi128ELi7ELi5ELi0EEES5_EENS_7arch_52IS4_S5_EEEEEZNS_13transform_lbsIS5_ZNS_15interval_gatherIS5_PiSF_SF_SF_EE
2.32308s  8.2070us  cudaFree

GPU trace:

2.37275s  1.0240us              (1 1 1)       (128 1 1)        11        0B        0B  GeForce GTX 970         1         7  _ZN4mgpu16launch_box_cta_kINS_15launch_params_tILi128ELi1ELi1ELi0EEENS_6detail11transform_fIS2_EEJZNS_21merge_path_partitionsILNS_8bounds_tE1ENS_19counting_iterator_tIiiEEPiNS_6less_tIiEEEENS_5mem_tIiEET0_lT1_llT2_RNS_9context_tEEUliE_mEEEvSF_iDpSG_ [120129]
2.37276s  4.0000us              (1 1 1)       (128 1 1)        56  5.5156KB        0B  GeForce GTX 970         1         7  _ZN4mgpu16launch_box_cta_kINS_12launch_box_tIJNS_7arch_20INS_12launch_cta_tILi128ELi11ELi9ELi0EEENS_7empty_tEEENS_7arch_35INS3_ILi128ELi7ELi5ELi0EEES5_EENS_7arch_52IS4_S5_EEEEEZNS_13transform_lbsIS5_ZNS_15interval_gatherIS5_PiSF_SF_SF_EEvT0_iT1_iT2_T3_RNS_9context_tEEUliiiNS_5tupleIJiEEESF_SF_E_SF_NSM_IJSF_EEEJSF_SF_EEEvSG_iSH_iSI_SL_DpSJ_EUliiSF_SF_E_JSF_SF_EEEvSG_iDpSH_ [120130]

You can clearly see that the cudaMalloc and cudaFree performance changes a lot, but not anything else:

Interestingly, if you leave the magic memory allocated, the performance change will persist between calls to your test function, for example:

void fun(bool magic, mgpu::context_t &context)
{
    int n = 10000000;
    mgpu::mem_t<int> m0(n, context);
    mgpu::mem_t<int> m1(n, context);
    mgpu::mem_t<int> m2(n, context);
    mgpu::mem_t<int> m3(n, context);

    void *tmpPtr = 0;
    if (magic) cudaMalloc((void **)(&tmpPtr), sizeof(int));

    for (int aa = 0; aa < 10000; aa++)
        mgpu::interval_gather(m0.data(), 1,
                              m1.data(), 1,
                              m2.data(), 
                              m3.data(),
                              context);

}

int main(int argc, char *argv[])
{
    {
    mgpu::standard_context_t context(false);
    std::clock_t c0 = std::clock();

    fun(false, context);
    context.synchronize();
    std::clock_t c1 = std::clock();
    print_ms(c0, c1, "1st");

    fun(true, context);
    context.synchronize();
    std::clock_t c2 = std::clock();
    print_ms(c1, c2, "2nd");

    fun(false, context);
    context.synchronize();
    std::clock_t c3 = std::clock();
    print_ms(c2, c3, "3rd");
    }

    return 0;
}

does this:

$ nvcc -arch=sm_52 -std=c++11 --expt-extended-lambda -I ~/mgpu/moderngpu/src -o mgpuspeed mgpuspeed.cu 
$ ./mgpuspeed 
1st: 1287.37 ms.
2nd: 201.205 ms.
3rd: 202.275 ms.

[All timings on at GTX970, CUDA 10.1, Ubuntu 18.04 LTS, 440.59 driver ]

This is in spite of the fact that each mem_t constructor call invokes a cudaMalloc call, and a corresponding cudaFree call when the objects fall out of scope. So it is something related sepcifically to the small "magic" cudaMalloc call and not to cudaMalloc in general. This makes me think that the performance change is actually in the memory manager and not some sort of subtle change in scheduling or asynchronous program flow. It could be something as simple as moving the slab allocator to a different alignment or page size or something which changes how it performs for the internal cudaMalloc calls.

I would file a bug report with NVIDIA and see what they say (caveat, it could be something weird in MGPU, but my appetite for exploring that codebase is pretty limited).

Related