如何在 Nvidia Shield 上正确计时 Android RenderScript 代码

How to do correct timing of Android RenderScript code on Nvidia Shield

我已经在 RenderScript 中实现了一个小型 CNN,并且想分析不同硬件上的性能。在我的 Nexus 7 上,时间有意义,但在 NVIDIA Shield 上却没有。

CNN (LeNet) 以 9 层驻留在队列中实现,按顺序执行计算。每层单独计时。

这是一个例子:

       conv1  pool1 conv2  pool2 resh1 ip1    relu1  ip2    softmax
nexus7 11.177 7.813 13.357 8.367 8.097 2.1    0.326  1.557  2.667
shield 13.219 1.024 1.567  1.081 0.988 14.588 13.323 14.318 40.347

nexus 的时间分布大致正确,conv1 和 conv2(卷积层)占据了大部分时间。但是在盾牌上,时间下降的方式远远超出了第 2-4 层的合理范围,并且似乎在接近尾声时聚集起来。 softmax 层是一个相对较小的工作,所以 40 毫秒太大了。一定是我的计时方法有问题,或者是其他原因。

层的代码 运行 看起来像这样:

double[] times = new double[layers.size()];
int layerindex = 0;
for (Layer a : layers) {

    double t = SystemClock.elapsedRealtime(); 
    //long t = System.currentTimeMillis(); // makes no difference

    blob = a.forward(blob); // here we call renderscript forEach_(), invoke_() etc

    //mRS.finish(); // makes no difference

    t = SystemClock.elapsedRealtime() - t; 
    //t = System.currentTimeMillis() - t; // makes no difference

    times[layerindex] += t; // later we take average etc

    layerindex++;
}

据我了解,一旦forEach_() returns,作业就应该完成了。无论如何,mRS.finish() 应该提供最后一道屏障。但是放眼时代,唯一合理的解释就是作业还在后台处理。

该应用程序非常简单,我只是 运行 从 MainActivity 进行测试并打印到 logcat。 Android Studio 将应用构建为一个版本,并运行将其安装在通过 USB 连接的设备上。

(1) 为 RenderScript 进程计时的正确方法是什么? (2) 当forEach_() returns 时,脚本生成的线程是否保证完成? (3) 在我的测试应用程序中,我只是直接从 MainActivity 运行。这是一个问题吗(除了阻塞 UI 线程并使应用程序无响应)?如果这会影响时间或导致异常,那么像这样设置测试应用程序的正确方法是什么?

我自己在 RenderScript 中实现了 CNN,正如您所解释的,如果您将它们分别实现为不同的内核,它确实需要链接多个进程并为每一层调用 forEach_*() 不同的时间。因此,我可以向您保证,返回的 forEach 调用并不能真正保证该过程已完成。理论上,这只会安排内核,所有排队的请求实际上 运行 只要系统确定最好,特别是如果它们在平板电脑的 GPU 中处理。

通常,要绝对确保您对内核真正具有某种控制权的唯一方法 运行ning 是通过显式读取层与层之间的 RS 内核的输出,例如使用 .copyTo() 在该内核的输出分配对象上。 "forces" 尚未 运行 的任何排队的 RS 作业(该层的输出分配取决于该作业)将在那时执行。当然,这可能会引入数据传输开销,并且您的计时不会完全准确——事实上,如果以这种方式计时,整个网络的执行时间肯定会低于各个层的总和。但据我所知,这是为链中的单个内核计时的唯一可靠方法,它会给你一些反馈来找出瓶颈在哪里,并更好地指导你的优化,如果那是你所追求的。

可能有点偏离主题:但对于 CNN,如果您可以使用矩阵-矩阵乘法作为基本计算块来构造您的算法,您实际上可以使用 RenderScript IntrinsicBLAS, especially BNNM and SGEMM.

优点:

  1. 8 位矩阵乘法 (BNNM) 的高性能实现,在 N Preview 中可用。
  2. 当使用 Build-Tools 24.0.0 rc3 及更高版本时,支持返回到 Android 2.3 到 RenderScript Support lib
  3. Nexus5X 和 6P 上的 SGEMM 高性能 GPU 加速,带有 N Preview build NPC91K。
  4. 如果你只使用 RenderScript Intrinsics,你可以在 java.
  5. 中编写所有代码

缺点:

  1. 你的算法可能需要重构,需要基于二维矩阵乘法
  2. 虽然在Android6.0中可用,但BNNM在6.0中的表现并不尽如人意。所以最好使用 BNNM 的支持库并将 targetSdkVersion 设置为 24.
  3. SGEMM GPU 加速目前仅适用于 Nexus5X 和 Nexus6P。并且目前要求Matrices的宽高都是8的倍数

如果 BLAS 适合您的算法,那么值得一试。而且使用方便:

    import android.support.v8.renderscript.*;
    // if you are not using support lib:
    // import android.renderscript.*;

    private void runBNNM(int m, int n, int k, byte[] a_byte, byte[] b_byte, int c_offset, RenderScript mRS) {
        Allocation A, B, C;
        Type.Builder builder = new Type.Builder(mRS, Element.U8(mRS));
        Type a_type = builder.setX(k).setY(m).create();
        Type b_type = builder.setX(k).setY(n).create();
        Type c_type = builder.setX(n).setY(m).create();

        // If you are reusing the input Allocations, just create and cache them somewhere else.
        A = Allocation.createTyped(mRS, a_type);
        B = Allocation.createTyped(mRS, b_type);
        C = Allocation.createTyped(mRS, c_type);
        A.copyFrom(a_byte);
        B.copyFrom(b_byte);

        ScriptIntrinsicBLAS blas = ScriptIntrinsicBLAS.create(mRS);
        // Computes: C = A * B.Transpose
        int a_offset = 0;
        int b_offset = 0;
        int c_offset = 0;
        int c_multiplier = 1;
        blas.BNNM(A, a_offset, B, b_offset, C, c_offset, c_multiplier);
    }

SGEMM 相似:

        ScriptIntrinsicBLAS blas = ScriptIntrinsicBLAS.create(mRS);
        // Construct the Allocations: A, B, C somewhere and make sure the dimensions match.
        // Computes: C = 1.0f * A * B + 0.0f * C
        float alpha = 1.0f;
        float beta = 0.0f;
        blas.SGEMM(ScriptIntrinsicBLAS.NO_TRANSPOSE, ScriptIntrinsicBLAS.NO_TRANSPOSE,
                   alpha, A, B, beta, C);