推荐模型为什么分成 embedding 表和稠密网络
推荐模型为什么分成 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 原生只提供了 Variable 和 Gather,用它们做推荐模型的表有两个问题: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 输入带来的结构,不是框架的设计,也不适用于图像和文本模型。它合理,因为表和网络在瓶颈、参数量、更新方式、部署位置四个维度上都相反,沿着这条线切开两边各得其所,主流框架和专用硬件都这样做。它的边界在两段之间的数据搬运,行为序列模型把这个边界推到了极限。替代方案里,取消表的做法还没有成功,改变比例的生成式推荐正在发生,但表仍然在。
暂无评论,欢迎留下第一条评论。