Aparapi GPU 执行速度低于 CPU

Aparapi GPU execution slower than CPU

我正在尝试测试 Aparapi 的性能。 我看到一些 blogs 结果表明 Aparapi 在进行数据并行操作时确实提高了性能。

但我在测试中看不到这一点。这是我所做的,我写了两个程序,一个使用 Aparapi,另一个使用普通循环。

程序 1:在 Aparapi

import com.amd.aparapi.Kernel;
import com.amd.aparapi.Range;

public class App 
{
    public static void main( String[] args )
    {
        final int size = 50000000;

        final float[] a = new float[size];
        final float[] b = new float[size];

        for (int i = 0; i < size; i++) {
           a[i] = (float) (Math.random() * 100);
           b[i] = (float) (Math.random() * 100);
        }

        final float[] sum = new float[size];

        Kernel kernel = new Kernel(){
           @Override public void run() {
              int gid = getGlobalId();
              sum[gid] = a[gid] + b[gid];
           }
        };
        long t1 = System.currentTimeMillis();
        kernel.execute(Range.create(size));
        long t2 = System.currentTimeMillis();
        System.out.println("Execution mode = "+kernel.getExecutionMode());
        kernel.dispose();
        System.out.println(t2-t1);
    }
}

程序 2:使用循环

public class App2 {

    public static void main(String[] args) {

        final int size = 50000000;

        final float[] a = new float[size];
        final float[] b = new float[size];

        for (int i = 0; i < size; i++) {
           a[i] = (float) (Math.random() * 100);
           b[i] = (float) (Math.random() * 100);
        }

        final float[] sum = new float[size];
        long t1 = System.currentTimeMillis();
        for(int i=0;i<size;i++) {
            sum[i]=a[i]+b[i];
        }

        long t2 = System.currentTimeMillis();
        System.out.println(t2-t1);

    }
}

程序 1 大约需要 330 毫秒,而程序 2 只需要大约 55 毫秒。 我在这里做错了什么吗?我确实在 Aparpai 程序中打印出执行模式,它打印出执行模式是 GPU

您没有做错任何事情 - 执行基准测试本身。

基准测试总是很棘手,尤其是在涉及 JIT 的情况下(如 Java),以及对用户隐藏了许多细节的库(如 Aparapi)。在这两种情况下,您至少应该多次执行要进行基准测试的代码部分。

对于 Java 版本,当循环本身被多次执行时,人们可能期望单次循环执行的计算时间会减少,这是由于 JIT 的启动。还有许多额外的需要考虑的注意事项 - 有关详细信息,您应该参考 this answer。在这个简单的测试中,JIT 的影响可能并不明显,但在更现实或更复杂的场景中,这会有所不同。总之:当重复循环10次时,在我的机器上单次执行循环的时间大约是70毫秒.

对于Aparapi版本,可能的GPU初始化点已经在评论中提到了。在这里,这确实是主要问题:当 运行 内核 10 次时,我机器上的时间是

1248
72
72
72
73
71
72
73
72
72

您看到初始调用导致了所有开销。这样做的原因是,在第一次调用 Kernel#execute() 期间,它必须进行所有初始化(基本上是将字节码转换为 OpenCL,编译 OpenCL 代码等)。 KernelRunnerclass:

的文档中也提到了这一点

The KernelRunner is created lazily as a result of calling Kernel.execute().

这种情况的影响——即第一次执行的相对较大的延迟——导致了 Aparapi 邮件列表中的这个问题:A way to eagerly create KernelRunners。建议的唯一解决方法是创建一个像

这样的 "initialization call"
kernel.execute(Range.create(1));

没有真正的工作量,只触发整个设置,以便后续调用快速。 (这也适用于您的示例)。


您可能已经注意到,即使 初始化之后,Aparapi 版本仍然不比普通 Java 版本快。这样做的原因是像这样的简单向量加法的任务是 memory bound - 有关详细信息,您可以参考 this answer,它解释了这个术语和一些问题一般的GPU编程。

作为您可能受益于 GPU 的情况的过度暗示性示例,您可能想要修改您的测试,以创建人工 计算限制 任务:当你改变内核来涉及一些昂贵的三角函数,像这样

Kernel kernel = new Kernel() {
    @Override
    public void run() {
        int gid = getGlobalId();
        sum[gid] = (float)(Math.cos(Math.sin(a[gid])) + Math.sin(Math.cos(b[gid])));
    }
};

和普通的 Java 相应的循环版本,像这样

for (int i = 0; i < size; i++) {
    sum[i] = (float)(Math.cos(Math.sin(a[i])) + Math.sin(Math.cos(b[i])));;
}

然后你就会看到不同。在我的机器上(GeForce 970 GPU 与 AMD K10 CPU),Aparapi 版本的时间约为 140 毫秒,而高达 12000 毫秒 对于普通的 Java 版本——通过 Aparapi 的速度提高了将近 90!

另请注意,即使在 CPU 模式下,与普通 Java 相比,Aparapi 可能会提供优势。在我的机器上,在 CPU 模式下,Aparapi 只需要 2300 毫秒 ,因为它仍然使用 Java 线程池并行执行。

在主循环内核执行之前添加

kernel.setExplicit(true);
kernel.put(a);
kernel.put(b);

kernel.get(sum);

之后。

Although Aparapi does analyze the byte code of the Kernel.run() method (and any method reachable from Kernel.run()) Aparapi has no visibility to the call site. In the above code there is no way for Aparapi to detect that that hugeArray is not modified within the for loop body. Unfortunately, Aparapi must default to being ‘safe’ and copy the contents of hugeArray backwards and forwards to the GPU device.

https://github.com/aparapi/aparapi/blob/master/doc/ExplicitBufferHandling.md