查看原文
其他

GPU 版 TensorFlow Lite

Google TensorFlow 2021-07-27

TensorFlow Lite 支持多个硬件加速器 


本文将介绍如何通过在 Android(需要 OpenGL ES 3.1 或更高版本)和 iOS(需要 iOS 8 或更新版本)设备上使用 TensorFlow Lite 代理 API 来使用 GPU 后端。


欢迎登录 tensorflow.google.cn/lite 了解更多 TensorFlow Lite。



GPU 加速的优势

速度

GPU 旨在为大规模可并行工作负载提供高吞吐量。因此,GPU 非常适合深度神经网络。深度神经网络由大量运算符组成,每个运算符均处理一些输入张量 tensor(s),而这些张量可轻松分割成较小的工作负载并且并行执行。这种并行方式通常会减少延迟时间。在最好的情况下,GPU 上的推理可达到足够快的运行速度,能够迎合之前无法实现的实时应用的需要。


精度

GPU 使用 16 位或 32 位浮点数进行计算,并且(与 CPU 不同)无需进行量化,即可实现最佳性能。如果精度下降会导致您的模型无法维持量化,则在 GPU 上运行神经网络可以打消这一顾虑。


节能

GPU 推理的另一项优势是省电。GPU 以非常高效且优化的方式执行运算,与在 CPU 上执行同一任务相比,在 GPU 上消耗的电量更少,产生的热量也更少。



支持的操作

GPU 版 TensorFlow Lite 支持 16 位和 32 位浮点精度的以下操作:

  • ADD v1

  • AVERAGE_POOL_2D v1

  • CONCATENATION v1

  • CONV_2D v1

  • DEPTHWISE_CONV_2D v1-2

  • FULLY_CONNECTED v1

  • LOGISTIC v1

  • MAX_POOL_2D v1

  • MUL v1

  • PAD v1

  • PRELU v1

  • RELU v1

  • RELU6 v1

  • RESHAPE v1

  • RESIZE_BILINEAR v1

  • SOFTMAX v1

  • STRIDED_SLICE v1

  • SUB v1

  • TRANSPOSE_CONV v1



基本用法

Android

通过 TfLiteDelegate 运行 GPU 版 TensorFlow Lite。在 Java 中,您可以通过 Interpreter.Options 指定 GpuDelegate。

// 新操作:准备 GPU 代理。

GpuDelegate delegate = new GpuDelegate();

Interpreter.Options options = (new Interpreter.Options()).addDelegate(delegate);

 

// 设置解释器。

Interpreter interpreter = new Interpreter(model, options);

 

// 执行推理。

writeToInputTensor(inputTensor);

interpreter.run(inputTensor, outputTensor);

readFromOutputTensor(outputTensor);

 

// 清理。

delegate.close();


iOS

如要使用 GPU 版 TensorFlow Lite,请通过 NewGpuDelegate() 获取 GPU 代理,然后将其传送给 Interpreter::ModifyGraphWithDelegate()(而非调用 Interpreter::AllocateTensors())。

// 设置解释器。

auto model = FlatBufferModel::BuildFromFile(model_path);

if (!model) return false;

tflite::ops::builtin::BuiltinOpResolver op_resolver;

std::unique_ptr<Interpreter> interpreter;

InterpreterBuilder(*model, op_resolver)(&interpreter);

 

// 新操作:准备 GPU 代理。

 

const GpuDelegateOptions options = {

  .allow_precision_loss = false,

  .wait_type = kGpuDelegateOptions::WaitType::Passive,

};

 

auto* delegate = NewGpuDelegate(options);

if (interpreter->ModifyGraphWithDelegate(delegate) != kTfLiteOk) return false;

 

// 执行推理。

WriteToInputTensor(interpreter->typed_input_tensor<float>(0));

if (interpreter->Invoke() != kTfLiteOk) return false;

ReadFromOutputTensor(interpreter->typed_output_tensor<float>(0));

 

// 清理。

DeleteGpuDelegate(delegate);


