GPU 版 TensorFlow Lite
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 相关阅读: