先回顾一下LLM的算力革命。统一范式:Transformer 作为 Base,NLP 曾经有 RNN、CNN、各种 attention 结构,分散且难以规模化。Transformer 出现后,把核心计算转化为大规模矩阵乘法,极度匹配 GPU 的计算模式。Scaling Law 的发现,参数量、数据量、算力三者之间出现了 清晰可预期的扩展曲线:只要算力足够,性能就会稳定提升。这直接催生了一系列“堆大”就有效的 LLM。工程与硬件结合,混合精度(FP16、BF16)、最优宽高比、显存优化(ZeRO、分布式)、高吞吐 kernel(Flash Attention)等手段,把 GPU 算力几乎“吃满”。硬件迭代(A100→H100→B200)与模型计算强度共同推进,让 LLM 的算力利用效率接近物理极限。
这就是所谓的“算力革命”:一套范式(Transformer)+ 一条扩展路径(Scaling Law)+ 一整套工程优化(GPU原生化),让算法效果和硬件算力形成闭环,进入了快速迭代的正循环。
业界推荐系统大多数基线,没有类似 Transformer 的统一 GPU-Base,现有范式依然是 Embedding Lookup + 序列处理 + MLP(或其他交叉结构)的GPU小卡遗产,把它们搬到大GPU上,并不能天然吃满算力。堆算力,很多时候显存先满了,效果还没有随算力线性提升,觉得自己卡数不够,实际GPU利用率不足5%。工程设计上,访存瓶颈 > 计算瓶颈,Embedding 和稀疏特征导致 GPU 大部分时间在等内存,记忆性特征 > 泛化性结构:梯度资源被记忆性特征消耗,削弱了对深层模型的训练,再加上低效的样本范式使得系统存在IO瓶颈。业界推荐系统普遍还没有真正进入类似LLM的Scaling Path。
大家都在定义和探索下一代推荐系统,是生成式的,端到端的,还是要超长序列的,还需要打一个问号。但有一件事情是确定的,要围绕着算力引擎GPU重新设计系统架构和模型,提升计算强度(AI,Arithmetic Intensity),因为无论是哪一个方向,都是基于scaling law假设的,如果有了大卡GPU但利用率低下,scaling path被封住了上限,也无从谈起下一代推荐系统了。推荐的模型算法是战术,高性能GPU是枪炮,再强的刀剑战术,也抵不过枪炮的一击。
我们讨论模型结构和训练方法,开启Scaling Path之前,先要提升对GPU和算力的理解,这是一个尚未普及但未来必备的基础认知,否则可能只是知道Scaling Law下要堆算力,但是却全然不知算力怎么堆,背后有什么理论支撑,堆算力变成了堆卡数,只是空耗成本。文章从理解GPU,提升计算强度,扫清显存和IO瓶颈,到重构样本与特征的范式几个角度展开,全文分为五章。
1. GPU 利用率与算力核心 Tensor Core
本章概要:要真正释放 GPU 的算力,第一步不是模型有多复杂,而是对硬件有清晰的认知。GPU 的底层结构——SM、Warp、层次化存储——决定了它的并行效率;而 GPU 利用率指标(MFU、SMU、TCA、TCU)就是最直观的“健康检查表”,它们能告诉我们这台机器的算力到底有没有被吃满。很多推荐系统工程师觉得自己“卡数不够”,但其实 GPU 大部分时间都在等内存、在低效路径上运行,真正的利用率可能还不到 5%。GPU 的核心战力在 Tensor Core,可前提是上层计算能被编译成大规模矩阵乘法(GEMM),并且满足基本规范:矩阵维度要对齐、tile 要能被 warp 并行切分,否则就会掉到低效的 CUDA Core 路径。换句话说,算力革命的起点,并不是算法技巧,而是你是否理解了 GPU 的硬件规律,并且让模型计算对齐这些规律。只有把利用率拉上去,后续的 scaling path 才真正存在。
把推荐模型从小卡切到大卡,训练吞吐并不会和算力(FLOPs)成比例增长。甚至如果在小卡上就已经被访存卡住了,那么换到大卡后吞吐的提升往往微乎其微。问题的本质是 GPU 的利用率低下:算力摆在那里,但大部分时间并没有真正被用上。
可惜的是,GPU 利用率并不是一个单一的指标。我们平时常听到的几种口径包括:
- GPU Utilization:整体 GPU 是否在忙碌
- MFU (Model FLOP/s Utilization):模型实际 FLOPs 相比理论峰值的比例
- SM Activity (SMA,流式多处理器活动度):SM 是否在发射指令
- Tensor Core Utilization / Active:Tensor Core 单元的活跃度
这些指标看起来相似,但关注点并不一样,容易让人混淆。想真正理解 GPU 利用率,并提高GPU利用率,就需要结合 计算单元 + 存储架构 来看待。
1.1 计算与存储硬件

为了理解清楚这些指标,先简单理解下GPU的核心部件。Compute Engine 会把任务分解并分配到多个 SM(Streaming Multiprocessor)上执行;SM是 GPU 的实际执行单元集群,每个 SM 内部包含多个计算/存储单元(CUDA Core、Tensor Core、SFU、Load/Store Unit 等),SM 之间相互独立,可同时执行不同任务,比如H100 SXM5 有 132 个 SM;在每一个SM中,CUDA Core运行通用算术逻辑运算(FP32、INT32、部分 FP64),主要负责标量/矢量运算;Tensor Core专用矩阵乘加单元(MMA,Matrix Multiply-Accumulate),高吞吐执行深度学习常见的矩阵运算(BF16、FP16、TF32、INT8 等)。
GPU 的计算单元能否跑满,取决于存储系统能否及时提供数据。寄存器是线程私有的存储,访问速度最快,延迟极低,但容量有限,每个线程只能使用少量寄存器;一旦寄存器不足,数据就会被溢出到更慢的存储层次,直接拖累算力发挥。共享内存与 L1 缓存位于每个 SM 内部,由 Block 内线程协作使用,延迟远低于显存,常用于矩阵乘法的 Tiling 或数据复用,让同一份数据在多个线程间高效共享,显著减少对显存的访问压力。L2 缓存由所有 SM 共享,起到全局缓冲的作用,避免所有请求都直接落到显存。HBM 显存则是 GPU 的主存储,容量和带宽都很大,但延迟也很高,如果计算单元频繁直接访问显存,Tensor Core 和 CUDA Core 就会因为等待数据而无法持续工作,造成 GPU 利用率下降。
换句话说,计算单元(CUDA Core、Tensor Core)决定理论算力上限,而存储层次结构决定这个上限能否被真正触发。在实际应用中,推荐系统之所以常常 memory-bound,就是因为大规模的 Embedding Lookup 难以在寄存器、共享内存或 L2 缓存中复用数据,不得不反复访问显存,导致算力单元被“饿着”。
1.2 GEMM的逐层tile
下面详细讲讲,GPU里SM、Block、Warp & Thread、Tensor Core以及存储的关系,以及在一个GEMM(矩阵运算)中,是如何逐步拆解的。
GPU 硬件层次结构
- SM(Streaming Multiprocessor):相当于 GPU 的“核心”,一个 GPU 有几十到几百个 SM。每个 SM 内含有 寄存器文件、共享内存、调度器、Tensor Core/FP32 CUDA Core。GEMM 的计算任务会被切分后分发到多个 SM 并行执行。
- Warp & Thread:线程(Thread)是最小的执行单位。Warp是32 个线程的集合,是 GPU 的最小调度粒度。每个 Warp 同时执行同一条指令,在 GEMM 中,一个 Warp 负责计算某个 tile(小矩阵块)的部分乘加结果。
- Block:一个 Block拆分为多个 Warp,Block 运行在一个 SM 上(但一个 SM 可以同时跑多个 Block),Block 内部的 Warp 可以共享 Shared Memory,用于 GEMM 的 tile 缓存。Block tile多数语境下等价于计算库的Kernel tile。
- Tensor Core:SM 内部的矩阵运算单元,专门执行小矩阵乘法。半精度下执行16x16x16尺寸的小矩阵乘法。需要计算时,被Warp调用。

