Tensorflow 设计技巧

图遍历

下述代码取自图计算逻辑的executor.cc文件.

遍历一张图有很多方法, TF中的这部分设计主要针对大量并行执行的场景: 即一个节点ready了就可以执行, 多个节点同时ready了可以同时执行, 为了达到并行的效果, 使用线程池作为后端支持. 通常来讲, 线程池内的一个线程如果执行完毕任务就会进入等待状态, 但此处的设计技巧, 减少了线程状态切换的开销

-15-26- 对于expensive的节点, 通常会丢入线程池执行, 但如果当前线程的queue是空, 就在当前线程执行,
-19-21-是这个算法的关键, 第一次遇到expensive并不会立即丢入线程池, 如果真要丢进去, 也会延迟到遇到下一个expensive节点的时候, 如此, n(直接丢入线程池的expensive节点) = n(一共遇到的expensive节点) – 1, 这样, 只要有expensive节点, 都会留一个给当前线程做最终定夺: 如果当前线程任务为空, 就不会劳烦线程池, 如果当前尚有其他工作, 还是丢给线程池执行.

Tensorflow Optimization机制详解

本文的Optimizer是Tensorflow内部图优化操作, 不是用途设计神经网络使用的Optimizer

除了固定的图优化逻辑, Tensorflow运行时还提供了可以注册的Optimizer机制, 使图优化过程更加可定制. 同其他注册机制一样, Optimizer也使用的registry, registertion以及registerar等概念.

源码中, 使用REGISTER_OPTIMIZATION()注册一个优化器, 具体实现如下

-144-new了一个我们注册的optimization对象并用unique_ptr指向它, 这个unique_ptr就是registry管理的对象, 通过它间接管理相应的optimization. 注册的本质是返回一个静态的, 类型为'OptimazationPassRegistration'的, 名为'register_optimization_##ctr'的对象, 这里使用了C++预编译宏'__COUNTER__'生成唯一变量名

下面是一个OptimazationPassRegistration对象的构造过程, 可以看出, 就是将我们构造的'register_optimization_##ctr注册到全局的'global_optimization_registry'.

Continue reading

Tensorflow 图计算引擎概述

tensorflow的用户可以使用多种语言来构造的自己的图, 但各种语言的API最终都会经由C API 进入tensorflow 运行时. 可以说, 对于运行时代码, 其上边界就是C API. 比如, 通过python描述的一张网络, 就是通过类似下面的几个python-C接口进入运行时的.

tensorflow整体上可以看做一个”图语言的编译器”, 和所有编译器的优化以及翻译的功能类似, Graph在运行时中的处理过程, 可以分为 图构造->图优化->图执行 几个阶段. 其中, 图优化随同图构造一同被执行.

全图构造及其优化

Session初次构造时, 应用层代码中定义的数据流图转换成GraphDef格式, 经由C API传入DirectSession.Extend(), 参考调用栈如下

Continue reading

BERT 模型参数量估计

根据BERT论文, 其12层transformer结构有110M参数, 24层更是高达340M, 虽然google公开了这两个网络的预训练模型, 用户只需在后面加一层Full-connected 但如果自己去做BERT预训练, 到底要耗费多少显存呢?

tensorflow中使用tf.variable()在模型中生成训练参数, 同时, tf.dense()内部也含有weight和bias两类参数. 比如,

生成了shape是[vocab_size, embedding_size]的variable, 即 vocab_size * embedding_size 个参数,

内含 from_tensor_2d.shape[-1] * to_tensor_2d.shape[-1] 个weight 以及 to_tensor_2d.shape[-1] 个bias. 共计 (from_tensor_2d.shape[-1] + 1) * to_tensor_2d.shape[-1] 个参数, 这就是一个FC-Layer的参数量

有了上面的例子, BERT的参数量就不难分析, 整个BERT网络可以分为4个模块: embedding layer + transformer layers + pooled layer+ classifier layer

embedding-layer

Continue reading

python 代码片段收集

逐行处理文件

test.txt是一个有15140行的文本文件. 第一种方法在我的笔记本上需要0.02s. 而后一种需要0.04s.

读取指定行

std::thread I – 加载(launch)新线程

标准库使用std::thread()启动一个线程, 原型如下:

最直接的方式就是顶一个函数, 将函数及其实参一同传入构造的thread实例, 此外, 在实际编码中, 还有几种常见的编码形式来启动一个线程

使用类

可以借助C++的类的封装, 将一个线程的资源和运行上下文封装在一起:

使用std::thread构造线程对象时, 如果使用临时对象而不是有名对象来构造, 就要注意下面这种写法是错误的.

事实上, 在编译器眼中, std::thread t(BackGroundTask()); 并不是用构造一个临时的BackGroundTask对象并用它来构造一个 std::thread对象, 而是声明了一个 “以函数对象为参数, 返回值为std::thread的函数t, 它的入参函数的参数是空, 返回值为BackGroundTask类型”. 如果修改这句话使之能够按照我们期望的方向构造std::thread实例, 可以改成下面两种写法:

使用lambda