请注意:在调用 Interpreter::ModifyGraphWithDelegate() 或 Interpreter::Invoke() 时,调用方的当前线程必须要有 EGLContext,并且必须从相同 EGLContext 中调用 Interpreter::Invoke()。如果 EGLContext 不存在,代理将会在内部进行创建,但之后,开发者必须确保始终从调用 Interpreter::ModifyGraphWithDelegate() 的同一个线程中调用 Interpreter::Invoke()。



高级用法

适用于 iOS 的代理选项

NewGpuDelegate() 接受选项 struct。

struct GpuDelegateOptions {

  // 支持量化张量、向下转换的值、半精度浮点数 (Float16) 的处理等。

  bool allow_precision_loss;

 

  enum class WaitType {

    // waitUntilCompleted

    kPassive,

    // 最大限度减少延迟时间。它使用自旋而非互斥和消耗

    // 其他 CPU 资源。

    kActive,

    // 在输出与 GPU 管道搭配使用或已设置外部

    // 命令编码器时有用

    kDoNotWait,

  };

  WaitType wait_type;

};


设置默认选项(如上文基本用法中所述)的方法是将 nullptr 传送到 NewGpuDelegate()。

// 这项:

const GpuDelegateOptions options = {

  .allow_precision_loss = false,

  .wait_type = kGpuDelegateOptions::WaitType::Passive,

};

 

auto* delegate = NewGpuDelegate(options);

 

// 与这项相同:

auto* delegate = NewGpuDelegate(nullptr);


虽然使用 nullptr 很方便,但我们建议您明确设置选项,避免日后更改默认值时出现任何意外行为。


输入 / 输出缓冲区

要在 GPU 上执行计算,GPU 必须要能够访问数据,而这通常需要进行内存复制。可取的做法是尽量不越过 CPU/GPU 内存边界,否则将会占用大量时间。这种越界通常无法避免,但在某些特殊情况下,可以忽略其中一个边界。


如果网络的输入是 GPU 内存中已完成加载的图像(例如,包含相机输入的 GPU 纹理),则该图像可以保留在 GPU 内存中,无需进入 CPU 内存。同样地,如果网络的输出形式是可渲染图像(例如,图像风格迁移),则其可以直接显示在屏幕上(https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf)


为实现最佳性能,用户可以借助 TensorFlow Lite 直接读取 TensorFlow 硬件缓冲区中的内容并向其中写入内容,同时绕过可避免的内存复制。


Android

假设图像输入位于 GPU 内存,则必须先将其转换为 OpenGL 着色器存储缓冲区对象 (SSBO)。您可以使用 Interpreter.bindGlBufferToTensor() 将 TfLiteTensor 与用户准备的 SSBO 相关联。请注意,必须在调用 Interpreter.modifyGraphWithDelegate() 之前调用 Interpreter.bindGlBufferToTensor()。

// 确保 EGL 渲染上下文有效。

EGLContext eglContext = eglGetCurrentContext();

if (eglContext.equals(EGL_NO_CONTEXT)) return false;

 

// 创建 SSBO。

int[] id = new int[1];

glGenBuffers(id.length, id, 0);

glBindBuffer(GL_SHADER_STORAGE_BUFFER, id[0]);

glBufferData(GL_SHADER_STORAGE_BUFFER, inputSize, null, GL_STREAM_COPY);

glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);  // unbind

int inputSsboId = id[0];

 

// 创建解释器。

Interpreter interpreter = new Interpreter(tfliteModel);

Tensor inputTensor = interpreter.getInputTensor(0);

GpuDelegate gpuDelegate = new GpuDelegate();

// 必须在安装代理前绑定缓冲区。

gpuDelegate.bindGlBufferToTensor(inputTensor, inputSsboId);

interpreter.modifyGraphWithDelegate(gpuDelegate);

 

// 执行推理;null 输入参数表示针对输入使用绑定缓冲区。

fillSsboWithCameraImageTexture(inputSsboId);

float[] outputArray = new float[outputSize];

