代码之家  ›  专栏  ›  技术社区  ›  mike

音频速率下的OpenCL/CUDA GPU计算-是否有足够快的方法可以在每个音频缓冲区从GPU读取一次?(即最低约43 FPS)

  •  0
  • mike  · 技术社区  · 11 月前

    UDPATE

    我在代码中发现了一个错误。多年前,我在子块中运行渲染功能,忘记了我已经将其设置为子块。所以它调用GPU读取函数的频率比我想象的要高得多。对不起的。

    问题

    我最近尝试将OpenCL添加到一个音频合成器中,该合成器将受益于GPU处理(由于处理中的高度并行化数学)。然而,我发现,即使只是尝试从GPU中读取每个音频缓冲区一次(甚至不是每个样本一次),也会降低性能,无法使用。

    现行方法

    我在这里使用OpenCL Wrapper项目: https://github.com/ProjectPhysX/OpenCL-Wrapper

    简单地创建一个小 Memory<float> test 20-125的对象在项目初始化时与它一起浮动一次,然后在每个音频缓冲区运行时浮动一次 test.read_from_device() 而不做其他事情会导致音频卡顿。

    这个 OpenCL Wrapper function for this 是:

        inline void read_from_device(const bool blocking=true, const vector<Event>* event_waitlist=nullptr, Event* event_returned=nullptr) {
            if(host_buffer_exists&&device_buffer_exists) cl_queue.enqueueReadBuffer(device_buffer, blocking, 0ull, capacity(), (void*)host_buffer, event_waitlist, event_returned);
        }
    

    要求

    音频通常必须以每秒44100个样本的速度运行。音频缓冲区最多可接受1024个样本。因此,如果我们在GPU上一次处理一个完整的缓冲区,我们需要以每秒至少43次或每23毫秒一次的速度从GPU平滑读取。

    每秒43次低于GPU通常可以处理的60-120fps左右,所以我认为这应该不会太不切实际。

    其他测试

    我读过这篇帖子,它表明我并不是唯一一个遇到这个问题的人: GPU audio processing

    特别是答复:

    对不起,马上会让你失望的。我尝试过使用NVidia CUDA(原生库)进行音频处理,使用神经网络。这是我公司的谋生之道,所以我们很有能力。我们发现典型的NVidia卡有太多的延迟。它们很快,这不是问题所在,但这意味着它们可以在一毫秒内完成数百万次操作。然而,向卡馈送数据的DMA引擎通常具有许多毫秒的延迟。对视频来说没那么糟糕,对音频来说很糟糕——视频通常是60赫兹,而音频可以是48000赫兹。

    (请注意,他在这里谈论的是在GPU上来回处理每个样本,而不是一次处理一个完整的缓冲区,这应该更现实。)

    工作制度

    目前有一家名为GPU Audio的公司声称可以有效地在GPU上处理音频插件: https://www.gpu.audio/

    为了在GPU上运行任何与音频相关的内容,每个音频缓冲区还必须至少从GPU读取一次。否则,您还能如何输出音频?因此,如果GPU音频正在GPU上处理任何内容,那么显然有一些方法可以做到这一点。

    我推测他们正在使用GPU上的完整缓冲区,就像我描述的那样。然而,我目前的方法还不够快。他们必须使用更快的方法。

    This study (来自上面链接的Stack Overflow线程)似乎表明我们应该能够在1.5毫秒左右完成数据传输,这应该是足够的时间了。但我并没有清楚地接近这场演出。

    问题

    有人知道如何做到这一点吗?上面的OpenCL函数有什么明显的问题吗?或者,您能否建议一种已知的替代方法,该方法可以从GPU读取,延迟不超过几毫秒,这样我们就可以在每个缓冲区的基础上保持同步?

    CUDA可能会提供更快的方法吗?或者可以编写一个更好的OpenCL函数吗?我更倾向于使用OpenCL。我想一定有某种方法,因为每秒从现代GPU读取43次应该不是非常不合理的。

    谢谢你的任何想法。

    1 回复  |  直到 11 月前
        1
  •  1
  •   Homer512    11 月前

    嗯,我不知道关于延迟的最佳实践,尤其是在OpenCL上。但我可以提供一个简单的基准来衡量往返时间。

    我在这里只做了两件事:

    1. 使用双缓冲使GPU保持忙碌
    2. 使用固定主机内存,不要进行显式复制

    Nvidia GPU可以直接访问固定主机内存。虽然这确实会减慢内核的速度,并在等待数据传输时占用计算资源,但它也避免了等待或与复制操作同步。然而,我并没有将其与具有双缓冲的传统复制设置进行比较。

    在我的硬件(运行Linux的Nvidia T1200笔记本电脑)上,此设置执行以下操作的往返传输 1024个样本,15个美国 在前一两个内核调用之后保持一致。在桌面RTX-3090上进行相同的测试会产生25 us RTT。

    以下是代码:

    #include <cuda_runtime.h>
    
    #include <algorithm>
    // using std::fill_n
    #include <cstdio>
    // using std::printf
    #include <chrono>
    // using std::steady_clock
    
    
    /**
     * Simple input = output kernel
     */
    __global__ void kernel(unsigned* out, const unsigned* in, int n)
    {
        const int idx = blockDim.x * blockIdx.x + threadIdx.x;
        if(idx < n)
            out[idx] = in[idx];
    }
    /**
     * Creates a time stamp in microseconds
     *
     * No defined zero-time. Only useful for measuring relative time intervals
     */
    unsigned current_time_us()
    {
        using us = std::chrono::microseconds;
        return static_cast<unsigned>(std::chrono::duration_cast<us>(
                std::chrono::steady_clock::now().time_since_epoch()).count());
    }
    /** Fills the buffer with the current time stamp */
    void fill_current_time(unsigned* buf, int n)
    {
        std::fill_n(buf, n, current_time_us());
    }
    
    int main()
    {
        int samples = 1024, repetitions = 100;
        int blocksize = 128;
        int gridsize = (samples + blocksize - 1) / blocksize;
        cudaStream_t stream;
        if(cudaStreamCreate(&stream))
            return 1;
        /*
         * We use pinned host memory that is directly accessible by the device and
         * the host for input and output transfer.
         * Two input and two output buffers for double-buffering
         */
        unsigned* transfer_bufs;
        if(cudaHostAlloc(&transfer_bufs, 4 * samples * sizeof(unsigned), 0))
            return 2;
        unsigned* input_bufs = transfer_bufs;
        unsigned* output_bufs = transfer_bufs + 2 * samples;
        /*
         * We use events for quick notification when a kernel is done without
         * having to synchronize the stream
         */
        cudaEvent_t output_avail[2];
        for(cudaEvent_t& event: output_avail)
            if(cudaEventCreate(&event))
                return 3;
        /*
         * Initial fill of the first double buffer
         */
        fill_current_time(input_bufs, samples);
        kernel<<<blocksize, gridsize, 0, stream>>>(
                output_bufs, input_bufs, samples);
        if(cudaEventRecord(output_avail[0], stream))
            return 4;
        for(int i = 1; i < repetitions; ++i) {
            int cur_buf = i & 1;
            int last_buf = cur_buf ^ 1;
            int cur_offset = samples * cur_buf;
            int last_offset = samples * last_buf;
            /*
             * Schedule the next computation
             */
            fill_current_time(input_bufs + cur_offset, samples);
            kernel<<<blocksize, gridsize, 0, stream>>>(
                        output_bufs + cur_offset, input_bufs + cur_offset, samples);
            if(cudaEventRecord(output_avail[cur_buf], stream))
                return 5;
            /*
             * Wait for the previous computation
             */
            if(cudaEventSynchronize(output_avail[last_buf]))
                return 6;
            /*
             * Measure the time interval from filling the input buffer to
             * receiving it back in the output buffer
             */
            std::printf("RTT %u us\n", current_time_us() - output_bufs[last_offset]);
        }
        /*
         * Wait for the last computation. No need to check the results
         */
        if(cudaEventSynchronize(output_avail[(repetitions - 1) & 1]))
            return 7;
    }
    

    输出:

    RTT 94 us
    RTT 22 us
    RTT 12 us
    RTT 15 us
    RTT 15 us
    RTT 15 us
    RTT 15 us
    RTT 15 us
    ...
    

    然而,我还应该注意到,使用只有1024个样本的完整GPU听起来几乎是不可能的。即使是单个多处理器也有比这更多的线程!因此,虽然传输延迟不是问题,但在不增加缓冲区大小的情况下实际使用计算资源将是一个问题。

    但我不知道,也许你把32个输入源和1024个样本混合在一起。顺便说一句,在我的测试中,将传输增加32倍只会将RTT增加到60 us(RTX 3090上为240 us)。