大规模 GEMM: ,GPU 运行时会分解成多级 tile:
- Grid / Block tile:整个输出矩阵 被划分为很多子矩阵,每个子矩阵就是 Block tile,与线程块(Block)一一对应。整个 Grid 就是所有 Block tile 的集合,每个 Block tile 通常是 或类似大小(内核决定),由一个 Block 独立完成计算。
- Warp tile:一个 Block 内包含多个 Warp,每个 Warp 会负责 Block tile 中的一部分。例如一个 的 Block tile,可能被 4~8 个 Warp 拆分成若干 或 的 Warp tile。
- Tensor Core tile:Warp 内部通过调用 Tensor Core 指令完成更小的矩阵乘。常见的是 (FP16/FP32 混合精度),在 Hopper 上可以扩展到更大的 tile。
- Thread tile:Warp 内每个线程持有寄存器片段,只负责少量元素的累加,例如 或者几个标量。最后这些结果合并,形成 Warp tile 的完整输出。
GEMM 的性能瓶颈往往不是 FLOPs,而是数据搬运。对应关系如下:
- Global Memory -> Shared Memory -> Register
- Block 级 tile:从 global memory 读进 shared memory
- Warp 级 tile:从 shared memory 读进寄存器
- Thread 级 tile:寄存器里参与 Tensor Core 运算
- Tensor Core 运算:寄存器 fragment -> 调用 MMA 指令 (Matrix Multiply Accumulate)
1.3 纷乱的利用率指标
回到几种GPU的利用率,最常见的也是最具有误导性的是GPU Utilization,它的计算方式是采样周期内,至少有一个SM正在执行指令的时间占比。即便GPU的利用率显示100%,也未必是计算引擎被占满,GPU包含多个引擎,这个使用率是一个周期里任意引擎有使用的比例,可以做到GPU利用率80%+,但计算引擎空闲,这是在做计算优化时应该最先忽略掉的指标。这是性能看板上最容易见到的,经常说自己GPU利用率60%+的,多半是看了这个指标。
GPU的计算引擎包含了多个SM(Streaming Multiprocessor),每个SM配备了一组计算核心,包括CUDA core和Tensor Core,SM Active(SMA)代表至少一个线程发射指令的周期比例,它更贴近了算力比率,但依旧存在欺骗性,SM内是多线程的,只要有一个线程发射指令,这个指令甚至是Load/Store都计算在内了。有可能SM的使用率达到100%,它的算力可能发挥了10%都不到,比如只是用到了CUDA core,而Tensor Core闲置。
Tensor Core是专门用于矩阵乘加的核心,是高性能GPU的核心算力。 以H100(PCIe Gen-5 80 GB)为例,它有114个SM(Streaming Multiprocessor),每一个SM包含128个CUDA core和4个Tensor Core,不要小看这4个TC,它是专门设计出来处理矩阵乘加运算的,H100上CUDA的理论算力是51.22 TFLOPS,而TC在TF32下是378 TFLOPS,半精度(BF16、FP16)下是756 TFLOPS(稠密值,开启2:4结构化稀疏时翻倍),Tensor Core占据了绝大部分算力。
Tensor Core Utilization是Tensor Core 在采样周期内工作时间占总时间的比例,局限是可能 Tensor Core 在跑低吞吐的小矩阵,时间上是活跃的,但实际性能没拉满。Tensor Core Active是Tensor Core 执行指令时实际活跃的运算单元比例,反映 Tensor Core 吞吐潜力是否被用满。它们分别可以直观地理解成时间和空间上的利用率。会出现TCU和TCA一高一低的情况,TCU 高 / TCA 低 :算子时间长,但批次小或矩阵分块不合理,单次执行浪费资源;TCU 低 / TCA 高: 算子本身满载,但频率低(可能被其他非 Tensor Core 算子占据时间)。实际上,TCA是系统的一个瞬时值,但是可以按时间采样和累计,得到一个时间加权的TCA,一般系统报表里的TCA特指这个,实际是TCU*TCA,我们叫总体TCA吧。
总体TCA(时间累计的TCA)也有局限。TCA是当前时刻活跃的 Tensor Core 单元数 / 总 Tensor Core 单元数,只要给每个 Tensor Core 分配了一个 warp 去执行 MMA 指令,它就被记作“活跃”,它不关心每个 Tensor Core 这一拍算了多少 FLOPs,有没有因为数据不够导致一部分 cycle 空转,pipeline 有没有完全填满。
MFU(Model FLOP/s Utilization)是一个理论上的完美指标,它用实际的浮点算力除以硬件理论最大算力,不同的系统在相同的GPU上,都可以通过MFU来比较总体利用率。唯一的问题是,它并不能直接定位瓶颈或指导围绕硬件的模型架构设计,要结合SMA,TCA(瞬时)和TCU来看。另外,它一般来自于框架和工具计算,系统的报表里想直接显示MFU,是要做系统改造的。
Tensor Core虽然占据了90%以上的算力,但会存在总体TCA高,但MFU低的情况。MFU≈TCU×TCA×η=总体TCA×η,η在0到1之间,受到算术强度不足或 pipeline 空洞影响。想象一个工厂:TCA 看的是“多少台机器有人上岗”,η 看的是“每台机器上岗期间有多少时间在加工 or 发呆”,如果原料供应不足(AI 低)或流水线卡顿(pipeline 空洞),机器是“有人站在那”(TCA 高),但加工产出很少(η 低)。
总结下,要想提升GPU的利用率,可以用MFU唯一衡量,由于Tensor Core是核心算力部件,要能够满足开启Tensor Core的计算条件,提高总体TCA,从而拉高MFU。
1.4 Tensor Core的精度要求
开启Tensor Core计算的必要条件是使用它所要求的数据精度类型,值得注意的是,很多推荐系统采用了TensorFlow 1.x,它并不支持TF32,所以如果没有做混合精度改造,矩阵运算基于的是FP32精度类型,所有的计算会由cuda core来完成,TCA是0。
速查表(按输入类型分类):
架构 | FP64 | FP32 | TF32 | BF16 | FP16 | FP8 | INT8 | INT4 | INT1 |
Volta | ✗ | ✗ | ✗ | ✗ | ✔ | ✗ | ✗ | ✗ | ✗ |
Turing | ✗ | ✗ | ✗ | ✗ | ✔ | ✗ | ✔ | ✔ | ✗ |
Ampere | ✔ | ✗ | ✔ | ✔ | ✔ | ✗ | ✔ | ✔ | ✔ |
Hopper | ✔ | ✗ | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ |
算力可以用每秒钟计算多少浮点数来定义,TFLOPS(Tera FLOPS)是衡量计算设备浮点运算能力的单位,全称是“万亿次浮点运算每秒”。以H100为例,处理FP32的算力是67 TFLOPS,TF32的算力是495 TFLOPS,BF16/FP16则为989 TFLOPS(稠密值,开启2:4结构化稀疏时翻倍)。这是因为Tensor Core为了半精度而设计的,模型的数据类型适配了TC要求,才能开启GPU核心算力。看看H100在8192大小的矩阵运算上,不同精度下的计算耗时。所以基于TensorFlow 1.x的推荐系统,要么升级到2.x,使用TF32;要么做混合精度改造,使用FP16或BF16.
不同数值类型的性能对比:
数值类型 | 理论峰值 (TFLOPS) | 理论耗时 | 70% 峰值耗时 |
FP32(CUDA 核) | ~67 | 16.41 ms | 23.44 ms |
TF32(Tensor Core) | ~495 | 2.22 ms | 3.18 ms |
FP16(Tensor Core) | ~989 | 1.11 ms | 1.59 ms |
BF16(Tensor Core) | ~989 | 1.11 ms | 1.59 ms |
(参考) FP16/BF16 非 TC | ~120 | 9.16 ms | 13.09 ms |
这里面关键问题是采用半精度进行前向和反向的计算,才能发挥出硬件的核心算力。混合精度计算,要考虑数值稳定性,需要处理更多细节。由于还能够降低显存,在显存优化里详细介绍。
1.5 矩阵维度的内核要求
GEMM(General Matrix Multiply)是最常见的矩阵乘法形式: , 的维度是 , 的维度是 。以下提到 就来源于这里。
先从一个矩阵计算的耗时实验开始,我们分别选取
K=4096 和 K=4095,预热后执行多轮的矩阵运算并计时。结果如下:GPU | FP32 | TF32 | FP16 | BF16 |
A100 (4096) | 72.219 ms | 11.374 ms | 5.666 ms | 5.523 ms |
A100 (4095) | 72.408 ms (+0.2%) | 18.669 ms (+64%) | 16.462 ms (+190%) | 16.457 ms (+197%) |
按照CPU上的思维,计算的FLOPS变小了,理论上耗时不应该有太多的变化。这里耗时反而增加了2倍,我们探究下其中的原因。
Tensor Core 处理矩阵运算有一个最小计算单元(MMA tile size),矩阵在最终硬件上一定是被切分成tile来计算的,对于半精度来讲,这个size是 。也就是矩阵的维度都要被 16 整除才行,否则会在 warp 内做 padding,浪费的是Tensor Core 指令级别的算力。
上层的计算内核会执行整个矩阵的分块调度策略(thread block tile),一般设置为 或 (不同版本有差异),这里是指 的维度,当不满足时,要进行padding,虽然计算变化不大,但触发了整个线程块级别的低效执行。计算库之所以有这样的分块,是因为访存的维度可能是256B,刚好对应128 的FP16/BF16,无法对齐的话,访存事务需要额外判断和掩码(mask)。
下面的图展示了,按照 分块,随着 的调整,每到超出 128 的倍数时,耗时直接翻倍,背后原因是tile数翻倍了。

