预设问题
- 推理优化主要做了什么工作?
主要会从两个视角来叙述,一个是不同优化层次中的通用优化工作,另一个是针对不同业务模型特点的优化工作。
先从通用的优化层次上说,主要是基于Tensorflow模型的优化,大致可以分为三个不同层次:模型,框架和系统。
模型级优化主要分为图层面优化、编译优化和模型压缩。
图层面的优化主要是为了后续推理优化步骤做准备,目的是尽量去除训练算子,使实际执行推理功能的子图尽可能小,并且尽量连通,便于识别子图pattern,或者通过编译优化方法融合子图。
图层面优化的工作包括:
- 图结构清理
去除可能包含自定义Op的训练图,比如PAI-TF定义了读ODPS表的算子
因用户构图API使用不当引入的TF不能识别的训练图,比如DropOut、Switch Merge
同时保护关联Node,清理前后校验数据正确性
- 子图优化
除了常用的contant folding,CSE之外,还做了Range Propagation优化为敞亮
子图结构变换,适配硬件指令结构,比如拆分卷积
- pattern匹配融合
对常见的子图pattern做fusion,比如BatchNorm。
对于常见业务模型pattern定制优化:比如LSTM,Tensorflow Feature Column的优化,Embedding LookUp,Self-Attention。
编译优化这边我们做了很多方向上的探索,比如XLA、TVM、MLIR、PolyHedron等等。我主要参与的是XLA和TVM这两个方面。
- XLA
利用XLA接硬件后端和TVM,XLA的IR定义、优化pass结构、后端IR emit和codegen流程比较完整,适配FPGA硬件和TVM,实际上利用XLA接入会遇到dynamic shape的问题,在业务模型shape动态范围比较大的时候,可能compilation cache爆炸。
利用了基础设施
FPGA后端
TVM的通用化JIT fallback
- TVM
TVM上主要是跟进社区工作,使用TVM的schedule和组里同学做的ansor的工作,做kernel tuning作为数据库供线上查找,在不同的shape,TVM的schedule会用默认schedule,ansor的全空间探索耗时比较长。我们试验下来,TVM在CPU上效果明显,差不多可以达到MKL-DNN优化的效果。
8 cpu 32 G
ResNet50 模型 3x 23 -> 7 ms
线上服务
bs=1 2x 88 -> 43 ms,QPS 30 -> 50
bs=8 3x 450 -> 190 ms,QPS 3 -> 10 * bs
Q:具体怎么用TVM的?
- 存储层次相关schedule
- 循环优化
- fuse 合并两层
- split 分割一层
- reorder 重排循环顺序
- tile 分块
- unroll 展开
- 多线程
- vecotrize simd指令
- bind将iter绑定到block或thread的index上,从而把循环的任务分配到线程,实现并行化计算,这是针对CUDA后端最核心的部分。
- parallel将指定iter的for循环替换为parallel操作,从而在GPU以外的CPU等设备上实现并行。
- 其他
- prefetch 空间局部性
模型压缩主要的工作是利用NVidia和Intel的量化工作。
GPU: FP16 不重训,INT8 calibration
Q: INT8的重训和calibration有啥区别?
框架runtime级主要包括框架。
厂商库,主要是用TensorRT和MKL-DNN。
针对不同代的CPU有指令结构的优化。
skylake AVX-512
cascadelake VNNI INT8*UINT8
cooperlake BF16
tigerlake 调研
Q:Tensorflow框架级别有优化吗?
有调整inter/intra op 线程池的大小。
系统级主要包括服务的各项指标优化。
指标包括:
- RT,对RT要求高的实时业务。
- QPS,对RT要求低的异步业务。
- 资源利用率,公有云成本,超卖和隔离。
单一优化点:
- 网络IO
- 序列化
- 多级缓存
- 负载均衡优化
- 网络直连
- 计算并行度
- 多线程
- 多进程
- 资源利用率
- 资源隔离,CPU/Memory/Network/Disk
- 资源复用,超卖
- 亲核性,NUMA,绑核
Q:serving调优怎么做?线上tracing?
一般离线profile,查看timelime。
线上tracing。
查看网络、队列、处理等多个步骤的耗时,针对性的优化。
- Disk IO
https://blog.kelu.org/tech/2019/10/11/kubernetes-Limit-iops-per-container.html
Docker中有修改dockershim,和kuberuntime的labels
1 | |
- Network IO
kubectl apply -f http://acs-public.oss-cn-hangzhou.aliyuncs.com/kubernetes/network/kube-tc.yml
https://developer.aliyun.com/article/388097
1 | |
Q:你们的多级缓存具体怎么做的?有哪些注意事项和坑?
主要是数据源缓存、后端缓存、客户端缓存,我们这边提供的是后端。
数据源缓存是redis
(1)redis keys命令不能用在生产环境中,如果数量过大效率十分低,导致redis长时间堵塞在keys上。生产环境我们一般选择提前载入一些warm up物品id的方式载入物品embedding
(2)Redis value 可以用protobuf格式存储, 存储上节省空间. 解析起来相比string, cpu的效率也应该会更高
(3)把item embedding提前加载到内存里
(4)关于user embedding,指定一个内存区域的大小,用FIFO的方案来缓存,这样内存用完了,就自动把早进来的用户pop出去
(5)如果有条件可以判断活跃用户,可以尽量选择活跃用户进行缓存
后端缓存,使用local-cache,对user-id, item-id区分,使用LRU/LFU策略。
Q: LRU会写吗?
list + map,存取过则放到
Q: LFU会写吗?
list和map
https://leetcode-cn.com/problems/lfu-cache/solution/lfuhuan-cun-by-leetcode-solution/
Q:本地缓存并行竞争严重吗?
加读写锁
Q:你是怎么使用TensorRT优化的?
TensorRT提供了高层API,输入graphdef,输出替换子图成Trt的算子。
内部会做分图,Layer & Tensor Fusion,量化,和Nvidia对CUDA使用的优化经验 Kernel Auto-Tuning。
量化,对于FP16和FP32的完全不用考虑。
缺点是只能针对static shape,dynamic shape会有大量warm up开销。
Kernel Auto-Tuning 可能是用的编译优化方法和手工优化函数(buket)结合的方式,
1 | |
从业务模型类别上大致可以分为CV、NLP,推荐
- CV
GPU
ResNet 3.3x
OCR-CRNN 3.5x
OCR-Attention
Yolo 2.5x
MobileNet 2.5x
CPU
ResNet50 3.8x
YOLO 2.5x
MaskRCNN 1.7x
密集计算,GPU优化,TensorRT和量化
- 文本
GPU
Bert-large >3x
TextCNN 1.5x
CPU
Bert-large的Self-Attention结构 2x
- 语音
GPU
ASR Transformer模型 2.46x,定制算子,GPU
CPU
Transformer 2.3x
- 推荐
Embedding优化,EasyRec,密集计算部分较简单,主要是Embedding的定制优化
典型场景端到端中等并发下RT降低为1/2以下,QPS提升1.8x左右
DSSM,Wide&Deep,DeepFM
8 vcpu
hb 3x 110 -> 30 ms, QPS 55 -> 175
zl 3x 160 -> 54 ms, QPS 40 -> 110
dy 1.5x 60 -> 40 ms, QPS 80 -> 100
对于推荐的工程支持方法还包括:
- 超大Embedding支持
- 为了提升推荐场景的效果,推荐模型通常采用无Hash的方式或者超大的Hash桶的方式。在这种情况下, embedding size会很大,超过1T,给单机部署tensorflow模型带来了挑战。
- 大规模Embedding导出的任务需要设置PS(parameter server)
- 单机无法restore带巨大embedding的模型
- embedding通常是分片的,分布在多个PS上
- embedding映射表
- 模型feature分为dense和sparse,sparse部分需要支持embedding,redis/OTS存储
- 模型热更新,ping-pong session
- 为了提升推荐场景的效果,推荐模型通常采用无Hash的方式或者超大的Hash桶的方式。在这种情况下, embedding size会很大,超过1T,给单机部署tensorflow模型带来了挑战。
特征哈希就是一种简单的降维方法,在微视使用也较多,特征哈希法的目标就是是把原始的高维特征向量压缩成较低维特征向量,且尽量不损失原始特征的表达能力,其优势在于实现简单,所需额外计算量小;降低特征维度,从而加速算法训练与预测的时间,以及降低内存消耗;但代价是通过哈希转换后学习到的模型变得很难检验,我们很难对训练出的模型参数做出合理解释。特征哈希法的另一个问题是它会把多个原始特征哈希到相同的位置上,出现哈希 collision 现象,但实际实验表明这种 collision 对算法的精度影响很小。
https://www.infoq.cn/article/fsfrcvm83tt88ulovfu2
优点(相对于One-hot Serialization):训练和预测的时间复杂度大幅降低;数据的一致性强,不存在同一个特征今天编码成这个、明天编码成那个的情况,便于跟踪单特征效果;对new feature可以直接编码并加入训练,无需等待编码表统计并反馈;降低feature space大小,(精心设计可以)降低over-fitting的几率。缺点:在不清楚hashing function细节的情况下,容易导致特征碰撞失效,且难以排查;难以通过hashing出的特征反推源特征;
作者:Ainika Peng
链接:https://www.zhihu.com/question/264165760/answer/279676705
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
cache: list(bool)表示的是否需要在op初始化的时候将相应的embedding加载到内存,减少对redis的访问,提高qps和rt。经讨论:
- item feature比较适合cache
- 一些比较小的user feature也适合cache,如性别、年龄等
- user_id等取值范围比较大的,访问频率不高的,暂时不考虑cache
- 模型导出时,业务同学指定哪些embedding需要cache。
- cache考虑LRU策略
Q:Tensorflow Feature Column优化?
- sparse embedding lookup优化
Reshape
Select
ZerosLike
SparseSegmentMean,SparseSegmentSum
GatherV2
Unique
GatherV2,SparseReshape
https://www.infoq.cn/article/fb4f7djbqihbc86dl7bb
- 整型特征哈希优化
categorical_column_with_hash_bucket
Int AsString再Hash,里面具体用了什么Hash函数
Tensorflow内置,循环移位和异或
AsString的代价太大
如果是均匀的uint64,可以直接作为hash值,或cast成char*
- 定长特征转换优化
定长特征是指使用接口tf.io.FixedLenFeature来解析的特征,比如用户的性别,年龄等,这类特征的长度通常都是定长的,并且固定为 1 维或多维。这类特征经过接口tf.io.parse_example 解析成 Dense Tensor,然后经过 Feature Column 处理,再进入到模型的输入层。
如果能直接将原始的 Input Tensor 转换成 One Hot Tensor,就可以省去两个转换过程,而且 Sparse Tensor 和 Dense Tensor 之间的转换其实是非常耗时的操作。
除了上面的 Vocabulary Categorical Column,还有别的类似 Feature Column 也有同样的问题,因此针对这类特征,平台专门开发了一套优化的 Feature Column 接口提供给业务使用,优化性能效果还不错。
- 用户特征去重优化
Q:你们的取embedding的平均latency有多少呢?
1ms左右。
Q:embedding怎么训练的?
用partitioned variable,放到多个PS上
Q:embedding怎么导出的?
定制Op,导出到redis上。
Q:例行任务时间单位,天
导出到redis+版本号
Q:Op的导出配置是什么?限流吗?
定时限速,间隔。
Q:Op是并行导出的吗?
每个feature会有自己的Op。
Q:Embedding会落盘吗?
不会。
Q:你们的取feature服务latency有多少?
整个推荐服务,客户通常要求控制在50-200ms,rank部分差不多要控制在30-50ms以内。
TP95 TP99 5ms以内,rank模型30-50ms。
Q:你们的机器用的多大?
推理平台统一
32 cpu 128G 的VM
Q:性能优化的一般思路?
profile分析关键路径,针对关键点着重优化
考虑绕过的情况,是否有其他解决方案
Q:redis存什么?
- 实时行为x
- 特征工程feature
- embedding
- i2i/u2i
- 缓存/hologres/mysql
Q: redis的内存淘汰策略知道吗?
在 64 位操作系统中 Redis 的内存大小是没有限制的,也就是配置项 maxmemory 是被注释掉的,这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操心系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此我们需要限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内存淘汰策略,内存淘汰策略在 Redis 4.0 之后有 8 种:
-
noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
-
allkeys-lru:淘汰整个键值中最久未使用的键值;
-
allkeys-random:随机淘汰任意键值;
-
volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
-
volatile-random:随机淘汰设置了过期时间的任意键值;
-
volatile-ttl:优先淘汰更早过期的键值。
在 Redis 4.0 版本中又新增了 2 种淘汰策略: -
volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
-
allkeys-lfu:淘汰整个键值中最少使用的键值。
其中 allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数据。
Q:超大规模数据遇到过性能问题吗?怎么解决的?
https://segmentfault.com/a/1190000022172968
- 缩短键值对的存储长度;
- 使用 lazy free(延迟删除)特性;
- 设置键值的过期时间;
- 禁用长耗时的查询命令;使用SCAN
- 使用 slowlog 优化耗时命令;
- 使用 Pipeline 批量操作数据;
- 避免大量数据同时失效;
- 客户端使用优化;
在客户端的使用上我们除了要尽量使用 Pipeline 的技术外,还需要注意要尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。 - 限制 Redis 内存大小;
- 使用物理机而非虚拟机安装 Redis 服务;
在虚拟机中运行 Redis 服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现,我们可以通过 ./redis-cli --intrinsic-latency 100 命令查看延迟时间,如果对 Redis 的性能有较高要求的话,应尽可能在物理机上直接部署 Redis 服务器。 - 检查数据持久化策略;
- 禁用 THP 特性;
- 使用分布式架构来增加读写速度。
推理平台
- 多模型合并
- N合1,放到一个session里
- 多框架支持
- C++ dlopen
- Java JVM JNI
- Python 固定入口
- ECI弹性扩容
- 双网卡网络直连
- 自定义镜像
- 镜像sidecar
EAS逐步暴露k8s的接口
ENABLE
场景优化
EAS设计
- failover
- 依赖k8s的probe
- 负载均衡
- 请求转发
公有云 SLA
Q: 训练的PS架构知道吗?
Parameter Server放参数,Worker执行训练。
Chief负责额外执行模型加载保存,初始化等功能。
evaluator需要放在Chief上。
TFJob负责机器调度,恢复。
- 线上服务
Q: 线上服务的failover如何处理?
k8s支持,Pod的probe监控
Q:服务并发
并发度 = 吞吐量 * 延迟 * batch
- 你觉得你自己
Q:做过的最突出/挑战性的工作是什么?
在工程上的事比较多,我觉得每个阶段都有比较值得我的工作。
FCNN加速后端,从编译器到RT驱动到硬件,全栈工程。团队协作分工,硬件背景驱动软件设计优化。
加速学习,理解业务和系统。
模型推理优化,接触各类模型,找出共性,Research,调研,技术选型,寻找合适的落地场景。
推理平台建设和垂直场景优化,通用平台功能,大客户定点支持,抽象复用,全栈工程x2。
Q:你遇到过的最大的挑战?
硬件转软件,runtime设计
Q:遇到过哪些大坑,是怎么躲开的?
- 过早优化和通用化,方案在迭代中改进,很多只是POC
- 闭源,内部技术迭代离社区渐行渐远,维护难度指数增加,难以PR merge
- 跨团队很重要,重复造轮子,技术选型和复用
Q:你最大的收获是什么?
- 考虑为什么多于怎么实现
- 考虑通用、抽象复用多于定制
- 工程能力
- 跨团队协作
大规模分布式工程能力
MT
Q: 特征工程如何保证离线在线一致性
https://zhuanlan.zhihu.com/p/65374268
说到是工程,其实就要处理一个很繁杂的问题——线上线下特征一致性。模型的试验一般在线下,通过离线方式进行,数据集自己从数据库之类的渠道采集,自己构建数据集,然后开始跑,但是还要面临一个上线的问题,对于同一个服务,线上的数据是按照数据库的格式过来的,要做的处理在线上还要再做一遍,然后才能得到符合你的模型格式的数据格式,这个一致性就是一个非常难的问题,因此线上线下就需要统一一个严格的格式,说白了就是为了保证一种"重现性"。
特征分为两部分:
- 离线特征,这部分是例行训练生成的,可以保证一致性,对新用户或新物品采取一致的默认值,0或历史平均。
- 实时特征,需要经过预处理,保证特征工程和后端服务的数据预处理逻辑一致。
Q: 训练需要考虑的通信开销优化方法
数据传输
数据分片
device placement
避免关键节点拥塞
增加带宽,专用协议
Q: 你们的后端服务的重排策略是硬编码吗?
后端服务模块化,可复用,召回、粗排、精排、重排,通过配置文件配置排序规则,尽量重用。
Q: 你们的AB服务是怎么做的?
自己开发的SDK,场景、层、桶三个层次。
场景是什么?场景是不同的推荐API。
层是召回、排序。
桶是不同的召回策略,向量召回、i2i,或排序策略。
Q: 你们的模型主要放在CPU上吗?如果放在GPU上呢?
主要是CPU,因为embedding的数量比较多。
Q: 超大规模Embedding是怎么做的?
redis,redis的开销比较高,但你们又做了一层缓存
Q: 分布式训练调优?
提高CPU分布式训练的训练速度,主要要从四个方面来考虑:
1)提高训练速度,主要是提高CPU的使用率;
2)提高通信速度,主要是减少通信传输的数据量;
3)提高数据IO速度;
4)更换分布式训练策略,提高分布式训练速度。
Q:分布式异步训练问题?
异步训练总体会训练速度会快很多,但是异步训练的一个很严重的问题是梯度失效问题(stale gradients),刚开始所有设备采用相同的参数来训练,但是异步情况下,某个设备完成一步训练后,可能发现模型参数已经被其它设备更新过了,此时这个设备计算出的梯度就过期了。由于梯度失效问题,异步训练可能陷入次优解(sub-optimal training performance)。图4中给出了一个具体的样例来说明异步模式的问题。其中黑色曲线展示了模型的损失函数,黑色小球表示了在t0时刻参数所对应的损失函数的大小。假设两个设备d0和d1在时间t0同时读取了参数的取值,那么设备d0和d1计算出来的梯度都会将小黑球向左移动。假设在时间t1设备d0已经完成了反向传播的计算并更新了参数,修改后的参数处于图4中小灰球的位置。然而这时的设备d1并不知道参数已经被更新了,所以在时间t2时,设备d1会继续将小球向左移动,使得小球的位置达到图4中小白球的地方。从图4中可以看到,当参数被调整到小白球的位置时,将无法达到最优点。
https://zhuanlan.zhihu.com/p/56991108
当然,任何的技术方案都是取舍,异步梯度更新的方式虽然大幅加快了训练速度,但带来的是模型一致性的丧失,也就是说并行训练的结果与原来的单点串行训练的结果是不一致的,这样的不一致会对模型收敛的速度造成一定影响。所以最终选取同步更新还是异步更新取决于不同模型对于一致性的敏感程度。这类似于一个模型超参数选取的问题,需要针对具体问题进行具体的验证。
除此之外,在同步和异步之间,还可以通过一些“最大延迟”等参数来限制异步的程度。比如可以限定在三轮迭代之内,模型参数必须更新一次,那么如果某worker节点计算了三轮梯度,该节点还未完成一次从server节点pull最新模型参数的过程,那么该worker节点就必须停下等待pull操作的完成。这是同步和异步之间的折衷方法。
https://zhuanlan.zhihu.com/p/82116922
Q: 无锁队列?
Q:大数归并排序?
外排序的一个例子是外归并排序(External merge sort),它读入一些能放在内存内的数据量,在内存中排序后输出为一个顺串(即是内部数据有序的临时文件),处理完所有的数据后再进行归并。[1][2]比如,要对900MB的数据进行排序,但机器上只有100 MB的可用内存时,外归并排序按如下方法操作:
读入100 MB的数据至内存中,用某种常规方式(如快速排序、堆排序、归并排序等方法)在内存中完成排序。
将排序完成的数据写入磁盘。
重复步骤1和2直到所有的数据都存入了不同的100 MB的块(临时文件)中。在这个例子中,有900 MB数据,单个临时文件大小为100 MB,所以会产生9个临时文件。
读入每个临时文件(顺串)的前10 MB( = 100 MB / (9块 + 1))的数据放入内存中的输入缓冲区,最后的10 MB作为输出缓冲区。(实践中,将输入缓冲适当调小,而适当增大输出缓冲区能获得更好的效果。)
执行九路归并算法,将结果输出到输出缓冲区。一旦输出缓冲区满,将缓冲区中的数据写出至目标文件,清空缓冲区。一旦9个输入缓冲区中的一个变空,就从这个缓冲区关联的文件,读入下一个10M数据,除非这个文件已读完。这是“外归并排序”能在主存外完成排序的关键步骤 – 因为“归并算法”(merge algorithm)对每一个大块只是顺序地做一轮访问(进行归并),每个大块不用完全载入主存。
Q:推荐分布式训练?
https://zhuanlan.zhihu.com/p/75946827
Q: 哈希膨胀