来源:美团技术团队
GPU 等专用芯片以较低的成本提供海量算力,已经成为机器学习领域的核心利器,在人工智能时代发挥着越来越重要的作用。如何利用 GPU 这一利器赋能业务场景,是很多技术研发者都要面临的问题。本文分享了美团外卖搜索 / 推荐业务中模型预估的 GPU 架构设计及落地的过程,希望能对从事相关应用研发的同学有所帮助或启发。
1 前言 近些年,随着机器学习技术的蓬勃发展,以 GPU 为代表的一系列专用芯片以优越的高性能计算能力和愈发低廉的成本,在机器学习领域得到广泛认可和青睐,且与传统的 CPU 体系不断融合,形成了新的异构硬件生态。
在这种技术浪潮之中,很多技术研发者会面临着这样的问题:在我们的业务上应用 GPU 硬件能获得什么?如何快速、平滑地从传统 CPU 体系基础上完成切换?站在机器学习算法设计的角度,又会带来什么影响和改变?在 GPU 生态下众多的技术路线和架构选型中,如何找到一条最适合自身场景的路径?
美团外卖搜索推荐团队,也面临着类似的挑战和问题。本文我们会分享美团外卖搜索 / 推荐业务中,模型预估的 GPU 架构设计与落地过程,并将一些技术细节和测试数据做了详尽的披露,希望能为广大的技术同行提供一些有价值的参考。
2 背景
当前,美团外卖主要通过搜索和推荐两种流量分发方式,满足用户对 “万物到家” 的需求。除了首页的搜索、推荐功能外,重点品类会在首页增加独立入口(下文称之为 “金刚”),每个金刚入口中都有类似于首页搜索、推荐的区域,而不同场景入口共同服务于外卖的最终成单。首页、金刚、店内的联动关系如下图所示:
面向点击率(CTR)/ 转化率(CVR)预估的深度学习,是每一个电商类搜索 / 推荐产品中的核心技术,直接决定了产品的用户体验和转化效果,同时也是机器资源消耗的 “大户”。而 CTR/CVR 精排模型的设计和实践,也是美团外卖搜索推荐(下称搜推)技术团队必须要攻克且不断追求卓越的必争之地。
从搜推系统设计的角度上看,不同的搜索、推荐入口会自然形成独立的调用链路。在传统的模型设计思路下,会对不同入口链路、不同漏斗环节的 CTR/CVR/PRICE 多个目标独立设计模型,这也是美团外卖搜推过往模型设计的经典方式。而从 2021 年起,基于多场景全局优化的考量,搜推场景的 CTR/CVR 预估模型开始逐步走向多模型统一,综合利用多个入口的数据、结合不同入口自身的业务特点实现多个入口的联动优化,逐步实现 “One Model to Serve All” 的目标。
从模型计算实践的角度上看,外卖精排模型的发展,让模型 Dense 网络的计算量显著膨胀,以 CPU 为计算主力的软硬件架构已经难以应对算法的发展需求,即便成本消耗大幅加剧,算力天花板仍然 “近在咫尺”。而 GPU 硬件面向稠密计算的算力优势,恰恰吻合新的模型特点,可以从根本上打破精排模型预估 / 训练中的算力困局。因此,从 2021 年开始,美团外卖搜推场景的深度学习体系开始逐步从纯 CPU 架构走向 CPU+GPU 的异构硬件计算平台,以满足美团外卖模型算法演进对算力的新要求。
本文接下来的内容,会从外卖搜推场景的精排模型设计出发,结合美团实际的软硬件特点,为大家详细分享在外卖精排模型预估领域,从纯 CPU 架构转型到 CPU+GPU 异构平台的探索和实践过程,供广大技术同行参考。
3 外卖搜推场景下的精排模型
本章节主要介绍在外卖场景下多模型统一的演进思路、模型特点以及在实践中的挑战。本文只对模型设计思路做简单的说明,引出后续模型计算在 GPU 落地中的实践思考。
3.1 精排模型的设计思路
如前文所述,在美团外卖多入口联动的场景特点下,经典的单体模型设计存在着以下局限:
- 首页推荐与各金刚入口推荐各维护一个精排模型,不仅维护成本高而且训练数据割裂,导致精排模型不能捕捉到用户在所有推荐场景的兴趣。
- 推荐场景的精排模型只使用推荐场景的训练样本,未利用用户在其他重要入口的训练样本,比如搜索、订单页,模型只学习到用户在局部场景的偏好信息。
- 推荐场景的训练样本中存在 Position Bias 问题,具体是指用户点击一个商家,有可能只是因为该商家在推荐 Feeds 中排序位置比较靠前,而非因为用户对此商家真正感兴趣,此类 Bias 会引起模型训练有偏。
- 多目标之间存在贝叶斯约束,网络结构中未考虑,CXR=CTR × CVR,CXR 预估值应比 CTR 小,模型在验证集上会出现 CXR 比 CTR 还高的现象,预估不准确。
- CTR/CXR 多目标的融合,实现多目标预测的模型统一。
- 场景专家网络与 Attention 网络的融合,实现不同流量入口之间的模型泛化和统一。
- 领域专属网络和共享网络的融合,实现推荐场景向搜索场景的迁移学习。
随着外卖精排模型的发展和演进,模型 Dense 网络的参数量显著增加,单次推理的 FLOPs 达到 26M,对 CPU 计算架构造成了巨大压力。另一方面,我们采用 Float 16 压缩、特征自动选择、网络交叉替代手动交叉特征等技术手段,将模型由 100G 缩小到 10G 以内,并且过程中通过模型的优化,做到了模型效果无损。
综上,外卖搜推精排模型稠密部分计算复杂、稀疏部分体积可控,这些良好的特性,为我们在 GPU 硬件架构上落地推理计算提供了相对适宜的模型算法基础。接下来,我们将探讨如何在高吞吐、低耗时的外卖搜索推荐系统中,利用 GPU 硬件有效解决外卖精排模型在线预估中的成本和性能问题,并给出我们的实践过程和结果。
3.2 模型应用的特点与挑战
在搜索 / 推荐技术领域中,稀疏模型预估(CTR/CVR)是决定算法效果的核心要素,模型预估服务是搜索推荐系统中必不可少的组成部分,业内各大公司已有很多经典的实现方案。在讨论具体实践之前,先介绍一下我们的场景特点:
① 需求层面
- 模型结构:如前文介绍,外卖场景下的精排模型的稠密网络部分相对复杂,单次推理的 FLOPs 达到 26M;而模型的稀疏部分经过大量的优化,体积得到了有效的控制,模型规模在 10G 以内。
- 服务质量要求:推荐服务作为经典的高性能 To C 场景,业内大部分同类系统的超时控制在百毫秒量级,分解到预估服务,超时一般需要控制在十毫秒的量级。
- 开发框架:模型开发采用 TensorFlow 框架 [1]。作为主流的深度学习第二代框架,TensorFlow 具备强大的模型表达能力,这也导致其算子粒度比较小,这一特点无论是对 CPU 还是 GPU 架构都会带来很大的额外开销。
- 在线服务框架:采用 TensorFlow Serving 框架 [2]。基于此框架,可将离线训练好的机器学习模型部署到线上,并利用 rpc 对外提供实时预估服务。TensorFlow Serving 支持模型热更新及模型版本管理,主要特点是使用灵活,性能较好。
- 机型特性:美团基于提升算力密度的考量,在预估服务采用了 GPU BOX 机型。相对于传统的 GPU 插卡机型,这一类机型每张 GPU 卡配套的 CPU 和内存相对有限,这需要我们在设计在线服务时,精细化的考量 CPU、GPU 上的计算和数据分布,达到更好的利用率均衡。
- GPU 固有属性:GPU kernel 大体上可以划分为传输数据、kernel 启动、kernel 计算等几个阶段 [3],其中每个 kernel 的启动需要约 10us 左右。因此,GPU 预估会面临一个普适问题,大量的小算子导致每个 kernel 的执行时间很短,kernel 启动的耗时占了大部分。相邻的 kernel 之间需要通过读写显存进行数据的传输,产生大量的访存开销。而 GPU 的访存吞吐远远低于计算吞吐,导致性能低下,GPU 的利用率并不高。
总结而言,与业内其他主流搜推场景相对比,我们的 CTR 模型预估场景有两个明显特点:
- 稠密网络部分计算复杂度高,相对的,稀疏网络在模型设计环节经过了大量的优化,体积相对较小。
- 使用 GPU BOX 机型,单 GPU 卡的 CPU 配额受限,需要针对性优化 CPU 的计算负荷。
4 模型服务架构概览
本章节简要介绍美团外卖搜推在线预估服务的整体架构和角色分工,也是外卖搜推精排模型在 GPU 落地实践的工程系统基础。
系统关键角色
- Dispatch:承担着特征获取和特征计算的职能,如前文所述,美团使用 GPU BOX 机型搭建预估服务,推理计算的 CPU 资源本身就十分吃紧,因此自然会考虑将在线特征工程部分独立部署,避免 CPU 资源的抢占。本部分和 GPU 实践关系不大,不是本文的重点。
- Engine:承担着模型在线推理的职能,通过 RPC 的方式输入特征矩阵、输出预估结果。采用 GPU BOX 机型(单容器 8 核 + 1 NVIDIA Tesla T4),平均响应时间需控制在 20ms 以内,下文所述 GPU 优化实践主要面向这一模块的特点进行。
- Booster:在模型更新过程中离线执行的模型优化器,内部以 Optimizer 插件的方式,混合了手工优化器插件和 DL 编译优化器插件,是下文所述 GPU 优化操作的执行者。
5 GPU 优化实践
本章节将展开分享精排模型预估计算在 GPU 架构落地中的优化过程。
与 CV、NLP 等经典机器学习领域不同,以 CTR 模型为代表的稀疏模型由于结构多变、包含大量业务特化等原因,硬件供应商难以对这一类未收敛的模型结构提供端到端优化工具。因此,在 CTR 模型大规模应用的领域中,一般会结合 GPU 特性,面向使用场景对模型执行 Case By Case 的优化措施。按模型优化的目标来区分,可以大致分类为系统优化和计算优化:
① 系统优化:一般指通过对计算、存储、传输的调度,使 CPU+GPU 的异构硬件体系可以更有效率的协同和被使用。典型的系统优化包括:
- 设备摆放
- 算子融合
- GPU 并发 / 流水线优化
- 冗余计算去除
- 量化计算
- 高性能库的应用
5.1 系统优化
5.1.1 设备摆放
TensorFlow 会为计算图中每个 Node 自动设置 Runtime Device,计算较重者放置在 GPU,计算较轻者放置在 CPU。在复杂计算图中完成一次完整的 inference,数据会在 CPU 和 GPU 之间反复传输。由于 H2D/D2H 传输很重,这会造成数据传输耗时远大于 op(operator)内部计算耗时,在 GPU 下模型一次预估耗时为秒级别,远高于只使用 CPU 时的耗时。此外,如前所述,我们所使用的 GPU 机型上 CPU 资源受限(一张 T4 卡仅对应 8 核 CPU),这也是我们在异构架构设计中需要解决的核心技术挑战。
为解决 TensorFlow 自动设定 Runtime Device 不合理的问题,我们为计算图中每个 Node 手动 Set Runtime Device。考虑到 CPU 资源受限,我们尽量的将计算较重的子图(包括 Attention 子图、MLP 子图)放置在 GPU 计算,计算较轻的子图(主要为 Embedding 查询子图)放置在 CPU 计算。
为进一步减少设备间数据传输,我们在 CPU 和 GPU 之间增加 Concat op 和 Split op,CPU 数据先 Concat 到一起再传输到 GPU,之后再按需 Split 成多份并传给对应 op,将 H2D/D2H 从上千次降低到数次。如下图所示,设备摆放优化之前,有大量的 H2D 数据传输;优化之后,H2D 减少为 3 次,优化效果十分明显。
5.1.2 All On GPU
完成基本的设备摆放优化后,计算较轻的 Sparse 查询部分在 CPU 完成,计算较重的 Dense 计算部分在 GPU 完成。虽然 CPU 上计算较轻,但压测发现其仍旧是整体吞吐瓶颈。考虑到整体计算图较小(约 2G),我们自然的想到是否可以将整图放在 GPU 执行,绕开 CPU 配额的限制,此即 All On GPU。为了将原在 CPU 进行的 Saprse 查询改为在 GPU 执行,我们新增了 LookupTable op 的 GPU 实现。如下图所示,HashTable 放置在 GPU Global Memory,它的 Key 与 Value 统一存储在 Bucket 中。针对输入的多组 Key,利用多个 Block 的 Threads 并行查询。
同时,为提高 GPU 利用效率,降低 kernel launch 开销,我们利用 TVM 对计算图进行编译优化(下文会进行详细介绍)。优化后的 All On GPU 模型图解决了 CPU 资源受限带来的瓶颈,整体吞吐提升明显(qps 55->220,约 4 倍)。
5.1.3 算子融合
外卖搜推精排模型十分复杂,计算图中包含上万个计算 Node。GPU 上执行计算图时,每个 Node 都有 kernel launch 开销,多个 Node 之间还有访问显存开销。此外,TensorFlow 框架本身在 Node 执行时会带来一定开销,每个 Node 执行时都会创建、销毁 Input/Output Tensor,内存控制引入额外成本。因此,计算图中 Node 过多会严重影响执行效率。为解决这一问题,常用的方法是进行算子融合,即在计算结果等价的前提下,将多个 Node 融合成一个 Node,尽量降低计算图 Node 数量,这样既可以将 Node 之间的访问显存开销转为访问寄存器开销,同时也可以减少计算图中每个 Node 带来的固定开销。
算子融合主要通过三种方式进行:
- 特定算子手动融合。例如模型训练阶段中,针对一个 Embedding Table 会有多个 Node 访问,在线预估阶段可将其融合成一个 Node,即查询 Node 和 Embedding Table 一一对应。此后可进一步融合算子,一个 Node 负责查询多个 Embeddding Table。
- 常见算子自动融合,主要是利用 TensorFlow Grappler [4] 优化器进行算子自动融合。
- 利用深度学习编译器自动融合,下文会详细进行介绍。
5.2 计算优化
5.2.1 FP16 低精度优化
一方面,在 CPU 架构下,为了降低内存开销,已经将 Embedding Table 压缩为 FP16 [5] 存储,但是计算时仍会展开为 FP32,这引入了转换开销;另一方面,模型预估仅进行模型图的前向计算,使用低精度计算引入的误差较小。因此,业界普遍使用低精度方式进行模型预估计算。
针对当前的业务场景,我们尝试了 FP16、INT8 [6] 等低精度计算,FP16 半精度计算对模型效果无明显影响,INT8 量化则会造成效果衰减。因此,我们采用 FP16 半精度计算的方式,在不影响模型效果的前提下,进一步提升预估服务的吞吐。
5.2.2 broadcast 优化
模型图中的数据可以分为 user 和 item 两类。通常情况下,请求中包含一个 user 以及多个 item。在模型 Sparse 部分,user 和 item 分别获取 Embedding;在模型 Dense 部分,两类 Embedding 组合成矩阵后进行计算。经过深入分析,我们发现模型图中存在冗余查询和计算。如下图橙色部分所示,在模型 Sparse 部分,user 信息先被 broadcast 成 batchsize 大小再去查询 Embedding,导致同一个 Embedding 查询了 batchsize 次;在模型 Dense 部分,user 信息同样被 broadcast 成 batchsize 大小,再进行之后所有计算,实际上在和 item 交叉之前不必 broadcast user,同样存在冗余计算。
针对以上问题,我们对模型图进行了手工优化,如下图紫色部分所示,在模型 Sparse 部分,user 信息只查询一次 Embedding;在模型 Dense 部分,user 信息与 item 交叉时再 broadcast 成 batchsize 大小,即整体上将 user 信息的 broadcast 后置。
5.2.3 高性能库应用
使用 CPU 时,可以利用 Intel MKL [7] 库对计算进行加速。受限于 CPU 硬件特点,加速效果有限。使用 GPU 时,我们可以利用 Tensor Core [8] 进行加速计算。每个 Tensor Core 都是一个矩阵乘累加计算单元,当前使用的 NVIDIA T4 卡具有 320 个 Tensor Core,在混合精度计算时算力为 65 TFLOPS,在单精度计算时算力为 8.1 TFLOPS,具有极强的推理性能。在 TensorFlow 中,可利用 cuBLAS [9] 调用 Tensor Core 进行 GEMM 加速计算,利用 cuDNN [10] 调用 Tensor Core 进行 CNN、RNN 网络加速计算。
5.3 基于 DL 编译器的自动优化
随着深度学习网络越来越复杂(Wider And Deeper),硬件设备越来越多样(CPU、GPU、NPU),神经网络的优化工作也变得越来越困难。在单一硬件、单一框架上的优化会受到优化库限制,很难进一步调优。在不同硬件、不同框架的优化又很难做到通用,优化很难移植。这导致优化神经网络时,需要大量的手动调优工作,成本很高。
为了降低手动优化的成本,业界普遍使用深度学习编译器(Deep Learning Compiler)对计算图进行自动调优。比较流行的深度学习编译器包括 TensorRT [11]、TVM [12]、XLA [13] 等,我们在当前的模型场景下利用深度学习编译器做了较多的优化尝试,下文会详细进行介绍。
5.3.1 基于 TensorRT 的尝试
TensorRT 是 NVIDIA 推出的高性能深度学习推理优化框架,支持自动算子融合、量化计算、多流执行等多种优化手段,并且可以针对具体 kernel 选择最优实现。TensorRT 的各优化均通过对应开关控制,使用很简单;但是整体闭源,并且支持的算子不多,只能对计算图的部分算子做优化,遇到不识别的算子则会跳过,十分影响优化效率。利用 TensorRT 优化后的计算图,仍旧存在大量 op,整体性能提升有限。为解决这个问题,我们从以下两个角度进行尝试。
① 手动切分子图
利用 TensorRT 进行图优化时,会先利用 Union Find 算法在全图中寻找可识别 op 并将其聚类,每个聚类进行具体的编译优化,并产生一个对应的 TRTEngineOp。由于计算图中存在大量不识别 op,对聚类过程造成了干扰,即使可识别 OP 也不一定能完成聚类,则无法进行对应编译优化,造成优化效率较低。为解决这一问题,图优化前我们先进行手动切图,将全计算图切分为若干个子图,每个可识别 op 都放入对应子图中,并将子图送入 TensorRT 进行优化。通过这一方法,有效解决了可识别 op 未优化的问题,有效降低了全图 OP 数量。
② 算子替换
如前所述,TensorRT 支持 OP 类型有限,全图中存在大量 TensorRT 无法识别的 op,导致优化效率偏低。为了缓解这一问题,我们将 TensorRT 不识别的 OP 尽量替换成其支持的等价 op。例如下图中,TensorRT 无法识别 Select op,我们将其替换成 TensorRT 支持的 Multiply op,并将 Select 关联的 ExpandDims op 从图中消掉。经过类似的等价转换操作,有效降低了未识别 op 数量,提高了编译优化覆盖率。
5.3.2 基于 TVM 的尝试
在尝试 TensorRT 优化时我们发现,TensorRT 对 TensorFlow 的算子覆盖率较低(只能覆盖约 50 + 算子),在当前的模型计算图中,有十多个算子无法支持。即使经过复杂的算子替换优化工作,仍然存在多个算子难以替换。由此我们思考采用其他的深度学习编译器进行图优化。
TVM 是陈天奇团队推出的端到端机器学习自动编译框架,在业界广泛使用。和 TensorRT 相比,TVM 代码开源,具有更强的拓展性和定制能力。此外,TVM 支持的 TensorFlow 算子超过 130 个,算子覆盖率远超 TensorRT。在当前计算图中,TVM 不支持的 OP 只有自定义的 LookupTable,这一 OP 负责查询 Embedding,无需进行编译优化。
因此,我们尝试利用 TVM 取代 TensorRT 对当前计算图进行自动编译优化。考虑到 TensorFlow 对 TensorRT、XLA 均做了官方支持,实现了对应的 wrapper op,但目前尚未支持 TVM,我们对 TensorFlow 做了适配改造,采用和 TensorRT 类似的方式,实现了 TVMEngineOp 以支持 TVM。考虑模型特点,我们将计算较重的 Attention 子图和 MLP 子图放入了 TVMEngineOp 中,利用 TVM 进行编译优化,如下图所示:
6 性能表现与分析
本章节展示实际生产环境下的测试数据,并分析上文一系列业内典型优化思路,在我们的特定场景下的表现及其背后原因。
压测环境中,CPU 环境为 32 核 Intel (R) Xeon (R) Gold 5218 CPU @ 2.30GHz+32G 内存,GPU 环境为 8 核 Intel (R) Xeon (R) Gold 5218 CPU @ 2.30GHz+Tesla T4 GPU+16G 内存。上图中,左图对比了不同 QPS 下(x 轴),精排模型在不同优化手段下的推理耗时(y 轴),其中 base-gpu 表示只经过简单的图优化并在 GPU 计算,trt 表示经过 TensorRT 优化并在 GPU 计算,tvm 表示经过 TVM 优化且叠加 All On GPU 优化并在 GPU 计算;右图表示极限 QPS 下,不同优化手段对应的 CPU 和 GPU 利用率。从图中可以看出:
- 只利用 CPU 进行预估计算时,极限 qps 为 55,此时 CPU 利用率已经高达 76%,成为瓶颈。
- 利用常规手工优化(设备摆放 + 算子融合 + Broadcast 优化 + 高性能库)的 GPU 预估时,相同 qps 下 latency 大幅降低,且可以将极限 qps 提升至 85(较 CPU 版提升 55%)。到达极限吞吐时 GPU 利用率并不高,瓶颈仍旧为 CPU 利用率。
- 利用 TensorRT 优化预估(手工优化 + TensorRT+FP16)时,得益于图编译优化,相同 qps 下 latency 降低约 40%。由于瓶颈仍为 CPU,极限吞吐未变化。
- 利用 TVM 优化预估(手工优化 + TVM+FP16+All On GPU)时,将所有 OP 都放置于 GPU 计算,CPU 只负责基本的 RPC,极大缓解了 CPU 配额的瓶颈。相同 qps 下 latency 大幅降低约 70%,极限吞吐大幅提升约 120%。到达极限吞吐时,GPU 利用率较高,成为瓶颈。
7 总结
综上,我们针对美团外卖场景的业务特点,将经典的 CTR/CVR 模型从多入口、多环节、多目标的单体模型,逐步演进到 “One Model to Serve All” 的多模型统一形态。
同时,结合美团的硬件条件和基础,实现了纯 CPU 预估架构向 CPU+GPU 异构架构的切换,在固定成本前提下,有效的释放了算力空间,计算吞吐提升了近 4 倍。针对 GPU BOX 机型对 CPU 资源的限制,我们采用手工优化 + DL 编译优化结合、模型网络计算 All On GPU 的思路,有效的提升了 GPU 在模型预估计算中的利用率,并在本文中详细分享了 GPU 落地中的优化过程和实测数据指标。
8 作者简介
- 杨杰、俊文、瑞东、封宇、王超、张鹏等,来自到家事业群 / 到家研发平台 / 搜索推荐技术部。
- 王新、陈卓、駃飞等,来自基础研发平台 / 数据科学与平台部 / 数据平台中心。