大的矩阵运算,要拆分成 thread block tile,送到所有 SM 并行计算。以 A100 为例子,它一共有 108 个 SM(Streaming Multiprocessor),假设恰好有一个分块需要额外一轮 SM 的并行计算,虽然计算量只多了一点,但恰好触发了 108 个中的 1 个 SM 计算,其余 107 个都在闲置等待完成一轮计算的时间。
下图的例子, ,分块采用 ( ), 方向分了 个 tile,108 个 tile 是一轮完整的 SM 计算, 方向的 tile 数为 12(
108/9),对应的 的维度是 ,所以下图中, 每突破 1536 的倍数,虽然 tile 数只多了一点,但执行时间却翻倍了。
综上3个特性,任何一个矩阵的维度,都应该是16的倍数;GEMM的M和N应该是128或256的倍数,保守起见建议都是256的倍数,且MxN拆分tile后,应该是SM的倍数。
举一个实际的例子,如果我们用Transformer处理用户行为序列。batch size=128,序列长度100,dim=64,head 数为 4;上层用Transformer做特征融合,特征域27个,dim=64,head数为4.
- 序列的 ,令
- 因为不满128,单个 tile 内有大量“无效边缘”,tile 量化浪费是 ,而且它跟 B 和 H是没有关系的,是 的矩阵运算执行了 次,所以序列一定要设置为128(保守点选256)的倍数。
- K 满足 16 的倍数时,不存在 warp 内做 padding 带来的浪费,这里常见的误解是 也需要128的倍数。但虽然对齐了最低硬件标准,但容易造成 Tensor Core 无法吃满—— 维太浅导致装载/收尾与访存开销比例比计算高,GPU 常在等待数据。 一般设置为 64或128,满足硬件要求又能很好打满 TC,且单 head 的表达能力充足。
- 在序列的 QKV 中, , ,tile 分块采用 ,一次计算的 tile 数是 ,它不是 108(SM 数)的倍数,存在尾波浪费。
上面可能太复杂了,简化版本的实操清单:
- GEMM 的 ,SM 数是
108, tile 下,M/N/K都必须要是 16 的倍数,K一般选 64 及以上,M 和 N 直接 128 的倍数,M 和 N 越小,越要保障 tile 数是 SM 的倍数( )。
- 具体的在 Transformer 上:
- 序列的 要是 128 的倍数,如果(特征) 无法设置足够大,要是 16 的倍数。
- 是 64 或 128, 是 128的 倍数,
- Batch Size 只影响 wave 浪费,矩阵大忽略,矩阵小精算下,可以设置为 SM 的倍数。
上面,从硬件(TC)与内核的角度,了解了如何开启 Tensor Core 计算,以及高效完成计算的基本矩阵维度设置。做到这些,只是及格,下面介绍如何通过提升计算强度,让算力飞升。
2. 提升计算强度:踏上 Scaling Path
本章概要:推荐系统算力不足,核心原因往往不是显卡不够,而是模型的算术强度(AI, Arithmetic Intensity)太低。用 Roofline 模型来观察,大多数推荐系统的训练过程都落在 memory-bound 区域:显卡在等内存,算力闲置,哪怕加再多卡,扩展效率依旧不理想。要进入真正的算力扩展路径,必须提升 AI。路径有几条:结构上,合理选择“加宽”与“加深”的比例,避免一味堆层数却拉不高算术强度;在序列上,通过拉长序列长度,让每次矩阵运算更大、更密集,从而获得近似线性的 AI 提升;工程上,利用算子 Fusion 减少 kernel 调度和访存开销,让计算更连续。甚至在 embedding 设计上,也可以通过融合方式减少低效的内存访问,把资源留给真正的矩阵计算。换句话说,Roofline 告诉我们一个残酷的事实:如果你的模型还停留在 memory-bound 区域,就不可能进入 scaling law 的正循环。提升 AI,是推荐系统踏入算力革命的关键一步。
算力提升,除了硬件层面的利用率,一个更好的理论框架是Roofline Model,它定义的AI(Arithmetic Intensity,计算强度)可以很好的衔接GPU的利用率与模型结构的设计与算子优化。LLM的发展过程中,提出了scaling law,它描述了算力和效果power law关系,通过提升算力就能稳定地提升效果,这不是说堆砌更多的GPU就好了,LLM(GPT)也经历了模型AI(计算强度)和GPU的AI之间的拉锯,最终抵达compute-bound。所以,讲一句AI(Arithmetic Intensity) powers AI(Artificial Intelligence)并不为过。
推荐系统的算法一直都在借鉴NLP的成果,源于用户行为序列和文本token序列的定义相似。而此刻传统的推荐系统,普遍的AI值在10以下,处于GPU的memory-bound最左端,堆砌硬件只会浪费成本。AI(计算强度)决定了算力是否能够被充分激发——当模型算子仍然停留在 memory-bound 区间时,GPU 的浮点单元大多处于闲置,智能的潜力无法兑现;而当通过更宽隐层、更长序列、更大批次、算子融合与访存优化逐步推高 AI,使其跨越 roofline 拐点进入 compute-bound,推荐算法才真正释放出硬件的全部计算能力。
2.1 Roofline model
AI、compute-bound、memory-bound这些词云里雾里,为了更好地理解,要讲一讲它们背后的理论Roofline Model。
Roofline 模型是可视化地表示计算能力与存储带宽之间关系的方法,用来判断一个 workload 是受限于计算(compute-bound)还是受限于存储(memory-bound)。核心公式:
Peak FLOPs与GPU相关,比如H100(SXM5)在FP16非sparse上的Peak FLOPs是989TFLOPs,模型AI作为横轴,Performance作为纵轴,画一个图,就是Roofline了(如下图)。当模型AI很低时,性能受限于带宽,memory-bound。当 AI 足够高时,性能达到峰值 FLOPs,compute-bound。转折点 ,叫 Roofline 拐点。H100的Memory Bandwidth是3.35 TB/s,算力拐点横坐标就是 。

Roofline的意义是清晰地展示了提升算力就是提升AI值,想发挥硬件极限,就必须把算子所在点从左侧(memory-bound)推到右侧(compute-bound)。而把模型看作“一堆 GEMM 的堆叠”,就能把问题还原为:如何让每个 GEMM 的 AI(Arithmetic Intensity)变大。只要我们知道如何分析GEMM的AI,就可以找出瓶颈并合理地设计模型和设置维度。
GEMM其算术强度 (AI) 可表示为: ,GEMM为矩阵 的表示,后续我们会常常提到矩阵乘法的M维、K维和N维,都出自这里的定义。
提升AI的思路,一个是让模型的设计使用大矩阵乘,矩阵够胖(方),M和N尺寸相当,K足够大,GEMM的AI分子是次方级变化,分母是平方级,AI才能线性增长。当仅放大某一维,AI 不再无界线性增长,而收敛到与其他未放大维度相关的常数。另一个思路就是减少AI的分母,减少内存访问,例如减少显存往返(Fusion)、提高缓存命中率。
在实际分析中,AI 的计算可分为静态 AI 和动态 AI。它们分子相同,静态AI的分母按每个算子必需的输入和输出数据量累加,假设这些中间数据完全驻留在寄存器或共享内存中并被完美复用,相当于假设算子融合充分、缓存命中率 100%、无额外数据搬运,因此是理想化的理论上限。动态 AI 则基于运行时性能计数器的实测值。分母是运行时真实的全局内存访问字节数,其中包含了中间结果写回显存、缓存未命中、padding 对齐浪费等实际执行过程中的额外访存。
从静态和动态AI角度提升AI:在模型设计层面,通过调整网络结构和矩阵维度来提高静态 AI 的理论值;在实现和硬件适配层面,通过算子融合、内核优化、访存模式优化等手段减少动态运行时的额外访存,从而让动态 AI 尽可能逼近静态 AI。
AI powers AI,Roofline Model除了告诉我们堆算力的具体方案,它还有另一种理解,不同网络结构的AI不同,更高AI的结构,意味着相同的访存信息下计算的连接和交互更密集,更能产生智能。如Scaling Law研究中,无论是参数还是token数目增长,高AI的Transformer比低AI的LSTM有更好的Scaling Law曲线。

