Skip to main content
  1. Posts/

推荐模型不只是一次 Forward:回看排序模型的 Serving 架构

·1821 words·4 mins
Table of Contents
Note: This article is available in Chinese only. 本文暂无英文版本。 View original

之前在腾讯做过一个面向推荐场景的深度学习框架(内部叫「无量」),我当时负责其中 Serving 相关的工作。后来和朋友聊起,有人问我:把一个推荐模型部署上线,是不是就是加载模型文件,然后来一个请求做一次 forward?

我发现这个问题很难用一句话回答。如果顺着数学公式去理解,推理确实就是一次 forward;但在真实的工程环境里,特别是推荐排序场景下,真实的执行路径要复杂得多。

回看当时的实现,最值得记录的是 embedding 的存放和读取:它占据了模型的大部分空间,却只有少量参数会在单次请求里被访问。远程存储、本地缓存和压缩,都和这个访问模式有关。

一种过度简化的直觉
#

顺着普通的神经网络经验,最自然的 Mental Model 可能长这样:

1model weights
23   load
45   input
67  forward
89  output

但这套模型真正在线上跑起来时,实际路径是这样的:

排序模型 Serving 架构

为什么不直接 load 然后 forward?问题卡在了一个非常特殊的系统约束上。

巨大的稀疏参数空间与 Sparse Access
#

推荐模型里最占地方的是 embedding,整个 embedding table 往往非常巨大。

但单次请求对 embedding 的访问是稀疏的。

整个 embedding table 虽然很大,但对于某一个特定的请求来说,它真正需要访问的 embedding 只是这片巨大 parameter space 中的极小一部分:

1Huge Embedding Table
23        │ current request
45     key_17
6     key_42
7     key_891
8     ...

这种强烈的 Sparse Access 模式意味着,在数学上它们都属于“模型参数”,但在系统层面,我们完全不应该对它们采用相同的存储和 Serving 策略

在当时的架构里,两类参数采用了不同的处理方式:

  • Sparse Parameters (Embedding):变成了基于 key 的远程数据检索(remote lookup)。
  • Dense Parameters:变成了本地的 TensorFlow 模型推理(local dense computation)。

Remote Lookup 与本地热点缓存
#

当时我们把这部分参数放进远程服务。Serving 会根据请求里的 sparse key,通过 RPC 跨网络从 Ronda Cache 去获取对应的 embedding。

这解决了“放不下”的问题,但跨网络带来了新的麻烦:如果每一个 key 的查询都去跑一次 RPC,请求的 latency、网络带宽消耗以及后端的 backend load 都会爆炸。

推荐流量里有一部分 key 会被频繁重复访问。

于是我们在 embedding lookup 发生前,又垫了一层 Local Hot-Key Cache

1              ┌── Local Hot-Key Cache
2Embedding Key ┤
3              └── Ronda Cache

命中本地缓存的 key 不需要再发起远程查询,既减少请求等待,也减轻后端负载。

用计算换数据:Embedding 压缩与 On-demand Restore
#

在当时的系统里,我们在 Ronda Cache 存储侧对 Embedding 做了 FP16 和 INT8 压缩。

在这条路径上,压缩收益会体现在参数存储、网络传输和缓存几个环节。

压缩 embedding,意味着更小的 storage footprint,也意味着更小的 memory footprint,最关键的是,它大幅度削减了 network transmission volume 和 cache footprint。

但这种压缩不是免费的。为了让后端的 TensorFlow dense model 继续推理,这些 FP16/INT8 的数据取回来之后,必须恢复成 FP32。这会带来额外的计算开销。

所以真正的执行路径更像是一种 On-demand Restore(按需恢复)

1compressed embedding space
23      lookup required keys
45    obtain required vectors
67 restore these vectors to FP32
89       model computation

因为单个请求只访问极小一部分 keys,系统就只为当前真正被消费的数据支付恢复成本

这里面藏着一个非常典型的 trade-off:我们是在用额外的计算成本,去交换更低的数据存储与移动成本。**在现代系统里,多执行一些 arithmetic 并不自动等价于更慢;相反,data movement 本身可能极其昂贵。**转换本身带来的性能问题,记录在CPU Serving 的压缩优化里。

服务启动:被忽略的 Runtime 配置
#

在画 request hot path 时,我们很容易把模型当作一个静态黑盒。但真实的 Serving 系统还有一个关键部分:服务启动与 Runtime 配置。

在我们的架构里,服务层是基于腾讯内部 RPC 框架 tRPC 开发的,启动时会从内部配置中心(七彩石)拉取一套 runtime configuration。

这套配置里,包含了一些直接影响 TensorFlow runtime 执行行为的参数,比如:

  • intra_op_thread_num:控制一个 TensorFlow op 内部执行时可使用的线程并行度。
  • inter_op_thread_num:控制不同、可以独立执行的 TensorFlow ops 之间的并发执行程度。

这两个参数告诉我一个直觉:即便扔给机器完全一样的模型 graph,只要 runtime execution configuration 不同,依然会产生非常不同的 Serving 行为。

Serving performance 绝不只取决于“模型长什么样”,也取决于“底层 runtime 到底是怎么调度和执行它的”。

回到 Serving 的执行路径
#

这套系统里,一次 forward 之前还有 embedding lookup、缓存查询和格式恢复;模型加载之后还有 runtime 的线程配置。只测 TensorFlow graph 的耗时,会漏掉这些开销。

对我来说,回看这套架构最有用的地方,是把每个组件重新放回请求经过的路径上:它持有什么数据,减少了哪一次访问,又把成本转移到了哪里。这样才方便继续判断该优化模型计算,还是参数读取。

Related

tensorflow 合并模型

·1696 words·4 mins
在这里存个备份,还有些问题没有解决。 raise ValueError(“GraphDef cannot be larger than 2GB.”) 记录一些思路好了。现在是没有生成.meta文件,爆掉应该是因为所有的变量都加载到了默认图里。

tensorflow checkpoint 学习笔记

·513 words·2 mins
参考资料: What is the TensorFlow checkpoint meta file? TensorFlow: Restoring variables from from multiple checkpoints 合并模型的时候发现.meta一直在累加,而其他数据文件没有改变。因此来探究一下checkpoint的几个文件的含义。