关于 TF Runtime 的疑问?

什么是TFRT ?

TensorFlow Runtime,简称 TFRT,它提供了统一的、可扩展的基础架构层,可以极致地发挥CPU多线程性能,支持全异步编程(无锁队列+异步化语义)。TFRT 可以减少开发、验证和部署企业级模型所需的时间。

TFRT 的输入是什么?

输入为Tensorflow GraphDef,TFRT 会调用基于MLIR的图编译器,执行图优化,并将其lower成 BEF —— 用于执行TFRT graph的二进制可执行格式。

  • 在TF原生框架中,执行的流程是:Python Layers → GradDef (DAG) → 执行OpNode (ThreadPool并行)

  • Runtime 的思路:Python Layers → GradDef (DAG) → Compile IR → Binary (BEF) → execute (BEFExecutor)

基础概念:

  • Host Program in MLIR是graph的低阶中间表示
  • BEF是一个BEFExecutor的可执行文件,读取BEF文件,然后异步执行里面的函数
  • 两者通过tfrt_translate来转换,类似汇编器 Assembler

这里的 IR 是什么?

其实可以理解为是一套表示拓扑关系的代码,甚至是一个graph。通过拓扑递推,可以很容易转为一段IR代码。这也是为什么BEF支持IR与Graph的互转的原因。比如:

%1 = hex.constant.i32 1
%2 = hex.constant.i32 2
%3 = hex.add.i32 %1, %2
hex.print.i32 %3
# 实际可以表示为一个DAG图

和 XLA 的区别?

XLA 本质上并没有脱离图执行的框架,它只是通过 graph cluster 把部分子图通过 HLO 的转换走 JIT 执行,将子图包裹在一个XlaRunOp里,再与图的其他节点一起执行。所以只是把几个节点换成了一个更快的大节点。(看起来有点类似fuse)

官方文档里称BEF为 Kernel graph的实际载体,实际还是一个graph,即表示bef executor最终执行的实体依然是一个 graph(但不是TF原生意义的GraphDef)。

TFRT 基本执行单元是什么?执行的流程?

TFRT里的 kernel 概念,分为如下两种:

  • 同步 Kernel

    • 完全在调用它的线程中执行,不会涉及到其他线程里的计算。它产生的AsyncValue状态都是available的

      int32_t TFRTAddI32(Argument<int32_t> arg0, Argument<int32_t> arg1) {
      // The thread that calls TFRTAddI32 performs this addition, and produces
      // an available AsyncValue.
      return *arg0 + *arg1;
      }
  • 异步 Kernel

    • 包含两个部分的计算:①调用它所在线程的同步计算 ② 其他线程中的异步计算。它产生的AsyncValue状态是unavailable的(并不全是)

      void TFRTAddI32Async(Argument<int32_t> arg0, Argument<int32_t> arg1,
      Result<int32_t> output, HostContext* host) {
      // Synchronously allocate an unavailable AsyncValue for ‘output’.
      auto result = output.Allocate(); // Asynchronously make ‘output’ available.
      host->EnqueueWork([arg0 = *arg0, arg1 = *arg1,
      result_ref = FormRef(result)] {
      // A ConcurrentWorkQueue thread performs this addition.
      result_ref->emplace(arg0 + arg1);
      }); // Synchronously returns unavailable ‘output’.
      }

执行流程:

  • 创建一个AsyncKernelFrame,包含输入参数和输入result
  • 将Frame传递给kernel执行
  • 所有的AsyncValue通过registers来跟踪

也提供了eager API (op-by-op):CoreRuntime 和 CoreRuntimeOp

  • CoreRuntime:

    • 执行OpHandler,借助内部类Impl来实现
    • 它可以调用MakeOp(op_name, op_handler)来创建一个CoreRuntimeOp直接运行
  • CoreRuntimeOp

    • 持有一个llvm::unique_function<void<const OpInvocation&>>类型的函数指针fn_
    • 仿函数用于执行函数fn_

如何整合硬件设备的?

借助 DeviceRuntime,让BEF只支持最底层的driver API的Op,从而尽量避免让每一种后端都单独实现一遍tf的各个Op。

如下图中使用的op直接对应到了cuda api:

Host Runtime的设计思路

Host Runtime 的位置?

