RankMixer:工业级推荐排序模型的规模化之路
论文:RankMixer: Scaling Up Ranking Models in Industrial Recommenders 机构:字节跳动(抖音推荐算法团队) arXiv: 2507.15551
1 动机
LLM 通过不断堆参数验证了 Scaling Law,一个自然的问题是:推荐排序模型能不能也这样做大? 现实中有两个卡点:
- 硬约束:线上精排要在十几毫秒内完成打分并支撑高 QPS,不可能像训练 LLM 那样无限堆算力。
- 架构落后:现有排序模型(DeepFM、DCN、AutoInt、DHEN 等)依赖内积、逐元素乘法、注意力等人工特征交叉算子,这些算子大多是访存密集型(Memory-bound)而非计算密集型(Compute-bound),无法吃满现代 GPU 的大矩阵乘算力——现代 LLM 的 MFU(算力利用率)普遍在 40%+,而字节线上精排模型当时只有 4.5%。MFU 低意味着参数量和计算成本近似线性绑定,一旦放大参数,FLOPs 和时延立刻爆炸,Scaling Law 的收益还没吃到,成本就先扛不住了。
为此,论文提出 RankMixer:一种硬件感知(hardware-aware)的统一特征交互架构,同时满足”硬件对齐(转化为 GPU 高效的大矩阵乘)”与”契合推荐数据特性(建模数百个异构特征域的个性化交互)”两个约束。核心做法是用无参数的 Multi-Head Token Mixing 替代平方复杂度的 Self-Attention,用 Per-token FFN 建模特征子空间的独立表达与交互,并扩展出 Sparse-MoE 变体进一步提升容量。
最终效果:MFU 从 4.5% 提升到 45%(约10倍),在几乎不增加推理时延的前提下将线上模型参数规模从 16M 提升到 1.1B(约70倍),全量上线抖音 Feed 精排后用户活跃天数 +0.3%、App 使用时长 +1.08%。
2 模型架构
RankMixer 整体架构类似 Transformer 的层次堆叠结构:输入被 Token 化为 $T$ 个特征 Token,经过 $L$ 层 RankMixer Block 逐层refine,最后做 mean pooling 得到输出表征,用于多任务预测(如 finish、skip、like、comment 等)。
2.1 总体公式
每一个 RankMixer Block 包含两个核心模块:Multi-Head Token Mixing 和 Per-Token FFN(PFFN),公式为:
\[S_{n-1} = \mathrm{LN}\big(\mathrm{TokenMixing}(X_{n-1}) + X_{n-1}\big)\] \[X_n = \mathrm{LN}\big(\mathrm{PFFN}(S_{n-1}) + S_{n-1}\big)\]其中:
- $\mathrm{LN}(\cdot)$ 是 LayerNorm
- $X_n \in \mathbb{R}^{T \times D}$ 是第 $n$ 层 RankMixer Block 的输出
- $X_0 \in \mathbb{R}^{T \times D}$ 由初始的 $T$ 个 Token($x_1, x_2, …, x_T$)堆叠而成
- $D$ 是模型隐藏维度
最终输出 $o_{output}$ 来自最后一层 $X_L$ 的 mean pooling,再接不同任务头输出预测。
整体结构如下图所示(对应论文 Figure 1,图中以 T=4 个 Token、Multi-Head Token Mixing 拆出 3 个 head 为例展示了内部机制):
图1:RankMixer 整体架构。输入特征经 Tokenization 得到 T 个 Token,堆叠 L 层 RankMixer Block(Multi-Head Token Mixing + Per-token FFN/SMoE,均带残差与LayerNorm),最后 mean pooling 输出多任务预测。
2.2 输入层与特征 Tokenization
2.2.1 为什么需要 Tokenization
推荐模型的输入特征包括:
- User Profiles:用户 ID、用户画像等
- Video/Candidate Features:视频 ID、作者 ID 等
- Sequence Features:经过序列建模模块(如 LONGER)处理后的用户行为序列表征 $e_s$
- Cross Features(Interacted Features):用户与候选物品之间的交叉特征
这些特征会先各自 Embedding 化,得到维度不一的 embedding 向量。为了在后续阶段实现高效并行计算,必须把这些”维度参差不齐”的 embedding 转换为维度对齐的向量,论文称这一过程为 Tokenization。
论文分析了两种朴素策略的问题:
- 每个特征一个 Token:由于特征数量高达数百个,会导致每个 Token 分到的参数量和计算量都极小,重要特征建模不充分,同时大量小 Token 也会造成 GPU 利用率低下(矩阵形状太”瘦”,无法发挥 GEMM 优势)。
- 只用一个 Token(全部特征拼接后过一个 DNN):模型退化为普通 DNN,无法区分不同特征子空间,高频/头部特征会淹没长尾特征信号。
2.2.2 基于语义分组的 Tokenization 方案
RankMixer 采用基于领域知识的语义分组策略:先利用业务先验知识,将数百个特征按语义划分为 $N$ 个组(比如”用户画像组”“视频内容组”“交互统计组”等),组内特征顺序拼接得到一个大向量:
\[e_{input} = [e_1; e_2; \dots; e_N]\]再将这个拼接向量等距切分为 $T$ 个固定维度 $d$ 的片段,每个片段经过投影映射到统一的模型维度 $D$,得到第 $i$ 个 Token:
\[x_i = \mathrm{Proj}\big(e_{input}[d\cdot(i-1) : d\cdot i]\big), \quad i = 1, \dots, T\]其中:
- $e_{input}$:拼接后的特征向量
- $d$:每个 Token 切分前的固定维度
- $N$:特征分组数
- $T$:最终生成的 Token 数量
- $\mathrm{Proj}(\cdot)$:将切分片段映射到模型宽度 $D$ 的线性投影
这样每个 Token $x_i \in \mathbb{R}^D$ 都代表一组语义相对一致的特征子空间,既避免了 Token 过多导致的碎片化,也避免了单一 Token 导致的特征淹没问题。
2.3 RankMixer Block 详解
2.3.1 Multi-Head Token Mixing:无参数的特征交互算子
这是 RankMixer 替代 Self-Attention 的核心模块,目的是让不同 Token(即不同特征子空间)之间做全局信息交换。
具体做法:每个 Token $x_t$ 被均分为 $H$ 个 head:
\[\big[x_t^{(1)} \Vert x_t^{(2)} \Vert \dots \Vert x_t^{(H)}\big] = \mathrm{SplitHead}(x_t)\]可以把每个 head 理解成 Token 在某个低维子空间上的投影——因为推荐任务本身需要从多个不同”视角”看待特征。
然后,Token Mixing 做的事情是:把所有 Token 的同一个 head 位置拼接(Concat)在一起,形成新的”混合 Token”:
\[s^h = \mathrm{Concat}\big(x_1^h, x_2^h, \dots, x_T^h\big) \\ \quad h = 1, \dots, H\]即:第 $h$ 个混合 Token $s^h$,由原来 $T$ 个 Token 各自的第 $h$ 个 head 拼接而成。这本质上是一次跨 Token 的维度重排(Shuffle),不引入任何可学习参数——因此是”parameter-free”的。
Token Mixing 输出为 $S \in \mathbb{R}^{H \times \frac{TD}{H}}$,由 $s^1, s^2, \dots, s^H$ 堆叠而成。论文中设定 $H = T$,即 head 数等于 Token 数,这样混合后 Token 数量保持不变,方便做残差连接:
\[s_1, \dots, s_T = \mathrm{LN}\big(\mathrm{TokenMixing}(x_1,\dots,x_T) + (x_1,\dots,x_T)\big)\]为什么不用 Self-Attention? 论文给出了明确论证:Self-Attention 的注意力权重基于 Token 间的内积相似度计算,这在 NLP 中效果很好,因为所有 token 共享统一的语言语义空间。但在推荐场景中,特征空间天然异构——用户侧和物品侧的 ID 空间可能各自包含数亿个元素,计算这些异构语义空间之间的内积相似度本身就”没有明确意义”,容易引入噪声,还会带来更高的计算量、显存 IO 开销和显存占用。实验结果也验证了这一点(见下文消融实验,Self-Attention 路由的 AUC 反而略逊于 Token Mixing,同时 FLOPs 增加 71.8%)。
2.3.2 Per-token FFN:参数隔离的特征子空间建模
传统 DLRM/DHEN 类模型往往把不同语义空间的特征塞进同一个交互模块共同处理,容易造成”高频特征域主导,长尾/低频特征信号被淹没”的问题。
RankMixer 提出 Per-token FFN(PFFN):每个 Token 拥有独立的、不共享的一套 FFN 参数,而不是像标准 Transformer 那样所有 Token 共享同一个 FFN。对第 $t$ 个 Token $s_t$:
\[v_t = f_{pffn}^{t,2}\Big(\mathrm{Gelu}\big(f_{pffn}^{t,1}(s_t)\big)\Big)\]其中第 $i$ 层线性变换为:
\[f_{pffn}^{t,i}(x) = x W_{pffn}^{t,i} + b_{pffn}^{t,i}\]参数形状:
- $W_{pffn}^{t,1} \in \mathbb{R}^{D \times kD}$,$b_{pffn}^{t,1} \in \mathbb{R}^{kD}$
- $W_{pffn}^{t,2} \in \mathbb{R}^{kD \times D}$,$b_{pffn}^{t,2} \in \mathbb{R}^{D}$
- $k$ 为超参数,控制 FFN 隐藏维度相对 $D$ 的放大比例
- $\mathrm{Gelu}(\cdot)$ 为激活函数
整个 PFFN 模块可写作:
\[v_1, \dots, v_T = \mathrm{PFFN}(s_1, \dots, s_T)\]PFFN 与传统结构的本质区别(论文特别强调):
| 结构 | 输入 | 参数 |
|---|---|---|
| 标准 Transformer FFN | 不同 Token 各自输入 | 所有 Token 共享一套 FFN 参数 |
| MMoE 中的 Expert | 所有 Expert 共享同一个输入 | 不同 Expert 参数不同 |
| RankMixer 的 Per-token FFN | 不同 Token 各自不同的输入 | 不同 Token 各自独立的参数 |
也就是说,PFFN 是”输入和参数同时按 Token 切分“,这种设计天然契合”让不同特征子空间学到有差异化的表达”这一目标,在不增加计算复杂度(相对共享 FFN 而言,计算量不变,只是参数量增加)的前提下显著提升了模型容量。
2.3.3 Sparse-MoE 变体:进一步提升 ROI
Per-token FFN 已经让参数量随 Token 数线性增长,如果想进一步扩容,一个自然的思路是把每个 Token 的 Dense FFN 换成 Sparse Mixture-of-Experts(SMoE):模型容量继续增大,但计算量基本不变。
但论文发现,朴素的 Sparse-MoE 在 RankMixer 里会”水土不服”,原因有两点:
- 均匀 Top-k 路由的问题:Top-k 选择对所有 Token 一视同仁,把有限的专家预算浪费在低信息量 Token 上,同时”饿”到了高信息量 Token,抹杀了 Token 间信息密度的差异。
- 专家训练不充分:Per-token FFN 本身已经把参数按 Token 数做了倍增,如果再叠加不共享的 Expert,专家数量会进一步爆炸式增长,导致路由极度不均衡、专家训练不充分(”死亡专家”问题)。
为解决这两个问题,RankMixer 组合了两个训练策略:
① ReLU Routing(替代 Top-k + Softmax)
为了让不同 Token 能拥有灵活的、可变的专家激活数量,同时保持路由函数可微,RankMixer 用 ReLU 门控 + 自适应 $\ell_1$ 惩罚替代常见的 Top-k + Softmax 路由(该思路借鉴自 ReMoE)。
给定 Token $s_i \in \mathbb{R}^{d_h}$、第 $j$ 个专家 $e_{i,j}(\cdot)$、路由函数 $h(\cdot)$:
\[G_{i,j} = \mathrm{ReLU}\big(h(s_i)\big) \\ \qquad v_i = \sum_{j=1}^{N_e} G_{i,j}\, e_{i,j}(s_i)\]其中 $N_e$ 是每个 Token 可选的专家总数,$N_t$ 是 Token 总数。由于是 ReLU 而非 Top-k,门控值 $G_{i,j}$ 天然带有稀疏性(可以为 0),且对高信息量 Token 会自动激活更多专家。
稀疏度通过正则项 $L_{reg}$ 和系数 $\lambda$ 控制,使平均激活专家比例维持在预算附近:
\[L = L_{task} + \lambda L_{reg}, \qquad L_{reg} = \sum_{i=1}^{N_t} \sum_{j=1}^{N_e} G_{i,j}\]② DTSI-MoE:Dense-Training / Sparse-Inference
借鉴文献 [Pan et al., 2024] 的思路,RankMixer 使用两套路由器 $h_{train}$ 和 $h_{infer}$:
- 训练阶段,$h_{train}$ 和 $h_{infer}$ 都会参与前向和梯度更新(保证所有专家都能获得充分梯度,即 Dense-Training)
- 稀疏正则 $L_{reg}$ 只作用于 $h_{infer}$
- 推理阶段,只使用 $h_{infer}$ 做稀疏路由(Sparse-Inference)
这一组合的效果是:专家在训练阶段都能被充分训练,不会出现”死亡专家”,而在推理阶段又能享受稀疏激活带来的低成本。实验(论文 Figure 3)显示,即使把激活专家比例压缩到 1/8,DTSI + ReLU Routing 组合几乎不损失 1B 稠密模型的精度,同时推理吞吐提升 50%。
2.4 四个可扩展维度(Scaling 公式)
RankMixer 是一个高度并行、可扩展的架构,其参数量和计算量可以沿四个正交的方向扩展:
- Token 数量 $T$
- 模型宽度 $D$
- 层数 $L$
- 专家数量 $E$(Sparse-MoE 场景)
对于全稠密激活版本,单样本的参数量和前向 FLOPs 近似为:
\[\#\mathrm{Param} \approx 2kLTD^2, \qquad \mathrm{FLOPs} \approx 4kLTD^2\]其中 $k$ 是调整 FFN 隐藏维度的缩放比例。而在 Sparse-MoE 版本中,每个 Token 的实际参数量和计算量会进一步被稀疏度 $s = \frac{\text{#}\mathrm{\text{Activated_Param}}}{\text{#}\mathrm{\text{Total_Param}}}$ 缩放。
论文实验发现一个和 LLM Scaling Law 一致的结论:模型效果主要与总参数量相关,而深度 $L$、宽度 $D$、Token 数 $T$ 这几个不同的扩展方向带来的效果几乎相同;但从计算效率角度看,增大隐藏维度 $D$ 能产生更大的矩阵乘形状,从而比单纯堆层数获得更高的 MFU。最终线上采用的配置为:
- 100M 版本:$D=768,\ T=16,\ L=2$
- 1B 版本:$D=1536,\ T=32,\ L=2$
2.5 消融实验揭示的架构价值
论文对 RankMixer-100M 做了详细消融(Table 2、Table 3),进一步印证了每个模块设计的必要性:
| 去掉的组件 | AUC 变化 |
|---|---|
| 残差连接(skip connections) | −0.07% |
| Multi-head Token Mixing | −0.50%(影响最大) |
| LayerNorm | −0.05% |
| Per-token FFN → 共享 FFN | −0.31% |
以及 Token→FFN 路由策略对比:
| 路由策略 | ΔAUC | ΔParams | ΔFLOPs |
|---|---|---|---|
| All-Concat-MLP(拼接后过大MLP再切分) | −0.18% | 0% | 0% |
| All-Share(不切分,全部输入共享,类似MMoE) | −0.25% | 0% | 0% |
| Self-Attention 路由 | −0.03% | +16% | +71.8% |
| Multi-Head Token Mixing(RankMixer采用) | — | — | — |
可以看到,去掉 Token Mixing 影响最大——因为一旦缺失,每个 FFN 只能看到局部特征、丧失全局信息交互能力;而 Self-Attention 虽然效果与 Token Mixing 接近,但代价是显著更高的参数量和计算量,验证了论文”异构特征空间上做内积相似度收益有限、代价高昂”的核心论点。
3 工程实现
3.1 特征 Tokenization 模块
import torch
import torch.nn as nn
import torch.nn.functional as F
class FeatureTokenizer(nn.Module):
"""
将拼接后的异构特征 embedding 切分为 T 个固定维度的 token,
并投影到统一的模型宽度 D。
对应论文公式 (2): x_i = Proj(e_input[d*(i-1):d*i])
"""
def __init__(self, input_dim: int, num_tokens: int, model_dim: int):
super().__init__()
assert input_dim % num_tokens == 0, "特征分组需能被 T 整除,否则请先padding"
self.num_tokens = num_tokens
self.chunk_dim = input_dim // num_tokens # 对应论文中的 d
self.model_dim = model_dim
# 每个 token 有独立的投影矩阵(Proj 函数),
# 也可以选择所有 token 共享一个 Proj,这里采用逐 token 独立投影
self.proj = nn.ModuleList([
nn.Linear(self.chunk_dim, model_dim) for _ in range(num_tokens)
])
def forward(self, e_input: torch.Tensor) -> torch.Tensor:
"""
e_input: [B, input_dim] 已经按语义分组拼接好的特征向量
return: [B, T, D]
"""
chunks = e_input.split(self.chunk_dim, dim=-1) # T 个 [B, chunk_dim]
tokens = [proj(chunk) for proj, chunk in zip(self.proj, chunks)]
return torch.stack(tokens, dim=1) # [B, T, D]
3.2 Multi-Head Token Mixing(无参数特征交互)
这是全文实现中最关键、也最容易写错的部分。核心操作是:把每个 token 切成 H 份 head,然后跨 token 拼接同一 head 位置,形成新的 mixed token。用 reshape + transpose 即可高效实现,纯张量操作,不含任何可学习参数。
class MultiHeadTokenMixing(nn.Module):
"""
对应论文 3.3.1 节 & 公式 (3)(4)(5)。
输入 X: [B, T, D] -> 输出 S: [B, T, D] (因为设定 H = T,token数不变)
实现思路:
1. 每个 token 切分为 H 个 head,每个 head 维度 D // H
2. 按 head 维度重组:把所有 token 的第 h 个 head 拼在一起,形成新 token s^h
3. 因为设定 H = T,输出 token 数与输入一致,天然可以做残差连接
"""
def __init__(self, num_tokens: int, model_dim: int):
super().__init__()
self.T = num_tokens
self.D = model_dim
self.H = num_tokens # 论文设定 H = T
assert model_dim % self.H == 0, "D 必须能被 H 整除"
self.head_dim = model_dim // self.H # D // H
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
x: [B, T, D]
return: [B, T, D] (H个mixed token,每个维度 T * (D//H) = D,
因为这里 H=T 所以 mixed token 维度恰好还是 D)
"""
B, T, D = x.shape
assert T == self.T and D == self.D
# Step1: SplitHead -> [B, T, H, head_dim]
x = x.view(B, T, self.H, self.head_dim)
# Step2: 交换 T 和 H 维度,实现"跨 token 按 head 重组"
# 变换前 x[b, t, h, :] 表示第 t 个token的第 h 个head
# 变换后 s[b, h, t, :] 把同一个 h 下所有 t 的 head 排在一起
s = x.transpose(1, 2).contiguous() # [B, H, T, head_dim]
# Step3: 把 (T, head_dim) merge 成一个维度,得到每个 mixed token 的向量
# s^h = Concat(x_1^h, x_2^h, ..., x_T^h) -> 维度 T * head_dim
s = s.view(B, self.H, T * self.head_dim) # [B, H, T*head_dim]
# 因为 H = T 且 T*head_dim = T*(D//T),一般设计中会让 T*(D//T) = D
# 若 D 恰好能被 T 整除,则 mixed token 维度与输入 D 完全一致
return s # [B, T(=H), D]
实现小贴士:论文要求 $H=T$ 是为了让 Token Mixing 前后 Token 数量保持一致,从而可以直接做残差连接(
+ X_{n-1})。在实现里,只要保证D % T == 0,上面的view+transpose+view三步就能保证输出形状[B, T, D]与输入完全对齐,可以直接残差相加。这个操作本质上等价于对[B, T, H, head_dim]张量做一次transpose(1,2),是纯 tensor 重排,没有任何矩阵乘法,正是论文强调的”parameter-free”、GPU 上极其高效(几乎零计算开销,只有内存重排)。
3.3 Per-token FFN(Dense 版本)
class PerTokenFFN(nn.Module):
"""
对应论文 3.3.2 节 公式 (6)(7)(8)(9)。
每个 token 拥有独立、不共享的两层 MLP 参数。
用 batched matmul(一次性对所有 token 做不同的线性变换)实现,
以保持GPU上的大GEMM并行效率。
"""
def __init__(self, num_tokens: int, model_dim: int, k: int = 4):
super().__init__()
self.T = num_tokens
self.D = model_dim
self.hidden = k * model_dim
# 用 [T, D, kD] 的参数张量表示"每个 token 独立的第一层权重"
self.w1 = nn.Parameter(torch.empty(num_tokens, model_dim, self.hidden))
self.b1 = nn.Parameter(torch.zeros(num_tokens, self.hidden))
self.w2 = nn.Parameter(torch.empty(num_tokens, self.hidden, model_dim))
self.b2 = nn.Parameter(torch.zeros(num_tokens, model_dim))
nn.init.xavier_uniform_(self.w1)
nn.init.xavier_uniform_(self.w2)
def forward(self, s: torch.Tensor) -> torch.Tensor:
"""
s: [B, T, D]
return v: [B, T, D]
"""
# 第一层:对每个 token t,做 s_t @ W1_t + b1_t
# 用 einsum 一次性对所有 token 做 batched matmul,映射到工程上就是
# 融合成一个大 kernel("fusing parallel per-token FFNs into one kernel"),
# 这正是论文提升 MFU 的关键工程手段之一。
h = torch.einsum('btd,tdh->bth', s, self.w1) + self.b1 # [B, T, hidden]
h = F.gelu(h)
v = torch.einsum('bth,thd->btd', h, self.w2) + self.b2 # [B, T, D]
return v
3.4 Sparse-MoE 版 Per-token FFN:ReLU Routing + DTSI
class Expert(nn.Module):
"""单个专家:标准两层MLP"""
def __init__(self, model_dim: int, hidden_dim: int):
super().__init__()
self.fc1 = nn.Linear(model_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, model_dim)
def forward(self, x):
return self.fc2(F.gelu(self.fc1(x)))
class PerTokenSparseMoEFFN(nn.Module):
"""
对应论文 3.4 节:ReLU Routing + DTSI-MoE (Dense-Training / Sparse-Inference)。
关键点:
1. 每个 token 有自己独立的一组专家(Per-token + MoE 组合)
2. 路由用 ReLU(h(s_i)) 而不是 Top-k + Softmax,天然稀疏且可微
3. 训练时使用两套路由器 h_train / h_infer,
L_reg(稀疏正则)只作用于 h_infer,
但两套路由器在训练阶段都会更新(Dense-Training);
推理阶段只使用 h_infer 做真正的稀疏计算(Sparse-Inference)
"""
def __init__(self, num_tokens: int, model_dim: int, num_experts: int,
expert_hidden: int, reg_coef: float = 1e-3):
super().__init__()
self.T = num_tokens
self.D = model_dim
self.Ne = num_experts
self.reg_coef = reg_coef
# 每个 token 拥有自己独立的一组专家
self.experts = nn.ModuleList([
nn.ModuleList([Expert(model_dim, expert_hidden) for _ in range(num_experts)])
for _ in range(num_tokens)
])
# 两套路由器:训练用 / 推理用,每个 token 独立
self.router_train = nn.Parameter(torch.empty(num_tokens, model_dim, num_experts))
self.router_infer = nn.Parameter(torch.empty(num_tokens, model_dim, num_experts))
nn.init.xavier_uniform_(self.router_train)
nn.init.xavier_uniform_(self.router_infer)
self.last_reg_loss = 0.0 # 供外部读取加到 total loss 里
def _route(self, s: torch.Tensor, router: torch.Tensor) -> torch.Tensor:
"""
s: [B, T, D], router: [T, D, Ne]
return gate: [B, T, Ne] (ReLU 之后, 天然带0,实现稀疏)
对应公式(10): G_{i,j} = ReLU(h(s_i))
"""
logits = torch.einsum('btd,tde->bte', s, router)
return F.relu(logits)
def forward(self, s: torch.Tensor) -> torch.Tensor:
B, T, D = s.shape
if self.training:
# ---- Dense-Training:两套路由器都参与前向、都计算梯度 ----
gate_train = self._route(s, self.router_train) # [B,T,Ne]
gate_infer = self._route(s, self.router_infer) # [B,T,Ne]
# 稀疏正则只施加在 infer 路由器上,对应公式(11)
self.last_reg_loss = self.reg_coef * gate_infer.sum(dim=(1, 2)).mean()
out_train = self._compute_experts_output(s, gate_train)
out_infer = self._compute_experts_output(s, gate_infer)
# 两路输出都参与训练(简单相加/平均均可,具体融合方式工程上可调),
# 保证所有专家都获得充分梯度,避免"死亡专家"
return 0.5 * (out_train + out_infer)
else:
# ---- Sparse-Inference:只用 infer 路由器,且可结合门控值做真稀疏跳过计算 ----
gate_infer = self._route(s, self.router_infer)
self.last_reg_loss = 0.0
return self._compute_experts_output(s, gate_infer, sparse_exec=True)
def _compute_experts_output(self, s: torch.Tensor, gate: torch.Tensor,
sparse_exec: bool = False) -> torch.Tensor:
"""
对应公式(10): v_i = sum_j G_{i,j} * e_{i,j}(s_i)
sparse_exec=True 时,跳过 gate==0 的专家计算以节省真实推理算力
(工程上通常按 batch 聚合非零 mask 做 gather/scatter,
这里为了可读性用简单实现)
"""
B, T, D = s.shape
outputs = torch.zeros_like(s)
for t in range(T):
token_in = s[:, t, :] # [B, D]
token_gate = gate[:, t, :] # [B, Ne]
token_out = torch.zeros_like(token_in)
for e_idx, expert in enumerate(self.experts[t]):
g = token_gate[:, e_idx].unsqueeze(-1) # [B, 1]
if sparse_exec:
active_mask = (g.squeeze(-1) > 0)
if not active_mask.any():
continue # 该专家在这个 batch 里对所有样本都未激活,跳过计算
active_idx = active_mask.nonzero(as_tuple=True)[0]
expert_out = expert(token_in[active_idx])
token_out[active_idx] += g[active_idx] * expert_out
else:
token_out += g * expert(token_in)
outputs[:, t, :] = token_out
return outputs
说明:真实工业实现中,为了拿到论文所述”融合并行 kernel、大 GEMM 形状”的高 MFU 收益,逐 token / 逐专家 for 循环会被替换为分组批量矩阵乘(grouped GEMM)+ 稀疏 gather/scatter kernel(例如基于 CUTLASS 或 Triton 自定义 kernel),并结合 fp16/bf16 量化推理。上面的实现优先保证与论文公式的一一对应关系、便于理解,工程落地时的优化方向见下一节。
3.5 完整的 RankMixer Block 与堆叠
class RankMixerBlock(nn.Module):
"""
对应论文公式(1):
S_{n-1} = LN(TokenMixing(X_{n-1}) + X_{n-1})
X_n = LN(PFFN(S_{n-1}) + S_{n-1})
"""
def __init__(self, num_tokens: int, model_dim: int, k: int = 4,
use_moe: bool = False, num_experts: int = 4, expert_hidden: int = None):
super().__init__()
self.token_mixing = MultiHeadTokenMixing(num_tokens, model_dim)
self.ln1 = nn.LayerNorm(model_dim)
if use_moe:
expert_hidden = expert_hidden or k * model_dim
self.ffn = PerTokenSparseMoEFFN(num_tokens, model_dim, num_experts, expert_hidden)
else:
self.ffn = PerTokenFFN(num_tokens, model_dim, k)
self.ln2 = nn.LayerNorm(model_dim)
def forward(self, x: torch.Tensor) -> torch.Tensor:
s = self.ln1(self.token_mixing(x) + x)
out = self.ln2(self.ffn(s) + s)
return out
class RankMixer(nn.Module):
"""
完整的 RankMixer 模型:Tokenization -> L层Block -> mean pooling -> 多任务头
"""
def __init__(self, input_dim: int, num_tokens: int, model_dim: int,
num_layers: int, k: int = 4, use_moe: bool = False,
num_experts: int = 4, task_names=("finish", "skip", "like")):
super().__init__()
self.tokenizer = FeatureTokenizer(input_dim, num_tokens, model_dim)
self.blocks = nn.ModuleList([
RankMixerBlock(num_tokens, model_dim, k, use_moe, num_experts)
for _ in range(num_layers)
])
self.task_heads = nn.ModuleDict({
name: nn.Linear(model_dim, 1) for name in task_names
})
def forward(self, e_input: torch.Tensor) -> dict:
x = self.tokenizer(e_input) # [B, T, D]
for block in self.blocks:
x = block(x) # [B, T, D]
pooled = x.mean(dim=1) # mean pooling -> [B, D]
return {name: torch.sigmoid(head(pooled)).squeeze(-1)
for name, head in self.task_heads.items()}
# ------------------- 简单自测 -------------------
if __name__ == "__main__":
B, T, D, L = 8, 16, 768, 2 # 对应论文100M配置 D=768,T=16,L=2
input_dim = T * 64 # 假设每个token切分前维度d=64
model = RankMixer(input_dim=input_dim, num_tokens=T, model_dim=D,
num_layers=L, k=4, use_moe=False)
dummy_input = torch.randn(B, input_dim)
outputs = model(dummy_input)
for task, pred in outputs.items():
print(task, pred.shape) # 期望输出: torch.Size([8])
3.6 工程优化要点
论文给出了一个非常实用的时延分解公式:
\[\mathrm{Latency} = \frac{\#\mathrm{Param} \times \mathrm{FLOPs/Param\ ratio}}{\mathrm{MFU} \times \mathrm{Theoretical\ Hardware\ FLOPs}}\]字节在把参数从 16M 扩到 1.1B(约 70 倍)的过程中,时延几乎持平(14.5ms → 14.3ms),依赖三方面工程手段:
- 降低 FLOPs/Param 比值(3.6× 收益):架构设计本身(Per-token FFN 保持计算量不变、只增加参数)使得参数增长 70 倍,FLOPs 只增长约 20.7 倍。
- 提升 MFU(约 10× 收益):
- 使用大 GEMM 形状(更宽的 $D$)
- 良好的并行拓扑:把并行的多个 Per-token FFN 融合进同一个 kernel(对应上文
einsum批量实现思路的工业级落地——用 batched/grouped GEMM 一次性计算所有 token 的 FFN,而不是 Python for 循环) - 降低访存带宽开销与调度开销,让模型从 Memory-bound 转为 Compute-bound
- 量化(2× 收益):RankMixer 的主要计算是若干个大矩阵乘法,非常适合 fp16 半精度推理,直接将理论峰值算力提升一倍。
4 总结
RankMixer 的核心贡献可以概括为一句话:用”硬件对齐 + 数据特性对齐”这两条准则重新设计了推荐排序模型的特征交互架构,从而真正打通了 Scaling Law 在工业推荐系统中的落地路径。
具体来看:
- 架构层面:用无参数的 Multi-Head Token Mixing 替代平方复杂度、且在异构特征空间上语义不明确的 Self-Attention,用极低成本完成全局特征交互;用 Per-token FFN 实现”输入与参数同步按特征子空间隔离”,在不增加计算量的前提下大幅提升模型容量,天然避免了高频特征淹没长尾特征的问题;进一步用 ReLU Routing + DTSI(Dense-Training/Sparse-Inference) 的 Sparse-MoE 变体,在几乎不损失精度的情况下把参数容量再放大数倍。
- Scaling 层面:论文证明了 RankMixer 在 Token 数 $T$、模型宽度 $D$、层数 $L$、专家数 $E$ 四个维度上都具有良好且相近的扩展效果,效果主要取决于总参数量本身,这与 LLM 领域观察到的 Scaling Law 规律高度一致。
- 工程层面:通过降低 FLOPs/Param 比值、大幅提升 MFU(融合 kernel、大 GEMM 形状)、以及 fp16 量化,三管齐下抵消了参数量 70 倍增长带来的成本压力,做到了”参数涨、时延不涨”。
- 业务价值:RankMixer-1B 已经全量上线抖音 Feed 精排和电商广告精排等数十个字节内部场景,线上 A/B 测试证明其在推荐和广告两大核心场景中都具有良好的通用性,且低活用户群体的收益尤其显著(Active Days +1.74%),验证了大模型对稀疏样本用户的泛化增益。
从更宏观的视角看,RankMixer 代表了推荐系统排序模型的一种范式转变:从”堆砌人工设计的特征交叉算子”转向”设计硬件友好、可无限堆叠的统一架构”,这与 LLM 领域从”手工特征工程”走向”Transformer + Scaling Law”的历史进程颇为相似。可以预见,随着 Sparse-MoE 进一步成熟,RankMixer 有望从当前的 1B 参数规模继续扩展到未来 10B 级别的工业推荐场景。