从C++ XI开始, 有了引入的lambda表达式, 我们也可以不用上述的封装方式, 使用lambda的capture list也可以方便的实现有参线程的启动, 重要的是不用单独定义线程执行函数, 特别适合线程函数只启动一次的场景

条件变量 II – std::condition_variable

条件变量是多线程同步的一种常用方法, 两个线程同步, 如果一个线程在到达同步点之前可能等待很久, 以至产生的额外开销超过了线程调度的开销, 这种情况就可以使用条件变量来同步, 反之, 如果等待的开销比较小, 就可以使用while(){sleep_1ms()}的方式来完成同步, 类似的思路和内核的spin_lock和mutex的区别一样.

C++ STL 提供了相对完善的并行编程接口, 当然少不了对于条件变量的实现. 和pthread实现相比, 除了接口更加面向对象, 并无明显不同, 毕竟, “条件变量的正确使用方式只有一种”. 使用条件变量, 需要准备三件材料: 1个条件 + 2个变量 + 多个线程.

  • 1个条件: 和所有的并行代码一样, 搞清楚执行流之间何时同步, 如何同步是整个程序的重中之重
  • 2个变量: 1个条件变量 + 1把mutex, 当然, 在不同的语言中这种mutex的实现可以不同, 但其本质并没有改变
  • 多个线程: 条件变量只能用于多线程同步, 不能用于多进程
Continue reading

条件变量 I – pthread

条件变量是线程同步的另一种方式,实际上,条件变量是信号量的底层实现,这也就意味着,使用条件变量可以拥有更大的自由度,同时也就需要更加小心的进行同步操作。条件变量使用的条件本身是需要使用互斥量进行保护的,线程在改变条件状态之前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定之后才能计算条件。

模型

pthread_cond_init()

Continue reading

C++ MiniTrick

时间戳打印

master-slave工作线程各自初始化

在master-worker模型中, 二者的初始化逻辑往往不同, 如果第一个执行的线程是master, 可以使用下面的代码来标识出:

Continue reading

C++ 右值引用与move()

C++11之前的标准有两种构造函数: 构造与拷贝构造, 前者用于直接分配一个对象并初始化: Instance original, 后者用于分配一个对象并通过”拷贝已有对象”方式初始化: Instance other = original, 编译器缺省会为每一个类提供一个构造和拷贝构造函数, 但对于管理堆内存等资源的类来说, 这种缺省提供的函数最大的问题就在于很容易造成资源的”浅拷贝”, 而要完成正确的”深拷贝”, 通常需要自己实现一个拷贝构造.

浅拷贝的问题显而易见, 两个对象底层管理着同样一块内存, 一旦一个对象执行了析构函数释放了内存, 那么会导致另外一个对象不可用, 通过重载完成底层内存的拷贝构造可以很好的解决这个问题. 但是, 深拷贝同样也有问题, 如果类中资源巨大, 那么每次拷贝一遍的开销就变得不可接受, 所以 有时候我们还想要这样一种功能: 将资源的管理权交接给另一个对象, 既防止了复制内存的开销, 又防止了两个对象引用了同一块内存. 就功能上来说, 我们完全可以自己封装函数实现这样的功能, 但作为一类和构造相关的功能, 显然如果编译器原生支持更好.

这种管理权交割看起来和拷贝构造如出一辙, 只要提供另外一种构造函数即可. 回顾拷贝构造的场景:

为了控制L3和L4的行为, 我们分别重载了拷贝构造和赋值运算符:

在上述代码中, original_0original_1都是左值, 即既能在等号右边, 也能在等号左边的值, 而我们在拷贝构造的实现中, 用const Instance &来表示这类值.

此外, C++的设计还有一类右值, 即只能出现在等号右边的值, 典型的就是常量

上述代码中的3就是一个右值, 我们只能取其值, 不能赋其值.

Continue reading

C++的引用与const引用

C++ 的引用源自C语言中对指针的使用, 众所周知, C的强大和巨坑都来自于指针, 所以C++着力进行了取其精华, 去其槽粕的改良, 其中, 引用(reference)就是其中一个关键. C++ 的引用可以看做是C中的”指针常量”的封装, “指针常量”, 即不能修改其指向的指针, 该指针的指向必须在初始化时指定, 在此基础上, C++ 引用针对代码的安全性和可读性进行了分类整理升级. C++ 的引用可以分为“引用”“常量引用”,

引用

引用按照 type& ref_name = object的形式初始化, 所以, 初始化引用之前一定要明确即将绑定(bind)的对象的类型. 比如, 变量的地址就是”指针常量”类型, 因为一个变量的地址在编译时期被分配的时候即被确定, 所以不是 type *而是type * const

const 引用

如果说引用是”指针常量”, 那么”常量引用”就是”常量指针常量”: 不但指针的指向不可以改变, 也不可以通过指针改变指向的内容.

有了指针的基础来理解引用, 很多问题就方便解释的多, 比如, 不能用将一个non-const 引用bind到一个const变量/引用上, 因为const指针是比指针更严格的限定, 在全靠程序员自觉的C语言中, 缺省编译器配置下这是一个warning:

但在作为强类型语言的C++中, 这就是一个error:

Continue reading