如果推荐系统也信仰Scaling Law,提升算力就具体化为提升AI。结构选择:用更高 AI 的结构(Transformer),维度配置:让关键 GEMM 处在靠右(compute-bound)的区间,实现优化:用算子融合把动态 AI拉近静态 AI。希望推荐系统能够逐步提升AI,突破Memory-Bound,以AI(Arithmetic Intensity)撬动推荐AI(Artificial Intelligence)。
2.2 GEMM的AI分析
回顾下,GEMM的算术强度 (AI) 可表示为:
有一个形象的说法,要提升GEMM的AI,矩阵要足够的胖(方)。
从公式可以看到:分子 是三维的乘积,增长是立方级别的;分母 是两两维度的乘积,增长是平方级别的。因此,当 同时增大时,AI 会呈现近似线性增长。
更胖的矩阵,Roofline角度有更高的AI,在底层核心和硬件上对应什么?
Roofline角度:矩阵要足够胖(M和N维度大且量级相同),AI的分子比分母涨的快,AI线性增长;单一维度增长,AI存在常数极限。
- 各维度同比涨:
- 仅一维度增长:
内核和硬件角度:
- M和N维度能切出足够多的Block tile数,足够填满Tensor Core的SM,使得计算充分并行
- M和N接近相等,能保证一次tile访存,在对向维度的计算重用足够多,Warp tile 在 M/N 两向都能找得到“邻居任务”,提高L1/L2缓存命中,避免出现“只在一边拉长”的稀疏占用。
- K足够大,让硬件层面能切出足够多的16x16x16的MMA(Tensor Core)micro-tile,K-loop 深,同一批A/B子块在 SM的片内的高速Shared Memory中被多次复用(C ← A×B + C),把一次全局加载“榨干”。
2.3 加宽加深与拉长
以Transformer结构为例,观察关键的GEMM的AI,来确定合理的维度设置。
加宽与加深对 AI 的影响
在网络结构中:
- 加宽(增加矩阵维度):能提高 FLOPS 的同时,也能显著提升 AI,因为每次矩阵运算的算术强度变大。
- 加深(堆叠更多层数):虽然总 FLOPS 增加,但每一层的 AI 值并未改变。多次堆叠的总体 AI 仍接近单层的 AI。
这带来一个 反直觉结论:加宽可以同时提升 FLOPS 和 AI,而加深只能提升 FLOPS,却无法提升 AI。在 memory-bound 场景里,优先“加宽”比“加深”更能把硬件吃满。
在推荐系统中,常见的设置往往采用比较小的矩阵,算术强度 (AI) 偏低,容易处于 memory-bound 状态。在这种情况下,应优先考虑 加宽,即增大矩阵的一个或多个维度,而不是单纯加深网络结构,否则算力将存在峰值无法吃满硬件的浪费,无论架构上做多少优化都无法突破。
LLM加宽历史(先把单层 GEMM 做“又大又方”)
- GPT-2 → GPT-3:显著增大 d_model、head_dim、heads,context 从 1k → 2k;单层矩阵更大、更“方”,GEMM 的 AI 明显上去,训练更趋 compute-bound。
- PaLM/PaLM-2 系列:大 d_model + 大 FFN 扩展比(SwiGLU),再配合超大批次与更长序列,典型的“先宽后深”配方。
- LLaMA → LLaMA-2/3(8B/70B 等):保持合理深度的同时,把 d_model、FFN(门控激活)、上下文长度做大做齐(对齐 64/128 粒度),侧重单层吞吐与 AI。
- 长上下文路线(GPT-4 时代、Claude、Longformer/FlashAttention 系):把 L 做大,提高 这条边的“胖度”,结合 FlashAttention 降访存,动态 AI明显拉高。
矩阵宽度和AI的关系
我们以Transformer Base的模型为例子,来理解如何设置参数以提升AI。选取几个大算力的部分,包括QKV映射和FFN,以及QK^T计算。
和 分析:
令 (QKV 有 ,FFN 一次 GEMM 常取 ):
- QKV GEMM( ),
- 若 : (对 饱和,随 线性增)。
- 若 : (对 饱和,随 线性增)。
- FFN 单次 GEMM( )
判据(以 H100 FP16 非稀疏拐点 为例):
- QKV 在 时需 。
- FFN 在 时需 。
综合看,就是当d不够大时,推荐系统常常设置比较小的维度(如128/256),无论怎么提高Batch或Seq Length,AI都达不到300。选一个512以上的隐藏层维度,才能在H100上有机会达到compute-bound。
分析:
设定与符号
- 批量 B、头数 H、序列长 L、每头维度 ,且
- 精度字节数 (FP16/BF16 时 )
对 每个 (batch, head):
- , (计算 )
- 等价 GEMM:
Attention这一步,并不是一个大GEMM,而是BH个并行的GEMM,它的AI:
性质分析
- 与 无关:AI 只依赖于 和 。
- 当 :分母中 主导, ,算术强度随 线性增长。
- 当 :分母中 主导, ,算术强度随序列长度 线性增长。
一般LLM的 选择64或128就足够了,它线性增长的空间很小。当 不断提升,AI极限是一个与 无关的常数。这里有一个平方级别的算力增加,却无法带来AI的提升,原因是访存也存在一个 的平方项。这意味着,推荐系统使用Transformer的架构,无论是特征数量还是用户行为序列,随着长度拉长的scaling law无法进行下去。
这里一个非常关键的优化就是提出了Flash Attention算法,使得 计算的算术强度 (AI) 分母里的 项被消除了。它的关键在于利用 Softmax 的平移不变性。将 按块加载到 GPU 片上 SRAM,在片上完成 、Softmax、乘 等完整计算,避免把中间 注意力矩阵写回显存(HBM),显存访问从 降到 。这样不仅大幅减少带宽消耗并加速计算,而且算出的结果与标准 Attention 完全一致。算法的原理放在后面讲显存优化里详细展开,这里先按下不表。
优化后的AI公式:
去掉了AI分母平方项的 ,这使得模型在拉长序列的同时,能线性地提升AI。
在模型设计中,加宽(增加 hidden size、embedding 维度、FFN 中间层宽度等)通常可以提升算术强度(Arithmetic Intensity, AI),更好地利用 GPU 的并行计算能力和带宽;加深(增加 Transformer 层数)则更直接地提升总计算量与模型的表达深度。在大语言模型(LLM)领域,已有研究基于 scaling law 和 compute-optimal 分析,给出了参数规模、宽度与深度之间的近似最优比例,这类比例既能充分利用算力,也能在训练数据量充足的前提下获得最佳性能。
对于推荐系统而言,尤其是在基于Transformer架构改造时,同样面临宽度与深度如何分配的问题:
- 宽度过小会导致 AI 偏低,算力利用不足;
- 深度不足会限制建模能力,尤其是在复杂用户行为建模和多模态融合中;
- 宽度和深度的最佳组合很可能依赖于特征数量、embedding 尺寸、序列长度以及多任务目标的权重分配。
因此,推荐系统也需要像 LLM 一样,系统性探索不同宽高比下的性能与效率边界,找到针对自身场景的最优宽高比,从而在既定的算力预算和数据条件下实现最大化的效果与效率平衡。
2.4 开大Batch Size的理性分析
一般推荐系统选择开大Batch Size以提升吞吐量,这来源于经验,但却是一种感性的理解。我们带着两个问题,然后对Batch Size做一个基于Roofline理论的分析,看清楚它和吞吐的关系:
- 开大Batch Size,总是能带来吞吐提升么,它是否是推荐系统通常设置下的特例
- LLM普遍采用Gradient Accumulation机制,会采用micro-batch累计梯度来降低显存占用,那这会显著影响吞吐么
我们以Transformer的QKV映射为例,分析对应的GEMM的AI, , , ,半精度 ,它的静态 ,在H100上半精度AI值达到 则进入compute-bound。
Batch Size 在 QKV 映射的 GEMM 里参与到了 M 维,矩阵的维度提升是能带来
AI 增长的。在 memory-bound 区域,M 维的提升能带来 AI 提升,只要显存还放得下,单位访存换得的计算量在提升,进而提升吞吐量。推荐系统常常设置较低的 ,以及较低的 L(短序列100,特征序列 64),比如 。当 时,GEMM 的算术强度为 ,小于 H100 的 Roofline 拐点 。也就是说,在矩阵的 K 和 N 维度较小时,哪怕无限增大 M,在 H100 上也处于 memory-bound。在这个区域,随着 M 提升,AI 提升,吞吐提升,也就是我们看到的增大 Batch 就能提升吞吐。但吞吐也有一个极限,我们计算极限 AI 的 95%和99%,代入反算 M 分别是 1824 和 9504,对应着 Batch Size 为 ,即 18 和 95。也就是说,虽然加 Batch 可以提 AI,但收益边际递减:Batch 从 18 加到 95,理论吞吐只涨了极限下的 4%,而且很有可能先被显存大小卡死了。换一组参数, ,M增大,AI的极限为768,成功进入H100的compute-bound,把AI=300代入到AI公式中,可以解出来M=492时GEMM的AI突破memory-bound,也就是M>492之后,吞吐不再增加,此时Batch Size=5之后继续增大无法提升AI,吞吐无法增加。为什么进入compute-bound,吞吐无法再增加?按照Roofline的那个图,compute-bound意味着GPU算力发挥到了极致,这种情况下只能增加更多GPU了。
当然,这是通过计算静态AI拿到的理论值,实际动态AI会更低,Batch Size需要更大的值才能到达极限;并且只是其中一个GEMM,实际情况需要多个GEMM的综合影响。
M 只是在把你推向该形状(N、K确定)的 AI极限:
- 当隐藏层维度较小:AI极限低 ,无论怎么加 M 都进不了 compute-bound,但在 memory-bound 区域 AI 与吞吐随 M 增长且快速递减趋于饱和。
- 当隐藏层维度足够大:存在一个有限的 M 门槛, 超过后几乎不再涨吞吐。
回到开始两个问题:
传统推荐系统由于设置了较小的隐藏层维度,会导致Batch Size增加,AI增加,吞吐增加的情况。本质是矩阵不够胖,无法随着M(Batch Size * L)提升快速进入compute-bound。再换言之,这是因为推荐系统处于memory-bound下一种特例,并不是普遍的规律。
LLM采用Gradient Accumulation,因为 足够大,micro-batch很小就能够达到吞吐极限,拆分micro-batch不会让吞吐下降。
2.5 Fusion算子减少访存与kernel launch成本
我们提到了静态AI和动态AI,Fusion算子就是通过多个算子融合,中间结果直接保存在寄存器/共享内存,不需要反复写回再读出,实际访存量下降。另外,减少 kernel 启动 & 调度开销:避免频繁切换内核造成的 pipeline 泄露。
Fusion(算子融合)是将多个逻辑上连续、数据依赖简单的算子合并成一个自定义内核(kernel),一次性完成多个计算步骤。中间结果不落显存:直接保存在寄存器或共享内存中,减少全局显存 I/O。一次访存,多次计算:将多个运算融合到一次内存加载周期内,提高 AI。
编译器级别的融合,依赖框架或编译器的图优化(Graph Optimization)Pass,自动分析算子拓扑,把访存开销大、数据依赖简单的连续算子融合成一个内核。比如,XLA(TensorFlow / JAX),TorchInductor(PyTorch 2.x)和TVM / TensorRT Graph Optimizer。无需改动算子 API,优化透明,能处理好典型的case,如Linear + BiasAdd + GELU、LayerNorm + Dropout + Residual Add的融合,可控性弱,不一定能融合关键瓶颈算子。
显式融合算子,直接调用或实现专门的融合算子 API 或自定义 CUDA kernel,如上面提到的Flash Attention,可以一次性计算QK^T + Softmax + Dropout + MatMul。可针对硬件(H100)深度优化,一般和 CUDA/ Triton kernel 写法绑定。性能可控,效果显著,但维护成本高,硬件/框架适配麻烦,采用LLM生态的通用开源算子,可以减小成本。
写法/调用级触发融合,通过特定代码结构和 API 调用方式,让编译器更容易识别和触发融合。如 y = gelu(x @ w + b) 保持在一行,TorchInductor 会融合 MatMul + BiasAdd + GELU;用 jax.numpy / tf.function 包裹运算触发 XLA fusion。在不写kernel的同时,尽可能帮助编译器去触发算子融合,不好的写法,会导致编译器难以识别。
用一个直观的例子,展示不同的计算流程导致的访存浪费。QKV映射环节,输入和W_Q/W_K/W_V进行3次GEMM,得到Q、K、V,如果把中间的权重合并成一个大的W_QKV,维度是[d_model, 3*d_model],一次计算后再拆分为Q、K、V,为了保证split那一步不做QKV大矩阵的显存写入,一般也是合并成了一个自定义kernel。这两种写法对比如下:
实现方式 | X 读取次数 | Kernel 启动次数 | 访存量(相对值) | 动态 AI 相对值 |
三次 GEMM | 3 | 3 | 3.0x (重复加载 X、重复写 Q/K/V) | 1.0x |
QKV Fusion | 1 | 1 | ~1.2–1.3x (一次加载 X,权重拼接后一次加载,输出一次性写好) | ~2.5–3.0x |
GPU 执行每个算子(kernel)前,都需要:Host → Device 的启动指令传输,硬件调度单元(GPC/SM scheduler)分配线程块、寄存器、共享内存,pipeline 填充(warp dispatch 到各个 functional unit)。这些步骤的延迟通常在 几到几十微秒,而且会阻塞后续 kernel 的执行。对于小计算量算子(例如激活函数、归一化),启动和调度延迟甚至可能占总执行时间的 30%~50%。
“Pipeline 泄露”现象。GPU 是靠深流水线来隐藏访存延迟的,如果 kernel 很短(计算少),pipeline 还没完全装满就结束了。连续多个短 kernel 会导致 pipeline 一直处于“加水—放水—加水—放水”的状态,吞吐率明显下降。这就是所谓的 pipeline leakage(流水线泄露)。
减少 kernel 数量:把多个小算子合成一个大 kernel,让 pipeline 装满一次,持续工作更久。减少调度切换:调度器不用频繁上下文切换,线程块一次分配、一次寄存器装载,多轮计算。延迟摊薄:原本 N 个 kernel 的启动延迟,现在摊在一个大 kernel 上,相当于延迟成本降低 N 倍。
AI 本身是计算量 / 访存量,但kernel 启动和调度开销虽然不直接算进访存量,却会拖低有效吞吐率。Fusion 把多个 memory-bound 小 kernel 合成一个算子后,单次访存对应更多计算量,动态 AI 提升,也更接近静态 AI。
已经提到了Kernel执行,就顺便说一下CUDA Graphs特性。它与Kernel Fusion不同,CPU 调度 GPU kernel 的启动开销太高,尤其是小 kernel 多、调用频繁的场景,每次启动都有CPU→GPU 往返延迟(launch latency),即使 GPU 算得很快,CPU 发指令的那几微秒到几十微秒也会累积成瓶颈。CUDA Graphs一次性把Kernel的调用Graph交给GPU,让 GPU 按图里的顺序批量执行所有 kernel。这样 CPU 启动的次数从 N 次 → 1 次,减少了大量调度延迟。CUDA Graphs 不改访存/计算本身,但减少了 idle gap,让整体执行效率更接近理论 AI。因为减少了 CPU→GPU 同步和调度的空转时间,提升了GPU利用率。
2.6 embedding的融合方式
在用户行为序列里,每一个行为token包含物品ID和它的side-infomation,传统推荐系统处理会采用多种融合方式:
- 每个side-info和ID选择相同维度embedding,逐元素相加
- 每个side-info自由选择维度,通过独立的线性映射到同维度,逐元素相加
- 每个side-info按照信息容量选择合适的embedding维度,concat之后线性映射到token维度。
实际上,第二种和第三种是代码实现不同,效果相同,它们是比第一种更常见的,原因是自由选择embedding的维度,减少PS的存储空间,根据效果量入为出。
我们逐一讨论,后两种核心是先concat做一次GEMM,还是多次GEMM后逐元素相加。
我们假设先不做任何fusion优化,一次GEMM的kernel launch时间占比小,且矩阵更大,容易吃满Tensor Core。我们常常犯的错误,选用了维度为4/8的embedding,它不满足Tensor Core的硬件基本要求,产生了额外的padding,如果多次映射里出现了side-info的embedding维度是小于16,那么通过Concat到16的倍数,能大幅度提升性能,当然这里要精心设计总体concat出来的维度,扩展性很差。在大GPU上,即便要节省embedding的存储,最好也是选择16的倍数。
concat操作本身,会产生一次拼接后维度矩阵的显存写入和后续GEMM的读取,而concat本身没有计算,AI非常低。concat操作需要通过算子fusion操作,避免掉这一次访存,TF打开XLA不一定自动地把Concat和后续GEMM融合到一起,稳妥地操作是使用batched matmul,也就是每个side-info单独做了一次GEMM后相加,但是在一个kernel中,只做一次launch,且在GEMM的K维均为16倍数情况下,MxN足够大(能切出足够多tile,吃满SM),它的计算性能与大矩阵GEMM几乎无差异。
方法2和3在考虑fusion后,是殊途同归,最终统一到一次kernel的多GEMM相加。对比方法1,它的优势是embedding存储节省,表达能力多了一次线性映射,但由于维度设置较小,远小于token的embedding维度,表达能力不相上下。方法1的优势,计算性能层面,id和side-info的embedding可以在GPU外就进行sum pooling操作,无论是基于PS还是EP策略,都减少了网络带宽和GPU的多次GEMM,并且浪费的存储维数虽然大,但side-info更为稠密,embedding table行数普遍远少于ID的数量,所以浪费的存储并不多。从性能角度选择方案1和2/3,要考虑瓶颈是网络带宽还是存储,以及2/3方案的传输维度和方案1的维度浪费。
如果考虑算法效果,在表达能力上,方法1的side-info维度更宽,不会因为人为判断side-info信息容量和维度偏低,导致语义表达能力不足,投影后“信息稀释”,方法2的Linear变换能起到学习side-info重要性的作用,算是一种表达能力的弥补;同维度embedding相加,ID和side-info天然统一语义空间,类目/品牌 embedding 会把相似 ID 拉在一起,隐式地帮助冷启动/长尾 item 学习得更好。这一点不好理解,sum pooling后,ID和side-info的embedding接收相同的梯度,一个batch里稀疏的ID embedding跟着稠密的类目embedding走了一步,如果没有稠密embedding更正确的表达,这个稀疏embedding随机的权重最初产生的都是更随机的梯度,这就是说它被稠密side-info拉着走,称之为语义锚点,或者叫隐式正则。
这里不仅讨论两种方式的优劣,也从这个讨论中提醒:
- 代码中的任何concat都需要审视——是否需要确定的fusion优化。
- embedding的选择一定保证是16的倍数,满足最低硬件要求,减少padding。
- GEMM的M和N是tile的倍数,并且要足够大能吃满SM,它是多GEMM fusion有效的前提。
3. 显存瓶颈:AI 还没拉高,先 OOM 了
本章概要:在真实训练里,先把你“卡死”的往往不是 FLOPs,而是显存:AI 还没进阶到算力扩展区,显存就炸(OOM)了。本章先把显存摊开——训练时显存主要由四块构成:模型参数、激活值、中间梯度、优化器状态,其中激活值随 Batch/序列长度 增长最猛,是大头。针对不同来源逐一减负:对激活,用 Gradient Checkpointing 以算换存、Gradient Accumulation 放大有效 Batch 而不放大显存、Flash Attention 以块化重算减少中间缓存;对参数/梯度/优化器,用 混合精度(FP16/BF16)与必要的权重量化降占用,并用 ZeRO/混合并行(数据/模型/流水)把参数、梯度、优化器状态切片到多卡。思路很朴素:先识别哪一块是“内存黑洞”,再用对路的工程手段把显存从“记忆”挪回“计算”。只有把显存瓶颈疏通,GPU 的算力与 AI 才有上升的空间,Scaling Path 才真正打开。
这里具体指的是显存大小的占用,与计算强度中的访存量不同。它和AI的关系是,显存能够优化减少,那么就能容纳更大的 batch size、序列长度、模型参数,按照前面的论述,AI能有效的提升。
3.1 训练时显存里有什么
- 模型参数。我们说1B参数,全精度下是4字节也就是4GB,半精度下推理是2GB,因为做了混合精度,有主权重在4GB加混合精度的2GB,一共是6GB。
- 梯度。训练反向传播时,每个参数都会有对应梯度张量,大小与参数量同级。1B参数的梯度,全精度下4GB,混合精度下2GB。
- 优化器状态,取决于具体的优化器,一般给每一个可训练参数都有对应的优化器变量,比如Adam每个参数保存对应的动量和二阶动量,1B参数无论是否混合精度都是8GB(一般情况)。这是因为动量是梯度的指数滑动平均值,梯度本身在训练初期可能很小,如果用 FP16(或 BF16)存储,会出现 精度下溢,导致动量更新几乎变成 0。总的来讲,为了保证收敛稳定性和更新精度,一般的框架都采用了FP32来存储优化器状态。
- 中间结果(激活值),这种是最复杂的一部分,1B参数的模型在H100(80GB显存)上OOM,大概率是这部分导致的。它是计算中的中间结果,保存它是为了在反向传播时使用。举个例子,Transformer的 产生的Attention Map,假设B=1024,H=16,L=1024,FP16精度,单层大小就有 。
3.2 优化激活值的手段
在大模型和推荐系统训练中,激活值是显存开销的主要来源之一,尤其是在反向传播阶段需要保存中间激活以计算梯度。如何优化激活值的存储与计算,是显存优化中最重要的环节之一。常见的手段包括:
优化激活值
- 激活检查点 (Checkpointing):丢弃中间激活,反向重算,换显存节省。
- 梯度累积 (Gradient Accumulation):把大 batch 拆成 micro-batch,间接缓解激活显存压力。
- 已经讨论过的 Flash Attention:避免显式存储 Attention 矩阵,本质也是激活优化。
- 已经讨论过的 混合精度训练:减少激活存储位宽,也使激活值占用的存储空间下降了一半。
3.3 Gradient Checkpointing
在深度学习训练中,中间激活往往是显存占用的最大部分。这些激活必须在前向传播时保存下来,因为反向传播计算梯度时需要用到它们。在 GPU 的 memory-bound 区间,计算资源通常比显存资源更充裕,因此用额外计算换取显存节省是划算的。然而,如果每次反向传播都从输入开始完整重算所有激活,并不能显著降低峰值显存,因为在计算最后一层梯度时,前面 N−1 层的激活仍需常驻显存。Gradient Checkpointing 的核心思想,是将模型划分为若干段,在前向传播时仅保存每段起始层的输入(检查点),而不保留段内所有激活;在反向传播时,从该检查点开始重新计算该段内的激活,然后立即执行该段的反向计算并释放激活。这样,显存占用由与总层数成正比降至与段长成正比 ,在大幅缓解 OOM 的同时,仅增加有限的重算开销。
3.4 Gradient Accumulation
在推荐系统中,隐藏层维度通常较低,为了提升吞吐量,往往会设置较高的 Batch Size。在一次矩阵乘法 [M, K] x [K, N] -> [M, N] 中,Batch Size 参与 M维,而隐藏层维度参与 K 和 N。根据算术强度的特点,矩阵越接近方阵(即 M、K、N 更均衡)时 AI 越高。在显存存储受限的情况下,如果同时调低 Batch Size 并调高隐藏层维度,就可以提升矩阵的“方形程度”,从而提高 AI。然而,小 Batch 会导致梯度方差增大,参数更新不稳定,影响收敛效果。为在小 Batch 下依然获得与大 Batch 等效的收敛性,可以采用 Gradient Accumulation:先在小 Batch 下计算梯度,累计到等量大 Batch 的规模后再执行一次参数更新。这里的小Batch叫做micro-batch,累计的大的Batch叫Effective Batch
梯度累积本身并不能提升 AI,因为单次小 Batch 的 AI 仍然较低、吞吐也较差,但在显存受限的条件下,它允许我们在提升隐藏层维度、改善 AI 的同时,不牺牲收敛稳定性。对大模型训练,Gradient Accumulation几乎是标配了。
3.5 Flash Attention
除了访存带宽的优化(AI计算强度-拉长序列时有介绍),它更重要的是优化显存存储大小,使得不必在存储 的Attention Map,为扩展宽度和拉长序列长度打开了显存存储上的空间。在常规实现下,B=512,L=1024,H=8,d_model=512,layer=4 时,Attention Map 占用显存高达 32GB(B×H×L×L×2 字节)。相比之下,QKV 投影只需约 6GB,FFN 激活约 18GB,Attention Map 是中间激活的最大瓶颈,而且随着 L 呈平方级增长。Flash Attention 改为 在线 softmax,只需保存归一化的局部统计量,将显存复杂度从 O(L²) 降到 O(L),显著降低了长序列训练的显存开销。

