如果你正在维护一个实时推荐系统,接口延迟卡在 200ms 上下,业务方天天催着优化,那你大概率会走到这一步:把推荐模型的计算从 CPU 迁到显卡服务器,在 RHEL 8 上把 GPU 算力真正用起来。
我近期参与了一次推荐链路延迟优化项目,场景是典型的 AI 实时推荐:用户每次刷新,系统要在几十毫秒内从海量候选里挑出他最可能感兴趣的内容。改造前线上服务跑在 CPU 集群上,接口 P99 始终在 180ms 附近,一到晚高峰就飘到 200ms 以上。我们把核心推理环节迁移到 RHEL 8 管理的显卡服务器后,P99 稳定压到 50ms 内,CPU 资源占用也明显下降。这中间踩了不少坑,也沉淀了一些可以复用的经验。
这篇文章适合两类人:一类是正在评估“要不要上 GPU”的推荐系统工程师,另一类是拿到 RHEL 8 服务器但不知道怎么把显卡驱动、容器运行时和推理服务正确串起来的运维同学。我不会只给命令,还会解释每一步背后的原因,以及在实际项目中怎么避坑。
1. 实时推荐系统的瓶颈到底在哪:为什么显卡能解决问题
1.1 推荐服务的三段式链路:召回、粗排、精排
实时推荐系统线上一般分三步:召回、粗排、精排。召回负责从全量物品里快速筛出候选,常见做法是用双塔模型产出用户向量和物品向量,再做向量检索找到 TopK。粗排做一个轻量计算,把召回的几千个候选过滤到几百个。精排是计算量最大的环节,通常是一个几十层甚至更深的深度模型,对每个候选挨个打分,最后排序输出结果。
这三步在用户刷新时是串行执行的。CPU 上跑精排,最直接的问题是:候选几百个,每个都要过一遍神经网络,模型打分本身是密集浮点运算,单个请求已经要吃掉大量 CPU 时间;高并发时请求一多,线程池被打满,延迟就会快速恶化。所以瓶颈从来不是“推荐逻辑写得不好”,而是计算资源形态和任务不匹配。
1.2 请求耗时的真实分布:模型推理才是大头
我们上线前对线上请求做过一次耗时拆解。特征工程和 embedding 检索大概占 25%,召回占 15%,精排模型打分占 55%,序列化和网络开销占 5%。模型推理占了超过一半,这部分是纯计算密集,CPU 并行能力有限,所以表现最差。
用生活化的类比理解:CPU 更像是出租车,适合处理逻辑复杂、不太容易拆分的任务;GPU 是公交车,适合把大量独立的小任务打包在一起顺路完成。推荐精排恰好是“几百个候选打分”这种高度并行的批量任务,交给 GPU 再合适不过。
这里还要说清楚延迟对体验的意义。用户点击刷新后,如果推荐内容能在 100ms 内出现,感知不明显;超过 300ms,用户就会觉得页面“转圈卡顿”。所以推荐系统的优化目标不是把平均延迟降一点,而是把尾延迟压到用户无感的区间。
1.3 显卡在推荐链路里的角色定位
显卡加入这条链路,解决的是“批量计算速度”问题:精排可以按 batch 推理,把几百个候选拼成一批,一次前向传播全部算完;向量召回的大规模矩阵运算也适合 GPU 并行。
另一个容易被忽略的点是 embedding 查表。推荐系统的用户和物品特征往往是稀疏 ID,需要查 embedding 表得到稠密向量。这张表在内存里的随机访存压力很大,CPU 查表经常发生 cache miss;GPU 显存带宽远高于内存带宽,查表吞吐提升明显。
所以结论很直接:推荐链路里最值得上 GPU 的是精排推理和向量召回,其次是 embedding 查表。至于特征拼接、规则过滤这类逻辑密集型环节,收益很小,不上也罢。后面我会把收益分层讲透。
2. RHEL 8 显卡服务器初始化:从禁用 nouveau 到 CUDA 可用
2.1 动手之前的三个前置问题
如果你是从 Ubuntu 切到 RHEL 8,会明显感觉到显卡环境初始化更繁琐。第一个拦路虎是 nouveau 驱动模块。RHEL 8 默认内核可能加载这个开源驱动,它和闭源驱动冲突,不屏蔽的话,装完驱动重启大概率黑屏或驱动加载失败。第二个是 Secure Boot。物理机或服务器固件开启安全启动时,内核模块必须有合法签名,否则即使装好了驱动,启动时也会被拒载。第三个是 kernel-devel 缺失。RHEL 8 默认不会给你装和当前内核严格匹配的头文件包,而 dkms 编译驱动模块又必须要它。
这些坑单个看都不难,但组合在一起,第一次操作很容易卡住。所以无论你打算用官方 run 包安装,还是用软件源装,第一步都一样:把系统更新到干净状态,装好开发工具和内核对齐的开发包。
2.2 驱动与 CUDA 的安装校验流程
先过一遍命令行流程。更新系统并安装依赖:
dnf update -y dnf groupinstall "Development Tools" -y dnf install -y kernel-devel kernel-headers确认内核版本和开发包版本一致,这一步非常关键:
uname -r rpm -q kernel-devel如果版本对不上,用dnf install kernel-devel-$(uname -r)补装。我在项目里见过不少次编译失败,最后发现就是内核升级之后没有同步安装对应的 kernel-devel。
接着禁用 nouveau。创建黑名单文件:
cat > /etc/modprobe.d/blacklist-nouveau.conf <<'EOF' blacklist nouveau options nouveau modeset=0 EOF重建 initramfs 并更新引导配置:
dracut --force grub2-mkconfig -o /boot/grub2/grub.cfg重启后检查模块是否真的没加载:
lsmod | grep nouveau没有输出再继续。安装驱动推荐走软件源方式,从显卡厂商官网的 CUDA 下载页找到对应 RHEL 8 的仓库文件,配置到/etc/yum.repos.d/下,然后:
dnf clean all dnf install -y cuda装完之后把 CUDA 的 bin 和 lib 目录写进/etc/profile.d/cuda.sh,再运行nvidia-smi验证。能看到显卡型号、驱动版本和显存容量,就说明驱动已经正常加载。顺便检查nvcc --version,确认 CUDA 工具链可用。
2.3 容器化环境的 GPU 透传
生产环境的推荐模型通常跑在容器里,不会直接装在物理机上。Docker 或 Kubernetes 容器运行时下,需要安装显卡厂商提供的容器工具包,并把对应的 runtime 注册进容器引擎。配置完成后,启动容器时加--gpus all参数,容器里就能访问到显卡。
容器内验证命令:
docker run --rm --gpus all <镜像名> nvidia-smi这里有一个高频报错要提前预防:容器里的 CUDA 版本如果超出宿主机驱动支持的版本范围,会直接报“CUDA 初始化失败”或者“驱动版本过旧”。我们的习惯是宿主机驱动保持较新版本,容器内 CUDA 向上兼容,这样模型迭代时不会受底层驱动限制。
2.4 Secure Boot 与内核模块签名
如果你的服务器开启了 Secure Boot,加载第三方内核模块前需要完成签名流程。具体做法是:在引导阶段进入 MOK 管理工具,导入驱动安装时生成的公钥,然后设置一个 MOK 密码,重启后系统会提示确认,确认一次就生效。
很多人在这一步栽跟头,表现为驱动安装一切正常,但重启后nvidia-smi找不到设备,同时系统日志里出现“要求签名”的报错。解决方案就是上面说的 MOK 签名流程。为了避免频繁被打断,我建议在测试环境先把 Secure Boot 关掉,跑通整个流程后再到生产环境走正式签名。
3. 推荐链路拆分:哪些环节上 GPU 收益最大
3.1 精排模型推理:收益最高的环节
精排是整个推荐链路里计算最重的部分,也是最应该上 GPU 的环节。典型精排模型输入特征几百维,网络层数几层,候选数量几百。CPU 上单个请求打分要吃几十毫秒;GPU 上把几百个候选拼成一个 batch,一次前向传播就能全部算完。
我们的改造里,精排单次耗时从 120ms 左右降到 20ms 上下。这个收益非常直接,因为精排耗时占比本来就高,GPU 把它一举变成了次要瓶颈。需要提醒的是,为了达到这个效果,光有 GPU 不够,还要做好特征拷贝和 batch 调度,这两块后面会详细讲。
3.2 向量召回:大候选集才有明显收益
召回环节如果用了向量检索,在物品量达到千万级以后,GPU 的加速效果非常显著。CPU 上的近似最近邻检索在高并发下 CPU 占用极高,而 GPU 能并行处理大批量向量距离计算。
但注意,召回不是所有场景都适合上 GPU。如果候选集只有几万条,CPU 做线性扫描或倒排索引也很快,强行上 GPU 反而要把数据搬到显存、再搬回来,增加无谓开销。只有检索规模足够大、QPS 足够高,GPU 的优势才真正体现出来。
3.3 别把简单计算搬到 GPU 上:收益最小甚至为负的环节
特征拼接、规则过滤、LR 粗排这类环节,主要是逻辑控制和内存访问,GPU 并行计算的优势发挥不出来。把它们放 GPU,还要频繁做 CPU 和 GPU 之间的数据拷贝(H2D/D2H),传输开销往往比计算开销还大,结果就是延迟不降反升。
所以迁移前要冷静判断。我习惯用一个简单判定表来帮助决策:
| 环节 | 计算类型 | GPU 收益 | 建议 |
|---|---|---|---|
| 精排 DNN | 密集矩阵运算 | 高 | 优先迁移 |
| 向量召回(大候选集) | 矩阵与距离计算 | 高 | 候选集大时迁移 |
| Embedding 查表 | 访存密集 | 中高 | 结合显存容量规划 |
| 特征拼接/规则过滤 | 逻辑控制 | 低 | 留在 CPU |
| LR/浅层粗排 | 简单算子 | 低 | 留在 CPU |
3.4 用 profiling 数据决定迁移顺序
确定迁移顺序前,我建议先给整条链路做一次完整 profiling。最简单的方式是在服务里加入耗时埋点,分别统计召回、特征处理、精排、序列化网络几段的耗时占比,压测环境跑一轮线上流量的副本,就能得到可信的数据。
更精细一点,可以用分布式追踪工具把一次请求的完整调用链串起来,观察瓶颈到底在哪个服务、哪个阶段。原则只有一个:让数据说话,而不是凭感觉。如果 profiling 显示 70% 的耗时在特征查询,那就算把 GPU 加到顶配也救不了,应该先去优化特征服务的缓存和数据结构。
4. 推理服务配置:动态批处理、精度量化与显存管理
4.1 动态批处理:吞吐和延迟的平衡艺术
GPU 推理不能一次只处理一个请求,那样算力利用率太低。主流推理服务框架都支持动态批处理:在一段很短的时间窗口内,把多个请求合并成一个 batch,GPU 一次算完,再拆开返回。这个机制是 GPU 推理吞吐的核心,但也是延迟优化的重灾区。
问题在于窗口设置。窗口设得太大,请求在队列里等待时间变长,P95、P99 会快速恶化;窗口设得太小,batch 稀疏,GPU 算力利用不起来。以推荐接口目标 P99 为 50ms 为例,我会从 2ms 的延迟窗口开始压测,观察 batch 填充率和延迟变化,再逐步加到 5ms、8ms,找到吞吐和延迟平衡点。不要一上来就配大窗口,P99 超时了你都不知道问题在哪。
4.2 FP16 与 INT8 量化:收益与风险
显卡服务器上的推理,通常不会一直用 FP32。支持张量核心的显卡在 FP16 下算力接近翻倍,显存占用减半,这是最简单的提效手段。推荐模型对 FP16 的容忍度很高,我们实际 A/B 测试基本没有观察到指标波动。
INT8 理论收益更大,但需要准备校准集做量化,部署复杂度和模型迭代成本都更高。我的建议是,先跑通 FP16,确认无感知后再评估 INT8。如果模型每一两周就迭代一次,INT8 的校准流程反而会拖累效率,这时候 FP16 已经足够。
4.3 显存规划:计算清楚再买卡
买卡之前,先估算几笔账。模型参数按文件大小计算,FP16 下减半;embedding 表如果全部放显存,要看规模是否扛得住;向量索引通常用乘积量化压缩后再放显存;动态 batch 需要预留临时空间。以一亿条 128 维向量为例,FP32 需要约 512GB,即使压缩成 FP16 也要 256GB,单卡显然放不下,必须设计拆表或缓存策略。
显存占用建议预留 20% 空余。CUDA context、cuDNN 工作区、推理框架本身都有固定开销,空余不足时动态 batch 跑到一半可能 OOM。这种错误在线上经常发生在流量峰值刚上来的时候,非常被动。
4.4 多模型并发与 CPU 侧的隐性瓶颈
一台显卡服务器上经常同时部署粗排模型和精排模型,甚至多个模型副本。这时要小心算力抢占。推理框架一般允许为每个模型配置独立显存池和最大并发,建议精排独占大部分算力,粗排限制并发数,避免小请求频繁打断大批量计算。
还有一个隐性瓶颈:CPU 侧能力。GPU 推理只是计算部分,特征处理、序列化、gRPC 收发都在 CPU 上。如果容器 CPU 限额压得太低,就会出现 GPU 利用率不高但接口延迟很高的怪现象。排查时先用top和pidstat看 CPU 有没有被打满,再去看 GPU,别被表象带偏。
5. 响应时间压测与锯齿状毛刺排查
5.1 压测指标设定:P99 比平均值重要得多
GPU 化改造完成,别急着上线,先做压测。压测看什么?我的答案是 P50、P95、P99,平均值只作为参考。平均延迟 45ms 看着很好,P99 可能已经到 300ms,用户实际感受是经常卡顿,因为平均被大量低延迟请求拉低了。
流量模型也要贴近线上,不能只做均匀并发。要模拟峰值抖动,比如把 QPS 在 10 秒内拉高两倍再降下来,观察延迟曲线是否出现拖尾,以及回到低流量后能否迅速恢复。能快速恢复说明系统有自愈能力,恢复慢则说明有排队堆积问题。
5.2 常见的延迟毛刺来源与排查套路
GPU 上线后最常见的异常是“锯齿状”延迟曲线:整体不高,但周期性出现尖峰。我整理过几个高频原因:
- 动态批处理的等待窗口和上游流量周期叠加,导致请求排队;
- GPU 温度过高触发降频或时钟切换;
- CPU 侧线程池占满,请求在进入模型前就排长队;
- gRPC 连接没有复用,频繁建连造成额外开销;
- 容器 CPU 配额限制导致数据传输线程被频繁打断。
排查第一步是看两个东西:GPU 利用率和显存使用曲线。把watch -n 1 nvidia-smi开着,同时跑压测,发现尖峰出现在 GPU 利用率低但显存波动大的时刻,说明问题可能出在数据搬运而不是计算。再用请求日志把耗时分布拉出来,对比就能精确定位。
一个实用的命令组合:
watch -n 1 nvidia-smi nvidia-smi dmon -s pucvmet -d 15.3 GPU 侧监控指标与告警阈值
上线后监控至少覆盖这几项:推荐接口 P50/P95/P99 延迟,GPU 利用率、显存使用量、温度和功耗,动态批处理的平均 batch 大小和排队时长,单次推理耗时,以及特征服务的查询耗时。
告警阈值不要只设一个固定值。P99 超过 80ms 要告警,GPU 利用率长期低于 20% 也要告警——低利用率往往代表配置不合理或者流量没有真正打上来。温度超过 85 度需要关注,超过 90 度基本可以断定出现了降频,延迟会周期性抬高,这时候优先排查机房散热和风扇策略。这些数值要结合自己业务目标定,不是金标准。
6. 用户体验层面的兜底:超时、缓存与降级策略
6.1 超时控制与队列保护
GPU 再快,也不是没有风险。模型加载、显存不足、GPU 故障都可能让请求变慢,所以接入层必须做超时控制和队列保护。我们项目里推荐接口总超时设了 120ms,模型推理预算 60ms,其余留给特征和网络。超过预算直接走降级逻辑,不允许无限排队。
队列保护同样重要。推理服务排队过深时,新请求应该直接返回兜底结果,而不是继续往队列里塞,否则一个毛刺会演变成全站雪崩。Kubernetes 环境里还要配合就绪探针和优雅退出,模型版本更新时不要打断存量请求。
6.2 多级缓存:热度、会话与应用层
推荐结果不一定要每次都全量算。我们按用户兴趣变化速度分了三个层级:热榜和热门 TopN 结果放 Redis,缓存几十秒,适合首屏这种高并发场景;同一用户短时间内的重复刷新,用本地进程内缓存直接命中,省掉一整轮特征和推理;针对固定场景的推荐结果还可以做预计算,在低峰期把常见请求的结果算好存放。
缓存要克制,推荐内容太“死”会伤害多样性,用户看到的永远是同一批东西,反而降低体验。所以缓存时间不宜过长,而且要用实验来验证缓存后的业务指标没有下滑。
6.3 冷启动与降级推荐:GPU 不能解决所有问题
GPU 化不会自动解决冷启动问题。新用户没有行为特征,新物品没有点击数据,再强的模型也拿不出高质量推荐。常规做法是给新用户走热门推荐、规则推荐或类别偏好试探,积累到一定行为量后再切回 GPU 模型;新物品则用内容相似度和运营规则补充。
最后分享一条我的个人体会:GPU 优化的是“计算能力”,但用户体验是系统工程。超时、降级、缓存、冷启动这些业务侧兜底,往往比多买两块显卡更能决定用户是否觉得“流畅”。做技术优化时,别只盯着显存和算子,把链路尾部的软性设计一起做扎实,P99 指标才能真正转化为体验提升。