host 指执行计算的机器设备,可能有,也可能没有硬件加速的资源。host 可以只是一个具有多GPU的服务器,或带有DSP和IPU的移动设备。

在TF原生的框架中,TF Core是按照 data-flow 进行op-by-op的执行,设计上有很多顺序同步执行的影子在里面。而 Host Runtime 通过重新编排计算逻辑,然后驱动 Device Runtime(如GPU、TPU)去加速计算,使得kernel的执行可以单独放在一个线程中,去异步执行,充分利用的多线程并行的优势。

为什么要做这件事?

  • 期望能高效的eagerly执行op

    • TF对graph执行已经优化的很好了,毕竟都在C++端执行。但在earge模式下,python和runtime端之间的不必要的开销还是在存的。
  • 统一图和op两个不同层次下多线程并行机制
  • runtime 中异步是一等公民
    • a non-strict kernel/function may execute before all its inputs are ready.
  • 更轻便地进行cross-kernel优化
    • TF 的op Kernel实现中封装了 Tensor 的内存申请之类的逻辑,这限制了cross-kernel中reuse buffe的优化。在 TFRT的kernel中,解耦了 shape计算和 tensor 内存申请的逻辑
  • 实现模块化、可插拔式的新硬件支持机制
    • 期望解决之前为了接入新硬件而不得不hack整个代码库的痛点;能够建立一种模块化机制,直接提供完善的接入文档给硬件团队即可,变被动为主动。

如何去设计来实现上述目标么?

先回顾下背景: Core Runtime, Graph Lowering 和 Eager Execution

  1. Core Runtime

    用来 eagerly 执行单个 op 或者整个graph function——包含GradDef 和 HLO。一个op graph通常是设备独立的。

  2. Graph Lowering

Compiler passes 将一个op graph 转化为一个Kernel Graph,它是一个数据流计算的更低阶表示,为更快执行而设计,因此不适合做编译分析,但可以通过低阶方言(如MLIR)来表示。Kernel graph是面向指定设备的(与平台绑定)

  1. Eager Execution

    Host Runtime支持eagerly 执行。但并不一定会涉及Graph/BEF的构造和BEFExecutor的使用。TF设计了两个方案:

    • Generic path:把 op 当做graph function来处理,可以很好处理组合 op 的情况,也可以复用graph function的那一整套代码。
    • Fast path:使用手写的C++或者预编译的 graph snippets 去完成op kernel的选取和调用(定制化优化?成本不高么?)

Kernel Graph 中的 Kernel 指什么?

TFRT里面也有 kernel 的概念,输入输出均为:AsyncValue——异步是一等公民的践行者。类似C++标准库中的 futurepromis的组合。 graph中的所有data全部都会替换为AsyncValue

执行流程:

  • 创建一个AsyncKernelFrame,包含输入参数和输入result
  • 将Frame传递给kernel执行
  • 所有的AsyncValue通过registers来跟踪
// Kernel that adds two integers.
// AsyncKernelFrame holds the kernel’s arguments and results.
static void TFRTAdd(AsyncKernelFrame* frame) {
// Fetch the kernel’s 0th argument.
AsyncValue* arg1 = frame->GetArgAt(0);
// Fetch the kernel’s 1st argument.
AsyncValue* arg2 = frame->GetArgAt(1); int v1 = arg1->get<int>();
int v2 = arg2->get<int>(); // Set the kernel’s 0th result.
frame->EmplaceResultAt<int>(0, v1 + v2);
}

TODO: Kernel中的内存申请接入机制