FlashAttention 原理简述
在标准注意力(Attention)中,我们会一次性计算整个 矩阵(大小 ),然后做 Softmax 得到注意力权重,再与 相乘得到输出。这种做法需要将中间的 矩阵写入和读取显存(HBM),显存访问量是 ,在长序列下极其耗时并且受限于带宽。
FlashAttention 的核心思路是分块计算。将 K 和 V 按列分成若干块(tile),每次只加载一个块到 GPU 片上存储(SRAM / 寄存器 / shared memory),与 Q 分块计算对应的注意力分数,然后直接在片上完成 Softmax 与乘 V,并把结果累加到输出向量 中。这样中间的 矩阵从未落到显存,显存访问量从 降到 。
Softmax 的定义与分块累积:
回顾一下Softmax 的定义: 。对于输出的每一行 ,我们可以在处理每个块时,计算该块对分子(加权和)和分母(权重和)的贡献,并累计到当前结果: , ,最终结果是 。在 FlashAttention 中,通过 分块计算,避免了显存中存储完整的 Attention Map,只需要维护每个 query 的: ,这两个量的存储开销都是与序列长度成正比,即 。
数值溢出问题与分块平移不变性:
假设没有指数溢出的问题,其实分块累计就是这么简单——分别累计分子和分母,最后除一下。但在长序列或大数值范围时,指数 会非常大,尤其是在 FP16 下,指数大约超过 11 就会溢出成
inf。这是普通 Softmax 里也会遇到的问题,通常的解决方法是 平移不变性: 。 平移后,所有指数项 ,不会溢出,并且结果分布完全不变。在 FlashAttention 里,因为是分块计算,我们无法一开始就知道全局最大值 ,所以采用在线维护的方式:
- 先计算当前块的最大值
- 与之前的全局最大值 比较,得到新的全局最大值
- 由于平移不变性,原来已经累积的分子 和分母 需要统一到新的基准 ,通过乘以: 完成缩放。
- 然后用当前块的分数 计算指数,加到 中
- 更新
这样,每次都把历史累积与当前块放到同一指数尺度,确保不同块的贡献可以直接相加。
多块合并与持续缩放:
进一步解释,对于两个合并块,只需要知道它们的局部最大值。累计指数的时候,减去的是最新的最大值 ;而之前维护的 ,减去的还是之前的最大值 ,需要通过乘以 来缩放。
假设过程中缩放了 N 次,实际累计为:
这就是持续缩放到全局最大值 的分子和分母。最后再除一次,就得到了 。
3.6 混合精度训练
在参数存储上,混合精度引入了主权重,使 1B 参数从原本 4GB(FP32)增加到 6GB(FP32 主权重 + FP16),但参数占用本身并不是瓶颈。真正的显存节省体现在 激活值:QKV、Attention Map、FFN 输出这些中间结果数量远大于参数。如果从 FP32 降为 FP16/BF16,激活显存直接减半。延用上面举例参数,非 Flash Attention情况下激活总量约 60GB,若用 FP32 会翻倍到 120GB 根本放不下,而混合精度则能将其压缩回 60GB,使模型训练可行。

