推荐模型为什么分成 embedding 表和稠密网络

推荐、广告、搜索排序模型几乎都可以描述成“一张很大的 embedding 表,加一个不大的稠密网络”。这篇文档回答三个问题:这个划分从哪里来,它是不是合理,业界有没有别的划分方式。面向做在线推理服务的工程师,例子沿用 DeepFM 那篇:用户 u123 搜“跑鞋”,候选广告 9001。

先说结论:这个划分不是 TensorFlow 的抽象,也不是所有深度学习模型的共性,而是以离散 ID 为主要输入的模型的共性。TensorFlow 只是把两段各自需要的算子都提供了,划分本身来自数据和硬件。

一、划分从哪里来:输入的性质

1. 三类模型的输入

模型 输入是什么 输入进网络前要做什么 表的规模
图像模型 一个像素矩阵,比如 224×224×3 个 0 到 255 的数 除以 255,不查表 没有表
语言模型 一串 token 编号,词表几万到几十万 查一张几万行的表 几万行 × 几千维,占参数的百分之几
推荐模型 几十个域的 ID:用户、商品、搜索词、类目、交叉特征 每个域查一张表 几亿到几十亿行 × 16 到 64 维,占参数的 99% 以上

像素和 token 之间有内在的顺序和距离,模型可以直接在上面做卷积或者 attention。用户 ID u123 和 u124 之间没有任何关系,编号只是名字。模型对一个名字唯一能做的事是“给它记一个向量”,向量的内容靠训练从这个名字出现过的样本里学出来。几亿个名字就是几亿行,表就这样出现了。

2. 一个例子看两段的差别

DeepFM 那篇的模型,用户表、搜索词表、广告表各几亿行,稠密部分是三层网络。一次请求 200 个候选:

表这一段 网络这一段
做什么 从几亿行里取出用到的几百行 200 行 × 640 列的矩阵乘几次
瓶颈 随机内存访问,每行一次 cache miss 浮点计算
参数量 3 亿行 × 16 维 × 4 字节 ≈ 19 GB 640×256 + 256×128 + 128×1 ≈ 0.8 MB
一条样本改多少参数 只改它碰到的几百行 全部 0.8 MB 都要改
参数放哪 大内存 CPU 机器,或者独立的 KV 存储 GPU 显存,每张卡一份完整拷贝
怎么并行训练 按行切到多台机器(模型并行),各自异步更新 每张卡算一部分样本(数据并行),梯度做一次同步

四个维度上两段都相反。用同一套机制管它们,不是浪费就是不够用:把表当普通变量放进 GPU 显存放不下,把网络当 KV 存储访问又太慢。沿着这条线切开,两边各用最合适的存储、并行和更新策略,是自然的选择。

3. 这条线不是 TensorFlow 画的

TensorFlow 原生只提供了 VariableGather,用它们做推荐模型的表有两个问题:Variable 的行数在建图时固定,新 ID 只能靠哈希取模挤进去;整张表是一个张量,导出、加载、更新都是整体操作。所以工业界在 TensorFlow 上做推荐时,几乎都换掉了这一段:阿里的 DeepRec、字节的 Monolith 都把静态 Variable 表替换成可以动态增删行的哈希表,配合无冲突哈希和按访问频次淘汰。稠密那一段用的仍然是原生算子。

这说明 TensorFlow 并没有为这个划分提供专门的抽象,是使用者按照数据的性质把表那一段改造了。PyTorch 那边同样如此,原生的 nn.Embedding 也是一个固定行数的张量,TorchRec 才是为推荐专门做的。

二、这个划分合理吗

合理,依据是它跟着数据和硬件的分界线走,而且被几乎所有推荐框架和一款专用硬件采纳。但它有边界。

1. 采纳它的框架

框架 表这一段怎么做 网络这一段怎么做 两段之间
Meta DLRM / PyTorch TorchRec EmbeddingBagCollection,按表或按行切到各张卡,模型并行 普通 PyTorch 模块,数据并行 一次 all-to-all 通信,把每张卡查到的向量发给需要它的卡
NVIDIA HugeCTR / Merlin GPU 显存里的哈希表,多卡分片 普通 GPU 计算 同上
阿里 DeepRec、字节 Monolith TensorFlow 上替换成动态哈希表,参数服务器存放 TensorFlow 原生 参数服务器远程查表
Google TPU SparseCore 专门做查表和梯度散射的硬件单元 TPU 的矩阵单元 芯片内部互联