interpreter.runInference(null, outputArray);


可将类似方法应用于输出张量。在这种情况下,应该传递 Interpreter.Options.setAllowBufferHandleOutput(true),以禁用默认将网络输出从 GPU 内存复制到 CPU 内存。

// 确保 EGL 渲染上下文有效。

EGLContext eglContext = eglGetCurrentContext();

if (eglContext.equals(EGL_NO_CONTEXT)) return false;

 

// 创建 SSBO。

int[] id = new int[1];

glGenBuffers(id.length, id, 0);

glBindBuffer(GL_SHADER_STORAGE_BUFFER, id[0]);

glBufferData(GL_SHADER_STORAGE_BUFFER, outputSize, null, GL_STREAM_COPY);

glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);  // unbind

int outputSsboId = id[0];

 

// 创建解释器。

Interpreter.Options options = (new Interpreter.Options()).setAllowBufferHandleOutput(true);

Interpreter interpreter = new Interpreter(tfliteModel, options);

Tensor outputTensor = interpreter.getOutputTensor(0);

GpuDelegate gpuDelegate = new GpuDelegate();

// 必须在安装代理前绑定缓冲区。

gpuDelegate.bindGlBufferToTensor(outputTensor, outputSsboId);

interpreter.modifyGraphWithDelegate(gpuDelegate);

 

// 执行推理;null 输出参数表示针对输出使用绑定缓冲区。

ByteBuffer input = getCameraImageByteBuffer();

interpreter.runInference(input, null);

renderOutputSsbo(outputSsboId);


iOS

假设图像输入位于 GPU 内存,则必须先将其转换为 Metal 的 MTLBuffer 对象。您可以使用 BindMetalBufferToTensor() 将 TfLiteTensor 与用户准备的 MTLBuffer 相关联。请注意,必须在调用 Interpreter::ModifyGraphWithDelegate() 之前调用 BindMetalBufferToTensor()。此外,默认情况下,将推理输出从 GPU 内存复制到 CPU 内存。可在初始化期间调用 Interpreter::SetAllowBufferHandleOutput(true),以关闭此行为。

// 准备 GPU 代理。

auto* delegate = NewGpuDelegate(nullptr);

interpreter->SetAllowBufferHandleOutput(true);  // disable default gpu->cpu copy

if (!BindMetalBufferToTensor(delegate, interpreter->inputs()[0], user_provided_input_buffer)) return false;

if (!BindMetalBufferToTensor(delegate, interpreter->outputs()[0], user_provided_output_buffer)) return false;

if (interpreter->ModifyGraphWithDelegate(delegate) != kTfLiteOk) return false;

 

// 执行推理。

if (interpreter->Invoke() != kTfLiteOk) return false;


请注意:默认行为关闭后,将推理输出从 GPU 内存复制到 CPU 内存需要为每个输出张量显式调用 Interpreter::EnsureTensorDataIsReadable()。



提示和技巧

CPU 上某些无关紧要的操作对于 GPU 而言可能需要承担高昂的成本。在这些操作中,有一类操作包含各种形式的重塑操作(包括 BATCH_TO_SPACE、SPACE_TO_BATCH、SPACE_TO_DEPTH,以及类似操作)。如果不需要这些操作(例如,插入这些操作是为了帮助网络架构师推断系统,但不会影响输出),则应将其移除以提升性能。


在 GPU 上,张量数据分为 4 个通道。因此,针对形状 [B, H, W, 5] 的张量计算与针对形状 [B, H, W, 8] 的张量计算效果大致相同,但明显比 [B, H, W, 4] 差。

  • 例如,如果相机硬件支持 RGBA 格式的图像帧,则馈送 4 通道输入的速度明显更快,因为在这种情况下可避免内存复制(从 3 通道 RGB 复制到 4 通道 RGBX)。


为了获得最佳性能,请立即使用经过移动优化的网络架构重新训练您的分类器。这是优化设备端推理的重要环节。



更多 AI 相关阅读:



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存