混合精度的改造,要关注模型的数值稳定。对于极端数值(如 softmax、layer‑norm 的分母),保留 FP32 运算;保留一份 FP32 的“主权重”用于累积梯度和权重更新,保证优化器状态的精度,在每次更新前,将主权重拷贝/转换为 FP16 张量供前向计算;FP16下, 动态范围有限,需要对损失值进行缩放以避免梯度下溢,可以选用BF16,它与FP32的指数位相同,动态范围一致,可以避免数值稳定问题,不必做loss scale。
这里常有的一个疑问,如果使用BF16,它的指数位和FP32一样,还需要主权重(Master Weights)么?BF16:1位符号 + 8位指数 + 7位尾数,FP32:1位符号 + 8位指数 + 23位尾数。虽然 BF16 几乎不会下溢/上溢,但尾数精度只有 ~2 位十进制有效数字,而 FP32 有 ~7 位。优化器更新(尤其是 SGD + momentum 或 Adam)需要高精度累积,否则更新会被量化噪声淹没——当学习率较小或梯度幅度较小时,BF16 的尾数无法表示很小的更新量,导致更新被截断成 0。大部分框架(包括 TF2.x AMP、PyTorch AMP)在 BF16 模式下仍保留 FP32 master 权重,就是为了优化器阶段的稳定性。推理阶段可以直接用 BF16 权重,不需要 FP32 master。
3.7 分布式并行与ZeRO
前面Gradient Checkpointing和Gradient Accumulation都是在处理激活值,再考虑参数、梯度和优化器的处理。当单卡显存有限,通过分布式并行来拓展显存空间。
分布式并行包括DP(Data Parallel)、TP(Tensor Parallel)和PP(Pipeline Parallel)。数据并行(DP)是最常用的方式,每张 GPU 保存完整模型,处理不同数据并在每步用 AllReduce 同步梯度,简单高效但要求模型能单卡放下;张量并行(TP)把单层的矩阵或张量运算切分到多卡执行,适合单层参数很大但会引入频繁通信;流水并行(PP)按层切分模型到不同 GPU,通过流水线传递微批数据,能训练超深网络但存在流水空泡和激活通信开销。
对于推荐模型来讲,即便充分提升AI,参数量还是远小于现在的大模型,总参数 < 3B 且 单卡 80GB 能跑通,先别上 TP/PP,TP/PP引入更多同步与流水填充复杂度,在“参数不大”的推荐模型上收益有限、复杂度高。我们主要考虑在DP上,通过ZeRO优化减低参数、梯度、优化器的显存占用。

