【问题标题】:How to do correct timing of Android RenderScript code on Nvidia Shield如何在 Nvidia Shield 上正确计时 Android RenderScript 代码
【发布时间】:2016-05-06 20:11:40
【问题描述】:

我已经在 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

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

运行层的代码如下所示:

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_() 返回,该工作就应该完成了。在任何情况下,mRS.finish() 都应该提供最后一道屏障。但是看时间,唯一合理的解释是作业仍然在后台处理。

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

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

【问题讨论】:

  • 卷积神经网络。

标签: android multithreading performance timing renderscript


【解决方案1】:

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

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

【讨论】:

  • 谢谢!我已经阅读了您的其他问题,这些问题有助于我理解 RenderScript。我可以再挑一下你的大脑吗?在使用 Shield 等 GPU 的平板电脑上测试类似的东西时,有什么好方法可以确定代码是否真的在 GPU 上运行?
  • 嗯,像往常一样,很难判断 RS 内核将在哪里执行,但如果您的时序设置已修复,您可以在内核正常运行并运行它们的情况下进行测试运行同时强制 RS 仅将 CPU 与 adb shell setprop debug.rs.default-CPU-driver 1 一起使用(和 0 将其关闭,请确保在更改此设置之间正确终止应用程序以使其生效)。如果您的内核构建良好并且确实使用了 GPU,您应该会看到明显的差异,尤其是在卷积中,与纯 CPU 模式相比,您应该获得至少 4-6 倍的加速。
  • 很好,我会试试的!希望某处有可用的分析器,但这肯定会给出一个指示。再次感谢您的帮助!
  • 我现在已经尝试了“adb shell setprop debug.rs.default-CPU-driver 1”,结果令人费解。该命令在 Nvidia Shield 和 Nexus 7 上都提供了大约 10 倍的加速。但这应该会强制 RS 使用默认的参考 CPU 实现。对此有何解释?参考应用程序的速度从大约 50 到 130 fps...
  • 我在这里添加了一个新问题:stackoverflow.com/questions/37228427/…
【解决方案2】:

可能有点跑题了:但是对于 CNN,如果您可以使用矩阵-矩阵乘法作为基本计算块来构建您的算法,那么您实际上可以使用 RenderScript IntrinsicBLAS,尤其是 BNNMSGEMM

优点:

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

缺点:

  1. 您的算法可能需要重构,并且需要基于二维矩阵乘法。
  2. 虽然在 Android 6.0 中可用,但 BNNM 在 6.0 中的性能并不理想。所以最好使用 BNNM 的支持库,并将 targetSdkVersion 设置为 24。
  3. SGEMM GPU 加速目前仅在 Nexus5X 和 Nexus6P 中可用。它目前要求矩阵的宽度和高度为 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);

【讨论】:

  • 感谢您的示例。我已经使用 im2col 和使用 SGEMM 的矩阵乘法实现了卷积,以及移动内核版本。我提出问题的原因是我试图对这两种算法进行相互分析。根据硬件的不同,我得到了截然不同的结果,并试图找出原因。您的 BNNM 示例非常有用,因为我接下来将转到该示例。
  • 也感谢有关 GPU 的信息,非常有用。这个可以在网上找到吗?
  • 还没有。但是会有更多关于它的教程和文档。一旦可用,我将在这里更新它,敬请期待。另外,如果您对 RenderScript 有任何建议或功能要求,请告诉我。我可以将它转发给 RenderScript 团队。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-26
  • 2013-01-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多