Meta 的 DLRM 论文直接把推荐模型定义为“embedding 表 + 底部 MLP + 特征交互 + 顶部 MLP”,TorchRec 在 API 层就把模块分成两类。Google 把这个划分做进了硬件。一个抽象被三家独立实现,又被固化进芯片,说明它抓住的是问题本身的结构。

2. 边界在哪:夹在两段之间的计算

计算 输入来自 计算性质 通常归到哪
多值特征池化:搜索词的几个词向量取平均 很小的稠密计算 两边都有,表侧做能少传数据
FM 二阶项:所有域两两内积 稠密,和域数平方成正比 网络侧
行为序列 attention(DIN、SIM 这类) 表,一个用户几百到上万条历史 稠密,和序列长度成正比 网络侧,但查表压力在表侧
DCN 的交叉层 表输出的拼接向量 稠密 网络侧

这些计算都建立在表的输出之上,所以表的输出格式——每个域一个定长向量、拼接顺序——是两段之间的接口,改一边就要改另一边。表不只是存储,它的输出协议是模型结构的一部分。行为序列模型把这个接口拉得很宽:一次请求要查几百条历史的向量再做 attention,表这一段的读取量从几百行涨到几万行,两段之间的数据搬运本身成了瓶颈。这是划分开始吃力的地方,也是行为序列模型往往需要独立 GPU 集群的原因。

三、别的划分方式

真正不同的抽象有两条线,一条试图取消表,一条改变两段的比例。

1. 取消表:用计算换存储

Deep Hash Embedding 这类方法不存向量。把 ID 用 k 个不同的哈希函数算成 k 个整数,拼成一个 k 维向量,再用一个小网络把它算成 16 维的 embedding。参数只有那个小网络,几 MB;代价是每次“查表”变成一次前向计算,而且 k 个哈希值本身不携带任何关于这个 ID 的信息,全靠网络从样本里学出“哪些哈希组合对应什么向量”,训练慢、表达能力弱于直接查表。工业上还没有取代查表。

2. 改变比例:生成式推荐

Meta 的 HSTU、快手的 OneRec 这类模型把用户的行为历史当成 token 序列,用 Transformer 直接生成下一个要推荐的商品,结构接近语言模型。看起来没有“表 + 网络”的分法,但商品 ID 的 embedding 表仍然在,只是稠密部分从 MLP 换成了 Transformer,两段的比例变了:

传统推荐模型 语言模型 生成式推荐
几十亿行,占参数 99% 以上 几万行,占百分之几 几亿行,占比下降
网络 几层 MLP,几 MB 几十层 Transformer,几十 GB 十几层 Transformer,几百 MB 到几 GB
计算主要花在哪 查表 网络 逐渐转向网络

生成式推荐正在把推荐模型往语言模型的比例上推,表的相对重要性在下降。在线推理的形态也随之变化:查表这一段的优化空间缩小,稠密这一段需要 GPU 和动态批处理。但只要输入还是离散 ID,表就不会消失。

3. 同一划分下的变体

以下都是“表 + 网络”划分内部的不同选择,不是新抽象:

  • 表放图内还是图外:放图内部署简单、更新慢;放图外可以流式增量更新、独立扩容。
  • 表放 CPU 内存、GPU 显存还是远程 KV:取决于表的大小和延迟预算。
  • 用户塔和物料塔拆分:把网络按“只依赖用户”和“依赖候选”切开,前者一个请求算一次,后者按候选数算。这是网络一段内部的划分,不涉及表。
  • 双塔召回:用户和物料各走一个网络得到向量,用内积打分,物料向量可以离线算好建索引。表和网络的关系不变,只是网络的输出被拿去做近邻检索。

四、小结

“embedding 表 + 稠密网络”是离散 ID 输入带来的结构,不是框架的设计,也不适用于图像和文本模型。它合理,因为表和网络在瓶颈、参数量、更新方式、部署位置四个维度上都相反,沿着这条线切开两边各得其所,主流框架和专用硬件都这样做。它的边界在两段之间的数据搬运,行为序列模型把这个边界推到了极限。替代方案里,取消表的做法还没有成功,改变比例的生成式推荐正在发生,但表仍然在。