DP下,每个GPU上仍旧保存了完整的模型参数,梯度和优化器状态,显存占用和通信开销是巨大的瓶颈,ZeRO就是一套参数分shard的维护和分布式同步策略,来实现低冗余、高并行的分布式训练同步和更新。
ZeRO有3个stage,分别是对优化器、梯度和参数做分shard处理。如stage1,每个GPU只维护优化器状态的一个Shard,在需要在前向、反向、更新时做 All-Gather 或 All-Reduce 操作,用于组装/同步 shards。把分片合并成完整张量到一个GPU上,计算完成后立即释放完整副本,只保留自己负责的那一片。
以下数据基于公开文献与社区报告,给出 ZeRO 各阶段在不同 GPU 规模下的强扩展效率(实际吞吐/理想线性吞吐)。具体数值会随模型规模、硬件拓扑、网络带宽等因素而变化,供参考。对于推荐系统,Dense选择DP+ZeRO,足够应对显存问题,且有着极高的扩展效率,即吞吐下降控制在10%内(128卡内,做ZeRO-1、2)。
GPU 数量 | ZeRO-1 效率 (优化器状态分片) | ZeRO-2 效率 (再分片梯度) | ZeRO-3 效率 (全参数分片) |
8 | 99% | 97% | 90% |
16 | 98% | 96% | 88% |
32 | 97% | 94% | 85% |
64 | 96% | 92% | 80% |
128 | 95% | 90% | 75% |
256 | 94% | 88% | 70% |
512 | 93% | 85% | 65% |
4. 系统 IO 瓶颈:样本、Embedding 与数据通路
本章概要:哪怕显存和算力的瓶颈都解决了,推荐系统依旧可能卡在 IO 通道上。传统 point-wise 的样本范式会带来存储和数据加载的平方级膨胀,升级到 list-wise 能显著减少冗余,提升吞吐。Embedding 方面,传统的 PS 架构更多是“小卡遗产”,在大 GPU 环境下反而变成拖累,必须演进为 混合并行 / GPU 原生方案,才能释放大规模并行的潜力。
在显存利用与算术强度(AI)已经优化完成后,GPU 计算单元仍可能长期处于空闲状态,其根源往往不是算力不足,而是训练数据的I/O 无法跟上计算节奏。推荐系统尤其如此:样本量庞大、特征维度稀疏、Embedding 参数规模巨大,使得数据加载、特征访问与参数拉取都成为主要的带宽消耗点。如果 I/O 流程效率不高,再强大的 GPU 也只能“干等”,整体训练效率大幅下降。
因此,I/O 优化的核心目标是:减少数据传输的总量,缩短传输路径,并提升带宽利用率。本章将围绕这一目标,从四个方面展开:
- 长序列下list-wise 样本重组 —— 减少冗余读取,提升顺序访问效率;
- 减少记忆化特征存储 —— 降低存储与加载压力,避免对 I/O 的过度依赖(第 5 章展开);
- Embedding 参数服务器(PS)架构改造 —— 在系统层面对参数分布与访问方式进行优化,缓解网络和存储带宽瓶颈。
- 改善传输路径 —— 通过 pinned memory、异步拷贝、GPUDirect Storage / RDMA 以及拓扑亲和性优化,让数据更快、更直接地抵达 GPU;
4.1 user-wise样本组织
如果把推荐系统的样本存储空间做一个容量分析,item-aware的特征定义为物品特征以及和物品相关的user、context特征(如Search-Base的序列),随着用户序列的拉长以及记忆性特征的取消(笛卡尔积交叉),item-aware的特征比重变得越来越低。业界普遍采用point-wise模式来存储排序样本,即每一个impression产生一条样本,一个Request里面有几十个impression,它的纯粹的用户侧特征是完全相同并且重复的。当序列拉长后,比如一个几千的序列,在用户一天内的impression中,序列的绝大部分历史行为是重复的。它是impression交叉历史engagement的平方级增长,产生了存储和网络IO的巨大浪费。