Kernel 类型分为如下两种:

  • 同步 Kernel

    • 完全在调用它的线程中执行,不会涉及任何其他线程的计算。它产生的AsyncValue状态都是available的

      int32_t TFRTAddI32(Argument<int32_t> arg0, Argument<int32_t> arg1) {
      // The thread that calls TFRTAddI32 performs this addition, and produces
      // an available AsyncValue.
      return *arg0 + *arg1;
      }
  • 异步 Kernel

    • 包含两个部分:①调用它所在线程的同步操作 ② 其他线程中的异步操作。它产生的``AsyncValue`状态是unavailable的(并不全是)

      void TFRTAddI32Async(Argument<int32_t> arg0, Argument<int32_t> arg1,
      Result<int32_t> output, HostContext* host) {
      // Synchronously allocate an unavailable AsyncValue for ‘output’.
      auto result = output.Allocate(); // Asynchronously make ‘output’ available.
      host->EnqueueWork([arg0 = *arg0, arg1 = *arg1,
      result_ref = FormRef(result)] {
      // A ConcurrentWorkQueue thread performs this addition.
      result_ref->emplace(arg0 + arg1);
      }); // Synchronously returns unavailable ‘output’.
      }

Kernel 的两种执行模式:

  • Strict mode:

    • 此类Kernel被调用时,所有的AsyncValue均已是available。
  • non Strict mode:

    • 只要有一个输入参数是available,就执行。比如三元操作,它其实只负责转发
    result = ternary(condition, true_result, false_result) //只要condition可用即可
    • 这类kernel实现难度较高

AsyncValue有什么用途?

前面提到:Kernel 的输入输出均为:AsyncValue,graph中的所有data也全部替换为了AsyncValue

// A subset of interface functions in AsyncValue.
class AsyncValue {
public:
// Is the data available?
bool IsAvailable() const; // Get the payload data as type T.
// Assumes the data is already available, so get() never blocks.
template <typename T> const T& get() const; // Store the payload data in-place.
template <typename T, typename... Args>
void emplace(Args&&... args); // Add a waiter callback that will run when the value becomes available.
void AndThen(std::function<void()>&& waiter);
// ...
};

AyncValuea有三个派生类:

  • ConcreteAsyncValue<T>:用于表示和存放具体data
  • ErrorAysncValue:用于处理异常传播和取消执行。BEFExecutor会监控每个Kernel执行返回的值,若果某个result值为此类型,则跳过所有依赖此值的下游op
  • IndirectAsyncValue:有些情况下,某个result的dataType还不知道呢,但为了实现非阻塞机制,先创建一个IndirectSyncValue,保证non-strick Kernel的执行。它其实并不持有数据,而是持有了一个指向另一个AsyncValue的指针。

生命周期:通过引用计数实现:

  • kernel会首先对results创建AyncValue(当dataType确定时)
  • 一个AsyncValue的所有权会从kernel移交给BEFExecutor
  • BEFExecutor将AsyncValue传递给所有使用它的下游 Op,并递增引用计数
  • 每个下游Op Kernel完成计算后,递减此AsyncValue的引用计数

管理AyncValueRegister具体做哪些工作?

Register其实是一个指向AyncValue的指针,它也只操作指针,因此不涉及数据的移动和copy。

举个栗子

available_value = upstream()
downstream(available_value, unavailable_value)

downstream需要等到两个参数都ready才会执行。当unavailable_value也available时,执行器从register加载数据,然后传递给downstream去执行

register有三种状态:

  • Empty:初始状态,不指向任何AsyncValue
  • Unavailable: 只用于异步kernel。同步kernel不会产生此状态。
  • Available: 最终状态,且状态不可逆。

RunTime 如何实现异步加速的?

在 TFRT 中,执行Kernel的线程,与调度其他已ready的kernel的线程,可能属于同一个。TFRT 把后台调度kernel任务放到了一个ConcurrentWorkQueue中来异步执行。

但反向需要梯度才能执行,如何处理反向op以及IO阻塞问题呢?

TF采用了两个独立的线程池:

①专用线程池:存放长时非阻塞任务

  • 固定线程数,每个硬件一个线程,避免线程资源抢占带来的开销。

②单独线程池:存放阻塞任务(如IO)

  • 申请多一些线程数来处理IO任务
  • 为了避免死锁,阻塞任务只能放在阻塞线程池里执行
  • 要求Kernel的实现不能直接包含阻塞操作(例如?),更不能将部分阻塞操作放到非阻塞队列里。

图执行——Graph Executation

图执行时,host program 会把 graph 转换为MLIR表示的 Kernel graph。此处会应用一些compiler passes 将设备无关的 graph 转化为面向特定硬件平台的 kernel graph。

func @sample_function() -> i32 {
%one = tfrt.constant.i32 1 // Make AsyncValue with value 1
%two = tfrt.constant.i32 2 // Make AsyncValue with value 2
%three = tfrt.add.i32 %one, %two // Make AsyncValue with value 3 (1+2) tfrt.print.i32 %three // Print AsyncValue %three
tfrt.return %three : i32 // Return AsyncValue %three
}

runtime 并不直接执行IR,而是通过mlir_to_bef将其转换为 BEF后再执行。通过 registers 跟踪和记录所有 AsyncValue 的状态。

如何解决control dependency问题?

在原生的TF中是通过tf.control_dependencies来对两个有顺序要求的Kernel添加依赖。在TFRT中,是通过Chain来实现。一个chain也是一个AsyncValue——可以是kernel的参数,也可以是result,这样的话,Chain要求consumer必须在producer之后,以此实现有序性。

func @control_dep1() {
%a = dht.create_uninit_tensor.i32.2 [2 : i32, 2 : i32]
%chain1 = dht.fill_tensor.i32 %a, 41
%chain2 = dht.print_tensor.i32 %a, %chain1
}

如何处理控制流的情况,如if ?

TFRT支持在Kernel中调用BEFExecutor(这一点跟Paddle目前的控制流处理思路有点类似)

void TFRTIf(AsyncKernelFrame* frame) {
const auto* true_fn = &frame->GetConstantAt<Function>(0);
const auto* false_fn = &frame->GetConstantAt<Function>(1); // First arg is the condition.
ArrayRef<AsyncValue*> args = frame->GetArguments();
AsyncValue* condition = args[0]; // Execute true_fn or false_fn depending on ‘condition’.
auto* fn = condition->get<bool>() ? true_fn : false_fn;
fn->Execute(args.drop_front(),
frame->GetResults(),
frame->GetHostContext());
}

与底层的session的区别和联系?

貌似没啥关系。(待深入了解)

BEF文件里都包含了什么信息?

BEF 是runtime和compiler的桥梁,同时将compiler从runtime中解耦,从而可以独立应用编译优化策略。它支持保存到磁盘,重新加载执行(mmap bytes)。感觉和二进制文件很类似,因为它也包括很多section的概念。

BEF 包含了一些与硬件设备相关的信息:每个Kernel在哪种设备(CPU/GPU/TPU)上执行,以及哪些特殊的Kernel会被调用。

MLIR和BEF之间可以互相转换:

BEFExecutor的作用是什么?有特殊性能收益吗?

它是一个执行器,而非一个解释器,因为它没有program counterd的概念。

性能收益来源:

  • 它是 lock-free 的
  • 非阻塞执行:
    • 无论一个Value是否available,它都会执行下去。对于unvailable的value,执行器会将其推迟到AsyncValue::AndThen
    • 由于AyncValue都会由Register来跟踪,它一旦ready,会通知和唤起所有相关kernel

遗留问题

TFRT中公布的文档中很少涉及训练和反向op的内容,是否支持?

在官网给出的 mnist_training.md介绍中,提到了TFRT对训练的支持,但只是原型展示,并非最终版本。

  • 单独重写了MNIST模型中所有的op,如matmul、relu、elem_add、argmax、reduce_mean
  • 这里只重写relu_grad的kernel,其他op的反向kernel默认使用的是Tensorflow框架的?

参考资料

  1. 【官方文档】—TFRT Host Runtime Design

浅析 TensorFlow Runtime 技术的更多相关文章

  1. 浅析AnyCast网络技术

    什么是BGP AnyCast? BGP anycast就是利用一个(多个) as号码在不同的地区广播相同的一个ip段.利用bgp的寻路原则,短的as path 会选成最优路径(bgp寻路原则之n),从 ...

  2. 浅析JAVA Runtime原理与过各大厂商免杀webshell制作

    Author:Sevck Date:2017年6月24日 昨天在网络尖刀老年活动中心群里,忽然想到一个问题,就是JAVA在运行Runtime执行命令的时候会不会调用bash,因为php等语言会调用ba ...

  3. 升级tensorflow1.0到1.3,报错ImportError: libcudnn.so.6: cannot open shared object file: No such file or directory Failed to load the native TensorFlow runtime.

    先定位问题,发现在 /usr/local/cuda/include/ /usr/local/cuda/lib64/ 下面只有 libcudnn.so.5 因此,只要下载cudnn6.*版本的文件分别覆 ...

  4. Java编程技术之浅析Java容器技术

    Java容器 集合是一种存储数据的容器,是Java开发中使用最频繁的对象类型之一. 或许提起Collection,都会第一时间意识到List和Set以及Map等相关关键词.因为这几乎是我们日常开发里接 ...

  5. iOS运行时Runtime浅析

    运行时是iOS中一个很重要的概念,iOS运行过程中都会被转化为runtime的C代码执行.例如[target doSomething];会被转化成objc)msgSend(target,@select ...

  6. Objective C运行时(runtime)技术的几个要点总结

    前言:          Objective C的runtime技术功能非常强大,能够在运行时获取并修改类的各种信息,包括获取方法列表.属性列表.变量列表,修改方法.属性,增加方法,属性等等,本文对相 ...

  7. Objective C运行时(runtime)技术总结,好强大的runtime

    前言:          Objective C的runtime技术功能非常强大,能够在运行时获取并修改类的各种信息,包括获取方法列表.属性列表.变量列表,修改方法.属性,增加方法,属性等等,本文对相 ...

  8. 深度学习Tensorflow生产环境部署(上·环境准备篇)

    最近在研究Tensorflow Serving生产环境部署,尤其是在做服务器GPU环境部署时,遇到了不少坑.特意总结一下,当做前车之鉴. 1 系统背景 系统是ubuntu16.04 ubuntu@ub ...

  9. 2018最新win10 安装tensorflow1.4(GPU/CPU)+cuda8.0+cudnn8.0-v6 + keras 安装CUDA失败 导入tensorflow失败报错问题解决

    原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/9747019.html 基本开发环境搭建 1. Microsoft Windows 版本 关于W ...

随机推荐

  1. Codeforces Round #668 C. Balanced Bitstring (Div. 2)题解(思维)

    题目链接 题目大意 给你一个长为n的01串,要你使得每一个01串中0和1的个数都要相等,01串中有?字符,你可以使得这个字符变为0或1,要你求是否可以满足条件.输出YES或NO 题目思路 这个题目的难 ...

  2. 放进你的收藏夹吃灰!Linux 运维必备的 40 个命令总结

    1.删除0字节文件 find -type f -size 0 -exec rm -rf {} ; 2.查看进程 按内存从大到小排列 PS -e -o "%C : %p : %z : %a&q ...

  3. sitespeedio前端性能测试工具介绍

    很久没有写博客了,今天给大家介绍一款比较好用的前端性能测试工具. sitespeedio简介: sitespeed.io是Jonathan Lee发布的一款可监视和衡量网站前端性能的开源工具. 1.开 ...

  4. 第7.28节 《Python类、类型、协议》章节总结

    本章详细介绍了Python协议.多态与"鸭子类型".类.类实例变量.类变量.实例方法.类方法.静态方法.类继承.抽象类.property函数和@property装饰器定义属性访问方 ...

  5. 第9.2节 Python的文件打开函数open详解

    一. 引言 在操作一个文件前,大部分情况需要先打开文件,才能进行,在Python中使用内置函数open来打开一个文件.open函数是Python的一个内置函数,io模块 定义的函数open是该内置函数 ...

  6. PyQt(Python+Qt)学习随笔:QListWidget插入多项的insertItems方法

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 除了insertItem方法能插入项外,QListWidget支持一次插入多个项,对应的方法就是in ...

  7. PyQt(Python+Qt)学习随笔:formLayout的layoutRowWrapPolicy属性

    Qt Designer的表单布局(formLayout)中,layoutRowWrapPolicy用于控制表单布局中表单行的标签和输入部件之间是否换行.如图: 上图中蓝色标记圈起来的下拉列表数据是其可 ...

  8. Java基础学习之流程控制语句(5)

    目录 1.顺序结构 2.选择结构 2.1.if else结构 2.2.switch case结构 3.循环结构 3.1.while结构 3.2.do while结构 3.3.for结构 3.3.1.普 ...

  9. js实现跳转的几种方式

    1. window.open("url"); 2.用自定义函数 <script> function openWin(tag,obj) { obj.target=&quo ...

  10. AtCoder Regular Contest 107(VP)

    Contest Link Official Editorial 比赛体验良好,网站全程没有挂.题面简洁好评,题目质量好评.对于我这个蒟蒻来说非常合适的一套题目. A. Simple Math Prob ...