首先,很容易做到的是一个request的impression共享一条用户历史序列,它的升级相对容易,无论过去是怎么生产样本的,是batch还是流式训练,从impression到Request级别并不影响它的搭配方案。但这还远远不够,继续把用户更长窗口的多个Request聚合成一条样本,通过index指针“挂”在一条用户历史长序列上。
把用户M days之前的request全部合并。对于行为丰富用户做多条切分,对于行为稀疏用户拉长了时间,能让更多request共享历史超长序列,减少存储和计算成本。假如M=30,则是用户一个月之前的样本全部合并。它就像一份pre-train Sample,以往推荐模型改造结构需要部分加载模型权重,是因为把历史样本全部重训一次成本巨大,现在这份“压缩”过的样本,极大地减少了历史全量重训练的成本。
为了兼容推荐系统常见的one-epoch增量训练,把用户M days之内的样本按N days切分,N days内的request共享一个用户历史行为序列,减少存储和计算成本,当然主要节省在行为丰富的用户上。这份样本incremental-train samples,假如N=1,就是daily增量训练。
这里有一个假设就是默认为批式训练,基于流式的系统改造成本高,它最多改造成基于Request共享超长序列,绝大多数重复序列计算无法避免,也注定了这样的系统在算力驱动的Scaling Path上会逐渐落后。
4.2 Embedding PS架构升级
推荐系统在小模型时代,是海量embedding+小参数量NN模型,这样的模型非常适合Parameter Server(PS)架构,但当NN模型参数量变大,PS的缺点暴露了出来。
- 带宽与延迟瓶颈
- PS 模式下,所有 Worker 计算完梯度后都要向 PS 推送(push),再从 PS 拉取(pull)最新参数。随着参数量和 Worker 数量增长,PS 节点的网络 I/O 压力呈线性甚至超线性攀升,容易成为整体训练的瓶颈。
- PS 通信路径长、跳数多,对网络延迟尤其敏感,往往无法像 DDP 的 NCCL all-reduce 那样高效地与计算重叠。
- 一致性与收敛风险
- 为了减轻 PS 压力,经常会采用异步更新(Asynchronous SGD),但这会引入梯度“延迟”(staleness),对大模型的收敛稳定性和最终精度都有不小影响。
- 如果改为同步更新,又必然进一步加剧网络开销和等待时间,回到瓶颈。
前面我们默认的 PS(Parameter Server)架构,在分布式训练中逐渐被 DistributedDataParallel(DDP) 替代。DDP 基于 NCCL(NVIDIA Collective Communications Library) 来完成梯度和参数的高效同步,特别适合深度学习中大规模 NN 参数的训练。但在 推荐系统场景 下,Embedding 的维度和规模往往极其庞大,直接用 DDP 同步会导致:
- 参数量过大:Embedding 表往往是 TB 级别,难以放进单机 GPU 显存。
- 通信开销极高:全量同步会拖垮网络带宽,训练效率大幅下降。
- 稀疏更新特性:Embedding 参数通常是稀疏访问,很多参数在单个 batch 中根本不会被用到,同步它们纯属浪费。
因此,在 从 PS 向 DDP 过渡的阶段,通常采用 混合架构:
- NN 参数(Dense 部分) 使用 DDP + NCCL,同步效率高,梯度更新稳定。
- Embedding 参数(Sparse 部分) 依旧放在 PS(Parameter Server) 上维护,通过 pull/push 的方式只传递访问到的参数切片。
但在IO与数据通路上依旧不是最优:小粒度随机访问导致通信低效,Embedding ID 的访问模式分散且稀疏,每个 step 都要发起大量零碎的 pull/push 请求,这让网络带宽利用率很低,GPU常常空等 embedding 返回。数据搬运链路过长,延迟高,Embedding 往往要经过 PS → CPU 内存 → 网络 → GPU 显存 的多次拷贝,缺乏与计算的重叠,IO 延迟直接暴露,算力利用率被拖累。
通信方案选择基于All-to-All 的 EP(Embedding Parallelism)策略,它把巨大的 embedding 表“切片分到多块 GPU”上,由各 GPU 负责自己那一片的查表与更新,然后用 All-to-All 集体通信把向量在 GPU 之间交换、归位。
以一次前向为例:
- 去重与定位:对 batch 里的 ID 做
unique,记录inverse_indices。
- 分片路由:根据哈希规则,把唯一 ID 按“谁拥有这行”分到各 GPU。
- All-to-All 分发:把各自负责的 ID 发给相应 GPU。
- 本地查表:每块 GPU 只在本地 shard 查 embedding。
- All-to-All 回传:把查到的向量发回原始样本所在 GPU。
- 还原顺序与池化:用
inverse_indices复原到样本位,再做 pooling,进入后续 dense tower。
与PS相比,PS中心化 KV 服务,Worker 通过网络拉/推行向量与状态;容易受 RPC 带宽与热点倾斜影响,EP把参数“分家”到各 GPU,靠 All-to-All 点对点交换,带宽利用更高、延迟更稳。
EP 可以进一步和 GPU 热缓存结合:把 Top-K 热 ID 在本地或节点级 GPU 缓存,其余再走 EP 的 All-to-All,相当于Top-K热的ID和Dense一样通过DP做全复制。
非常巨大的embedding哪怕分片也会撑爆宝贵的GPU显存,这时候只有EP策略是远远不够的。由于推荐系统的IDs有非常显著的长尾效应,可以把尾部放到CPU内存或SSD上存储。
整个Embedding表被拆成了多部分:
- 热 ID:走 DP 风格缓存,避免 A2A 与跨机带宽。
- 温 ID:走 EP(All-to-All)。
- 冷 ID:EP 的 shard 查不到时,再向 CPU / SSD offload 请求。
从算法设计层面,避免依赖稀疏的笛卡尔积组合特征,尽可能不用UID特征,保证最大的部分是物品ID的embedding,则会对训练吞吐有一个巨大的提升,同时也隐含着让模型更加泛化。
4.3 训练数据优化传输
测试整体路径是否存在短板,通过hack输入来测试瓶颈在哪里,先确认是否存在瓶颈以层层缩小范围确认瓶颈在哪:
- rand 张量直接在 GPU 上生成,如果训练飞起,说明 CPU I/O 路径(磁盘读、解码、DataLoader)有问题,继续排查。
- batch 在 CPU rand生成,再搬到 GPU,如果这一步拖慢,H2D或内存带宽有问题。如果起飞,本地 IO/CPU 瓶颈。
- 单机单卡对比多卡,如果单卡快多卡慢,分布式通信 (NCCL、AllReduce) 瓶颈。
完全未优化的训练数据的传输路径:HDFS → (网络传输到节点) → 本地内存 → DataLoader → GPU (H2D,Host2device)
- HDFS 的网络吞吐和小文件访问延迟很容易成为瓶颈:
- 网络带宽有限(尤其多机同时训练,大家抢 HDFS 带宽)。
- 小文件随机读会导致 Namenode/ Datanode 压力大,延迟高。
- 即使数据进了 CPU 内存,GPU 也要再走一遍 H2D 拷贝,还会多一次内存搬运。
初级优化需要把传输路径优化到:HDFS (一次大流量拉) → NVMe (顺序读) → pinned memory (CPU) → GPU
- HDFS → NVMe。把足够大的数据集合,一次性大流量拉到本地 NVMe SSD。HDFS 上是网络存储,NVMe 是本地直连,带宽差别巨大(几百 MB/s vs 几 GB/s)。避免每个 batch 都去“挤网络”,变成“本地高速顺序读”。
- NVMe → pinned memory (CPU)。从本地 NVMe 顺序读取 batch 数据到 pinned memory。因为 pinned memory 是页锁定内存,可以被 GPU 的 DMA 引擎直接访问。CPU → GPU 的 H2D 拷贝速度更快,还能异步传输。
- pinned memory → GPU 显存。通过 PCIe / NVLink 拷贝到 GPU,开启 non_blocking=True,可以边拷贝边计算。减少 GPU idle 时间,让传输与计算重叠。
这个方案避免 batch 粒度的网络瓶颈,但CPU/DRAM 仍是中转,GPU I/O 等待明显。
继续优化为,LLM标配:HDFS (预拉/预切分) → NVMe (本地存储分片) → GPU 显存 (GPUDirect Storage)
- HDFS → NVMe。训练前将数据预切分好,批量落盘到各节点 NVMe(Sharding),避免训练时随机打 HDFS。
- NVMe → GPU 显存 (GPUDirect Storage)。本地 NVMe SSD 通过 GPUDirect Storage机制直接 DMA(Direct Memory Access) 到 GPU 显存,绕过 CPU 内存。
LLM顶配,全面零拷贝:HDFS / 对象存储 (远端) → GPU 显存 (GPUDirect RDMA)
- 远端 HDFS / 分布式对象存储。不再依赖本地 NVMe,而是把存储池(HDFS on NVMe-oF)直接暴露给 GPU。
- RDMA → GPU 显存直达。通过 GPUDirect RDMA 把远端数据直接搬进 GPU 显存,CPU 和本地 DRAM 完全绕过。
5. 范式重构:让算力流向泛化
本章概要:在特征选择上,过多依赖“记忆性特征”只会占满带宽和梯度预算,拉低泛化能力,真正有价值的是提升 泛化性特征 的比重,让算力流向更有前景的方向。随着生成式推荐和超长序列场景的出现,KV Cache 的高效组织与传输也变得关键,它决定了长序列推理能否保持线性成本。换句话说,IO 不只是“搬数据”,而是整个系统范式的设计问题:只有从样本、特征、Embedding 到 KV Cache 全面重构,推荐系统才能真正踏上算力扩展的高速路。
当前关于“下一代推荐系统”的讨论,很像2016–2018年那一波。当时的图景比现在清晰一些:至少要实现 deep learning base。比如在排序模型上,原本以 LR 为基础的方法,把一维的 weight 替换成多维的 embedding,再塞进一个 MLP 中,这是一个非常自然的思路。但实践中,有的人试了有效,有的人无效,总体提升并不大,尤其是当基线的手工特征工程越强,增益就越难体现。那个阶段也出现了 wide & deep 这样的工作,本质上是老一代与新一代方法的混合体。即便如此,要把它真正推上线,依然是一个相当困难的过程。
深度学习在推荐系统中真正带来显著提升的(我个人的判断),是用户序列建模的引入。用户行为序列是一类传统方法难以处理的特征形式,而深度学习架构能够有效捕捉其中的模式,这是质的突破。
当然,随着实践深入,业界也开始怀疑单纯依赖 MLP 的特征交叉能力是否足够,于是另一个方向便是研究新的特征交叉结构,如 DCN。这样,排序模型的研究逐渐被拆分为两个部分:
- 特征交叉结构 —— 如何更好地建模静态或上下文特征之间的交互;
- 用户序列建模 —— 如何提取并利用行为序列信息。
它们之间的关系是:序列建模先产生一个 pooling 后的表征,再与其他特征一起输入到特征交叉结构中,这几乎是业界标准的范式了。一个直观的例子是:MLP/DCN(concat(DIN(序列), 其他特征))。很多模型复杂化的工作,还是在这个范式上,把DIN改成了Transformer,把MLP换成了Transformer、TokenMixer,范式没有变化。
下一代推荐系统的核心突破,不仅在于AI的提升,更在于以用户行为序列为主体的建模。通过延展用户行为序列,可以在Transformer这类架构中获得近似线性的AI增长,这一点已在LLM中得到验证。而用户行为序列的知识挖掘,目前仍停留在研究的早期阶段,是一座深厚而有待开发的宝库。
这里面有多个问题:
- 传统的point-wise样本组织模式在超长序列上产生巨大的IO和存储浪费,以及超长序列的多次重复计算。
- 各类记忆性特征占满数据通路,产生IO瓶颈;也会竞争梯度,使得序列上的泛化性建模难以成功。
5.1 user-level training与KV cache
第 4 章通过样本和序列特征的重组织,已经减少了存储和IO浪费,接下来的关键是如何避免无效的重复计算浪费。
这里面需要一些模型结构的依赖,比如基于Transformer的decoder-only架构,因为有Casual Attention的存在,用户更远期的行为的Key、Value表征不会受到近期行为的影响,近期行为作为Query去查询远期行为的KV。所以通过user-wise样本入图,序列可以只计算一遍,需要和target交互时,以target的ID和Side-info为Query和用户历史序列做Attention即可。
在预测时,因为模型是Batch的,依靠好的泛化性一段时间不做更新,推理时也就能做KV cache,用户Session内甚至N天内的历史序列的KV就可以持久化住,新产生的行为只需要更新增量的KV,在用户发起请求时无需重复计算,只需完成target和历史序列KV的特征交互。
参考ChatLLM应用,选择会话粘性(Session Affinity)方案,而非分布式 KV Cache 存储,实现简单、延迟最低,也不用额外承担序列化/反序列化与网络传输的开销。具体实现上,同一UID打到同一个GPU计算节点,Session内已经计算过的历史行为Attention通过KV Cache,把已经计算过的Key和Value缓存在显存、内存、SSD中。
5.2 去“记忆性”特征
“记忆性”特征,典型的包括稀疏笛卡尔积特征、UID大规模FFM交叉、用户Match统计特征。这些都是现有基线下强有力的特征,能快速地收敛拿到效果。
由于记忆性特征往往在训练初期产生更强的梯度信号,它们会在优化过程中“抢占”有限的学习能力,压制对泛化性结构的训练。这种现象在 one-epoch 工业训练场景下尤为严重:
- 记忆性特征先行收敛,loss 快速下降,但泛化性结构未能充分训练;
- 导致模型整体表现过度依赖历史数据,对冷启动与长尾推荐无能为力。
这种现象可以理解为梯度竞争(gradient competition):记忆性部分以强梯度主导训练,泛化性部分因弱梯度被掩盖,学习不足。
之前,没有引入长序列和高AI的模型设计,这些特征还是相对有益的。但是由于“梯度竞争”的存在,我们发现记忆性特征确实在阻碍泛化性的建模,它直接表现就是在复杂化模型基础上加入这些特征,长期AUC下降。现在已经去掉了笛卡尔积和Match特征,下面就是要挑战去掉大规模的UID隐式交叉了。我现在的感觉,就是这些已经是常识了的记忆化特征,像三体里的智子,封住了推荐技术的发展。
结语
回到开头的判断:下一代推荐系统,无论是生成式、端到端还是超长序列,都建立在 Scaling Law 的假设之上;如果有了大卡 GPU 但利用率低下,Scaling Path 就被封住了上限。
这五章是同一条链路上的五个环节:先理解 GPU 和 Tensor Core 的硬件规律,让计算落在核心算力上;再用 Roofline 把计算强度推过拐点;然后疏通显存和 IO,给计算强度留出上升的空间;最后在范式上去掉记忆性特征、以用户行为序列为主体建模,让算力流向泛化。任何一环没打通,堆算力都只是在堆卡数。
推荐的模型算法是战术,高性能 GPU 是枪炮。再强的刀剑战术,也抵不过枪炮的一击。
References
Roofline: An insightful visual performance model for multicore architectures
The Case for Co-Designing Model Architectures with Hardware
Scaling Laws for Neural Language Models