1. 为什么我要从业务开发转向AI-Infra
1.1 一个让我彻底清醒的线上事故
三年前我还在做推荐系统的业务开发,每天的工作就是调参、跑A/B实验、写特征工程。直到有天凌晨两点,训练集群的GPU利用率突然从85%掉到12%,整个推荐模型的迭代进度直接卡死。我花了四个小时排查,最后发现是数据加载的IO瓶颈——特征文件被切成了太多小文件,每个epoch要打开几十万个句柄,文件系统的元数据操作直接把CPU打满了。
那次事故让我意识到一个问题:模型效果的上限由算法决定,但迭代效率的下限由基础设施决定。你可以有最好的算法工程师,但如果训练一个模型要等三天,再好的想法也跑不起来。从那以后我开始系统性地研究AI-Infra这个方向,从GPU调度、分布式训练、存储优化一路摸到推理加速和资源编排。
这篇文章就是把我这几年踩过的坑、搭过的系统、调过的参数整理出来,给同样想从业务转向Infra的工程师一条相对清晰的路径。不管你是做CV、NLP还是推荐,只要你的模型大到单卡跑不动、训练慢到影响迭代,AI-Infra就是绕不过去的坎。
1.2 AI-Infra到底在解决什么问题
很多人对AI-Infra的理解停留在“装显卡驱动”或者“配CUDA环境”,这其实只是最表层的东西。真正的AI-Infra要解决的是三个核心矛盾:
算力供给与需求的错配。一个团队可能有100张A100,但10个算法团队都在抢,谁先谁后、怎么分配、如何防止一张卡被占着不用,这些都需要调度系统来管。我见过太多公司买了卡之后利用率不到30%,本质上就是缺少资源管理。
训练效率与模型规模的矛盾。GPT-3级别的模型有1750亿参数,单卡根本放不下,必须做模型并行、流水线并行、数据并行。这些并行策略怎么组合、通信怎么优化、checkpoint怎么存,都是Infra要解决的问题。
推理成本与响应延迟的平衡。线上服务要求P99延迟在100ms以内,但大模型推理一次就要几百毫秒。量化、蒸馏、KV Cache优化、动态批处理,这些技术手段需要Infra工程师来落地。
我个人的判断是,AI-Infra的核心能力可以拆成四块:计算资源管理、分布式训练框架、高性能存储与网络、推理服务优化。这四块构成了一个完整的技能栈,后面我会逐一展开。
1.3 适合谁来看这篇内容
如果你满足以下任意一条,这篇内容应该对你有用:
- 做算法或业务开发两三年,感觉遇到瓶颈,想往底层走
- 团队里开始出现多卡训练需求,但不知道怎么搭环境
- 已经在做Infra相关的工作,但知识比较零散,想系统化梳理
- 对GPU、CUDA、分布式训练这些概念有耳闻但没实操过
我不打算写成教科书,而是按照一个工程师从零开始搭建AI-Infra能力的顺序来组织。每一块都会讲清楚“为什么需要这个”“核心原理是什么”“实际怎么操作”“我踩过哪些坑”。
2. 计算资源管理:从裸机到集群的必经之路
2.1 GPU调度为什么比CPU调度复杂
CPU调度我们都很熟悉,Linux的CFS调度器、Kubernetes的默认调度策略,本质上都是在时间维度上做分时复用。但GPU不一样,GPU是一种独占性极强的设备资源。一张A100有80GB显存,一个进程占用了60GB,剩下的20GB虽然理论上可以给别的进程用,但实际中很容易因为显存碎片或者CUDA Context的问题导致第二个进程跑不起来。
更麻烦的是GPU的拓扑结构。一台8卡服务器里,GPU之间的互联方式可能是PCIe、NVLink或者NVSwitch,带宽差异巨大。PCIe 4.0 x16的带宽是32GB/s,而NVLink 3.0可以到600GB/s。如果你的分布式训练任务需要做All-Reduce通信,把两个通信密集的GPU分配到PCIe链路上,性能直接打对折。
所以GPU调度器需要考虑的维度比CPU多得多:
| 维度 | CPU调度 | GPU调度 |
|---|---|---|
| 资源单位 | 核数、内存 | 卡数、显存、算力类型 |
| 独占性 | 可分时复用 | 基本独占 |
| 拓扑感知 | NUMA节点 | NVLink/PCIe拓扑 |
| 异构性 | 基本同构 | A100/V100/T4混部 |
| 故障率 | 较低 | 较高(ECC错误、掉卡) |
我刚开始做调度的时候,用Kubernetes的默认调度器直接调度GPU Pod,结果经常出现两个训练任务被分到同一张卡上,或者一个任务的两张卡跨了NUMA节点导致通信慢。后来才明白,GPU调度必须做拓扑感知和显存隔离。
2.2 主流GPU调度方案对比与选型
目前市面上做GPU调度的方案主要有几类,我按自己的使用体验做个对比:
Kubernetes + Device Plugin。这是最基础的方案,NVIDIA官方提供了k8s-device-plugin,可以把GPU作为扩展资源暴露给Kubernetes。优点是生态成熟、和现有K8s集群无缝集成。缺点是默认调度器不支持拓扑感知,也不支持GPU共享。你需要额外装NVIDIA的GPU Feature Discovery来打标签,再配合拓扑感知调度器。
Volcano。这是华为开源的批量计算调度器,专门为AI训练场景设计。它支持Gang Scheduling(一组Pod要么全调度要么全不调度),这对分布式训练非常重要——你肯定不希望8个Worker只起来了7个,剩下一个卡在Pending。Volcano还支持队列管理和公平调度,适合多团队共享集群的场景。
Slurm。传统HPC领域的调度器,在AI训练场景下也有不少公司在用。Slurm的优点是成熟稳定、对作业队列管理非常完善。缺点是和云原生生态割裂,如果你整个技术栈都是Kubernetes,再维护一套Slurm成本很高。
YARN。Hadoop生态的调度器,部分公司在大数据平台上扩展了GPU支持。但YARN对GPU的支持比较弱,容器隔离性也不如K8s,新项目不太建议选。
我个人的建议是:如果你从零开始搭,直接上Kubernetes + Volcano。K8s的生态和可扩展性是其他方案比不了的,Volcano补足了批量调度和Gang Scheduling的能力。如果团队已经有Slurm的积累,继续用也没问题,但要做好和云原生生态打通的准备。
2.3 实操:用Volcano做Gang Scheduling的完整配置
下面是我在实际项目中用的一套配置,目标是让一个8卡的分布式训练任务能够原子性调度。
首先安装Volcano:
kubectl apply -f https://raw.githubusercontent.com/volcano-sh/volcano/master/installer/volcano-development.yaml然后定义一个Queue,限制这个队列最多使用16张GPU:
apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: training-queue spec: weight: 1 capability: nvidia.com/gpu: 16接着定义PodGroup,这是Gang Scheduling的核心。minMember: 8表示这个组至少要有8个Pod同时调度成功才会真正启动:
apiVersion: scheduling.volcano.sh/v1beta1 kind: PodGroup metadata: name: distributed-training spec: minMember: 8 queue: training-queue最后在训练任务的YAML里引用这个PodGroup:
apiVersion: batch.volcano.sh/v1alpha1 kind: Job metadata: name: pytorch-training spec: minAvailable: 8 schedulerName: volcano tasks: - replicas: 8 name: worker template: spec: schedulerName: volcano containers: - name: pytorch image: pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime resources: limits: nvidia.com/gpu: 1这套配置跑下来,8个Worker要么全部调度成功,要么全部等待,不会出现部分启动导致的死锁。我实测下来,在16卡的集群上,Gang Scheduling能把分布式训练的任务启动成功率从70%提升到接近100%。
注意:Volcano的PodGroup需要和Job的
minAvailable配合使用,如果只配了PodGroup没配Job的minAvailable,调度行为可能不符合预期。
2.4 GPU共享与隔离的取舍
有时候一张A100跑一个小模型太浪费,我们希望能多个任务共享一张卡。NVIDIA提供了MPS(Multi-Process Service)和MIG(Multi-Instance GPU)两种方案。
MPS的原理是让多个进程共享同一个CUDA Context,减少Context切换的开销。但它不提供显存隔离,一个进程OOM可能影响其他进程。MIG则是把一张物理GPU切成多个独立的实例,每个实例有独立的显存和计算单元,隔离性最好。但MIG只支持A100及以上的卡,而且切分后每个实例的算力是固定的,不够灵活。
我的经验是:生产环境优先用MIG做隔离,开发环境可以用MPS提高利用率。如果卡比较老不支持MIG,那就老老实实一卡一任务,别为了省卡引入稳定性风险。
3. 分布式训练:从单卡到千卡的核心技术
3.1 数据并行、模型并行与流水线并行的本质区别
分布式训练听起来很复杂,但核心思想就一句话:把计算和存储压力分散到多个设备上。根据拆分方式的不同,分为三种基本策略。
数据并行(Data Parallelism)是最简单的。每个GPU上都有一份完整的模型副本,把一批数据切成N份,每个GPU算一份,然后通过All-Reduce把梯度同步。优点是实现简单,PyTorch的DDP几行代码就能跑。缺点是每个GPU都要存一份完整模型,显存占用不随GPU数量减少。一个10B参数的模型,用FP16存储就要20GB显存,加上优化器状态和梯度,单卡至少需要60GB以上。
模型并行(Model Parallelism)是把模型本身切开,不同的层放在不同的GPU上。比如前10层在GPU0,后10层在GPU1。优点是显存占用被分摊了。缺点是GPU利用率低——同一时刻只有一个GPU在干活,其他都在等。而且通信量很大,每层之间的激活值都要传输。
流水线并行(Pipeline Parallelism)是模型并行的改进版。把模型切成多个Stage,每个Stage放在不同的GPU上,然后把一个Batch切成多个Micro-Batch,让不同Stage的GPU可以同时处理不同的Micro-Batch。这样GPU利用率就上来了。缺点是会有Bubble(流水线气泡),即流水线填充和排空阶段部分GPU空闲。
实际训练大模型时,通常是3D并行:数据并行 + 流水线并行 + 张量并行。张量并行是模型并行的一种细粒度形式,把单个矩阵乘法切开,比如把权重矩阵按列切分到多个GPU上。Megatron-LM就是这种方案的典型实现。
3.2 PyTorch DDP的通信原理与调优
PyTorch的DDP(DistributedDataParallel)是目前最常用的数据并行方案。它的核心是Ring All-Reduce算法。
假设有N个GPU,每个GPU上有一份梯度。Ring All-Reduce把每个GPU的梯度分成N份,然后进行两轮通信:第一轮是Reduce-Scatter,每个GPU把自己的一份梯度发给下一个GPU,同时接收上一个GPU的梯度并累加;第二轮是All-Gather,把累加好的梯度广播回所有GPU。整个过程每个GPU只需要发送和接收2(N-1)/N份数据,通信量接近最优。
DDP调优有几个关键参数:
bucket_cap_mb。DDP会把梯度分成多个Bucket,每个Bucket内的梯度一起做All-Reduce。默认是25MB。如果模型很大,可以适当调大,减少通信次数。但调太大又会增加显存峰值。我一般设置在50-100MB之间。
gradient_as_bucket_view。设为True可以让梯度直接写入Bucket,减少一次内存拷贝。这个参数在PyTorch 1.10之后支持,实测能省5%左右的显存。
find_unused_parameters。如果模型有分支结构,某些参数在前向传播中没有被用到,需要设为True。但这会带来额外的图遍历开销。如果模型结构固定,建议设为False。
下面是我常用的一段DDP初始化代码:
import torch import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP def setup_ddp(rank, world_size): dist.init_process_group( backend='nccl', init_method='env://', world_size=world_size, rank=rank ) torch.cuda.set_device(rank) def create_ddp_model(model, rank): model = model.to(rank) model = DDP( model, device_ids=[rank], bucket_cap_mb=100, gradient_as_bucket_view=True, find_unused_parameters=False ) return model注意:
gradient_as_bucket_view=True和find_unused_parameters=True不能同时使用,会报错。如果你的模型确实有未使用参数,只能牺牲显存优化。
3.3 混合精度训练:省显存还是省时间
混合精度训练(Mixed Precision Training)是AI-Infra里性价比最高的优化手段之一。它的核心思想是:前向和反向传播用FP16计算,参数更新用FP32。
为什么这样能省显存?因为FP16占2字节,FP32占4字节。模型参数、梯度、激活值都用FP16存储,显存直接减半。为什么参数更新要用FP32?因为FP16的表示范围太窄,梯度很小的时候会下溢变成0,导致模型不收敛。所以需要维护一份FP32的Master Weight。
PyTorch提供了torch.cuda.amp来自动做这件事:
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for data, target in dataloader: optimizer.zero_grad() with autocast(): output = model(data) loss = loss_fn(output, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()GradScaler的作用是动态调整Loss的缩放因子。因为FP16的最小正数是6e-8,梯度小于这个值就变成0了。GradScaler先把Loss放大一个倍数,让梯度也放大,更新时再缩回去。
我实测下来,混合精度训练能省40%-50%的显存,训练速度提升20%-30%。但有几个坑要注意:
- 某些算子对FP16支持不好,比如LayerNorm的方差计算容易溢出,需要用FP32
- Batch Size太小时,梯度本身很小,GradScaler可能频繁调整缩放因子,影响稳定性
- 混合精度和梯度累积一起用时,要确保累积的梯度也是FP16的
3.4 实操:用TorchRun启动一个多机多卡训练任务
假设你有两台机器,每台8张卡,总共16卡。用TorchRun启动的命令如下:
# 在机器0上执行 torchrun \ --nnodes=2 \ --nproc_per_node=8 \ --node_rank=0 \ --master_addr="192.168.1.100" \ --master_port=29500 \ train.py # 在机器1上执行 torchrun \ --nnodes=2 \ --nproc_per_node=8 \ --node_rank=1 \ --master_addr="192.168.1.100" \ --master_port=29500 \ train.pymaster_addr是机器0的IP,master_port是任意一个空闲端口。TorchRun会自动设置RANK、WORLD_SIZE、LOCAL_RANK等环境变量,你的训练脚本里直接用dist.init_process_group就能初始化。
我踩过的一个坑是:多机训练时NCCL的网卡选择。默认情况下NCCL会自动选择网卡,但如果机器有多张网卡(比如一张万兆管理网、一张InfiniBand计算网),NCCL可能选错。这时候需要设置环境变量:
export NCCL_SOCKET_IFNAME=ib0 # 指定用InfiniBand网卡 export NCCL_IB_DISABLE=0 # 启用InfiniBand export NCCL_DEBUG=INFO # 打印NCCL调试信息如果发现多机训练速度远低于单机,十有八九是网卡选错了或者MTU没配对。用NCCL_DEBUG=INFO看日志,确认NCCL用的是哪张网卡、带宽是多少。
4. 高性能存储与网络:被忽视的性能瓶颈
4.1 为什么你的训练任务总是卡在数据加载
很多团队花大价钱买了A100,结果GPU利用率只有30%,瓶颈全在数据加载上。我见过最夸张的案例是:一个图像分类任务,每个epoch要读100万张小图片,文件系统每秒只能处理几千次元数据操作,数据加载成了整个训练流程的瓶颈。
解决这个问题的核心思路是:把随机小IO变成顺序大IO。具体做法是把原始数据打包成WebDataset或者TFRecord格式,每个文件包含几千条样本,训练时顺序读取。这样文件系统的元数据压力就小了两个数量级。
另一个方案是用内存缓存。如果数据集能放进内存,第一次epoch从磁盘读,后续epoch直接从内存读。PyTorch的DataLoader可以配合prefetch_factor和num_workers来做预取。我一般设置num_workers=8、prefetch_factor=4,让数据加载和GPU计算重叠起来。
还有一个容易被忽视的点是数据本地性。如果数据存在远端NAS上,每次读取都要走网络,延迟很高。最好的做法是把数据缓存到计算节点的本地NVMe SSD上。Kubernetes里可以用emptyDir或者hostPath来做本地缓存,配合Init Container在任务启动前把数据拉下来。
4.2 分布式文件系统选型:NFS、Lustre还是Ceph
训练数据的存储方案直接决定了IO性能。我按自己的使用经验做个对比:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| NFS | 小规模、数据量<10TB | 部署简单、运维成本低 | 性能差、单点故障 |
| Lustre | 大规模HPC、数据量>100TB | 性能极强、支持并行IO | 部署复杂、需要专用硬件 |
| Ceph | 云原生环境、对象+块+文件 | 统一存储、扩展性好 | 性能中等、调优复杂 |
| JuiceFS | 云上环境、需要弹性扩展 | 兼容POSIX、元数据分离 | 依赖对象存储、缓存调优 |
我的建议是:小团队用NFS就够了,但一定要配SSD缓存。中大团队如果预算充足,上Lustre或者全闪Ceph。云上环境优先考虑JuiceFS,元数据引擎用Redis,数据存在对象存储上,本地用SSD做缓存。
不管选哪种方案,都要做IO基准测试。用fio测顺序读写和随机读写,用mdtest测元数据性能。我一般要求训练集群的存储至少达到:顺序读5GB/s、顺序写3GB/s、元数据操作10万IOPS。达不到这个标准,训练效率肯定受影响。
4.3 RDMA与InfiniBand:多机通信的加速器
多机训练时,GPU之间的梯度同步要走网络。如果用普通的TCP/IP协议栈,数据要从用户态拷贝到内核态,再经过协议栈处理,延迟高、CPU占用大。RDMA(Remote Direct Memory Access)技术可以让网卡直接访问远端内存,绕过内核,延迟从几十微秒降到几微秒。
InfiniBand是RDMA的主流实现,目前主流的是HDR(200Gb/s)和NDR(400Gb/s)。一张HDR网卡的理论带宽是25GB/s,实际能跑到20GB/s以上。相比之下,万兆以太网只有1.25GB/s,差了十几倍。
部署InfiniBand需要注意几点:
- 子网管理器(SM)必须配置好,否则IB网络起不来。一般用OpenSM,跑在一台管理节点上。
- MTU设置。IB支持多种MTU,最大是4096。建议设成最大,减少包数量。
- NCCL配置。前面提到的
NCCL_SOCKET_IFNAME和NCCL_IB_DISABLE要配对,确保NCCL走IB而不是以太网。
我实测过一个ResNet-50的分布式训练任务,用万兆以太网做All-Reduce,8卡扩展到16卡时加速比只有1.3倍;换成HDR InfiniBand后,加速比提升到1.8倍。网络带宽对多机扩展性的影响是决定性的。
4.4 实操:用NCCL Tests验证多机通信性能
在正式跑训练之前,我强烈建议先用NCCL Tests做一次通信基准测试。这能帮你快速定位网络问题。
编译NCCL Tests:
git clone https://github.com/NVIDIA/nccl-tests.git cd nccl-tests make MPI=1 MPI_HOME=/usr/lib/x86_64-linux-gnu/openmpi跑All-Reduce测试:
mpirun -np 16 \ -H 192.168.1.100:8,192.168.1.101:8 \ -x NCCL_DEBUG=INFO \ -x NCCL_SOCKET_IFNAME=ib0 \ ./build/all_reduce_perf -b 8 -e 128M -f 2 -g 1-b 8表示从8字节开始测,-e 128M表示测到128MB,-f 2表示每次翻倍,-g 1表示每进程用1张卡。
输出里会显示不同消息大小下的带宽和延迟。重点看大消息(>1MB)的带宽是否接近网卡理论值。如果只有理论值的50%以下,说明网络配置有问题。常见原因包括:MTU没设对、网卡没跑在全速模式、交换机有拥塞。
5. 推理服务优化:从实验室到生产环境
5.1 推理和训练的本质差异
训练和推理看起来都是前向传播,但优化目标完全不同。训练追求吞吐量,即每秒能处理多少样本,延迟高一点没关系。推理追求延迟,尤其是P99延迟,因为线上服务有SLA要求。
这个差异导致优化手段也不同。训练可以用大Batch提高GPU利用率,推理如果Batch太大,单个请求的延迟就会增加。训练可以容忍一定的精度损失,推理对精度更敏感,因为直接影响用户体验。
推理优化的核心指标有三个:
- TTFT(Time To First Token):从请求发出到收到第一个Token的时间,主要影响用户感知的响应速度
- TPOT(Time Per Output Token):每个输出Token的生成时间,影响流式输出的流畅度
- 吞吐量:每秒能处理的请求数,影响服务成本
这三个指标是相互制约的。Batch越大,吞吐越高,但TTFT和TPOT也会增加。所以推理优化的本质是在延迟约束下最大化吞吐。
5.2 模型量化:FP16、INT8与INT4的取舍
量化是推理加速最直接的手段。把FP32的权重和激活值用更低精度表示,计算量成倍减少。
FP16量化是最安全的,精度损失几乎可以忽略。大部分GPU对FP16都有硬件加速,推理速度能提升1.5-2倍。实现也简单,PyTorch里model.half()就行。
INT8量化需要校准。因为INT8的表示范围是-128到127,而FP32的权重分布可能很广。需要用一批校准数据统计激活值的分布,确定缩放因子。TensorRT和ONNX Runtime都提供了INT8量化的工具。精度损失一般在1%以内,速度能再提升1.5倍左右。
INT4量化更激进,通常需要GPTQ、AWQ这类算法。它们把权重分成组,每组单独量化,减少精度损失。INT4能把模型大小压缩到FP16的1/4,但精度损失可能到2%-5%,需要根据业务场景评估是否可接受。
我的经验是:CV模型优先用INT8,NLP大模型用FP16或INT8,INT4只在显存极度受限时考虑。量化后一定要做精度回归测试,确保业务指标不下降。
5.3 KV Cache优化与PagedAttention
大语言模型推理有一个特殊之处:自回归生成。每生成一个Token,都要把之前所有Token的Key和Value缓存下来,避免重复计算。这个缓存叫KV Cache。
KV Cache的大小是:2 * num_layers * num_heads * head_dim * seq_len * batch_size * dtype_size。以一个7B模型为例,32层、32头、head_dim=128,序列长度2048,Batch Size=16,FP16存储,KV Cache就要2*32*32*128*2048*16*2字节,约16GB。这比模型本身的14GB还大。
vLLM提出的PagedAttention解决了这个问题。它把KV Cache分成固定大小的Block,像操作系统管理内存页一样管理这些Block。不同请求的Block可以非连续存储,按需分配。这样显存利用率能从传统的60%提升到90%以上,吞吐量提升2-4倍。
如果你在用vLLM或者TGI(Text Generation Inference),PagedAttention是默认开启的。但要注意gpu_memory_utilization参数,默认是0.9,表示用90%的显存做KV Cache。如果设得太高,可能OOM;设得太低,吞吐上不去。我一般从0.85开始调,根据实际显存占用微调。
5.4 实操:用vLLM部署一个7B模型的推理服务
下面是我在生产环境用的一套vLLM部署配置。
安装vLLM:
pip install vllm启动服务:
python -m vllm.entrypoints.openai.api_server \ --model /path/to/model \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.85 \ --max-model-len 4096 \ --dtype half \ --port 8000tensor-parallel-size 2表示用2张卡做张量并行。max-model-len是最大序列长度,设得太大会占更多KV Cache。dtype half表示用FP16。
启动后可以用OpenAI兼容的API调用:
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy") response = client.chat.completions.create( model="/path/to/model", messages=[{"role": "user", "content": "你好"}], max_tokens=256, temperature=0.7 ) print(response.choices[0].message.content)我实测下来,7B模型在2张A100上,FP16精度,P99延迟能控制在200ms以内,吞吐量约20 requests/s。如果换成INT8量化,吞吐能到35 requests/s,延迟基本不变。
注意:vLLM的
tensor-parallel-size必须能整除模型的注意力头数。比如7B模型有32个头,可以用1、2、4、8张卡,但不能用3张卡。
5.5 动态批处理与连续批处理
传统推理服务是静态批处理:攒够一个Batch就推理一次,推理期间新来的请求要等。这导致两个问题:Batch没满时GPU利用率低,Batch满了之后新请求延迟高。
连续批处理(Continuous Batching)解决了这个问题。它的核心思想是:不等整个Batch推理完,而是每生成一个Token就检查一次,把已经完成的请求踢出去,把新来的请求加进来。这样GPU始终在处理满Batch,利用率大幅提升。
vLLM和TGI都支持连续批处理。在vLLM里,这是默认行为,不需要额外配置。但要注意max_num_seqs参数,它控制同时处理的最大请求数。设得太小,吞吐上不去;设得太大,显存可能不够。我一般设置在128-256之间。
还有一个优化是Prefix Caching。如果多个请求有相同的System Prompt,可以把这部分KV Cache缓存下来,避免重复计算。vLLM支持enable_prefix_caching=True,对多轮对话场景效果很明显,TTFT能降低30%以上。
6. 常见问题与排查技巧实录
6.1 GPU掉卡与ECC错误处理
GPU掉卡是训练集群最常见的故障之一。表现是nvidia-smi看不到某张卡,或者训练任务突然报CUDA error。原因可能是硬件故障、驱动问题、散热不良。
排查步骤:
- 先看
dmesg | grep -i nvidia,有没有Xid错误。Xid是NVIDIA的错误码,Xid 48表示ECC错误,Xid 79表示GPU掉卡。 - 如果是ECC错误,用
nvidia-smi -q -d ECC查看错误计数。单比特错误可以纠正,多比特错误必须重启。 - 如果是掉卡,尝试
nvidia-smi -r重置GPU。如果重置无效,需要重启机器。 - 如果频繁掉卡,检查散热。A100的TDP是400W,如果机箱风道不好,GPU温度超过85度就可能降频甚至掉卡。
我踩过的一个坑是:GPU掉卡后,Kubernetes的Device Plugin没有及时更新资源状态,导致调度器还把任务往故障节点上调度。解决办法是配置Node Problem Detector,检测到GPU故障后自动给节点打Taint,阻止新任务调度。
6.2 NCCL超时与通信失败排查
NCCL超时是多机训练最常见的问题。报错信息通常是NCCL timeout或者Watchdog caught collective operation timeout。
排查思路:
- 检查网络连通性。用
ibping或者ping测试节点之间是否通。 - 检查NCCL配置。
NCCL_DEBUG=INFO看日志,确认用的是哪张网卡、什么协议。 - 检查防火墙。NCCL需要多个端口通信,如果防火墙没放行,会超时。
- 检查GPU拓扑。
nvidia-smi topo -m看GPU之间的连接方式,如果跨NUMA节点,通信会慢。
我遇到过一次NCCL超时,排查了半天发现是MTU不匹配。一台机器的IB网卡MTU是4096,另一台是2048,导致大包被分片,性能急剧下降。统一MTU后问题解决。
6.3 显存OOM的定位与解决
显存OOM是训练和推理都会遇到的问题。定位方法:
import torch # 打印当前显存占用 print(torch.cuda.memory_allocated() / 1024**3, "GB") print(torch.cuda.memory_reserved() / 1024**3, "GB") # 打印显存快照 torch.cuda.memory._dump_snapshot("memory_snapshot.pickle")memory_allocated是实际使用的显存,memory_reserved是PyTorch缓存池占用的显存。如果reserved远大于allocated,说明有显存碎片,可以用torch.cuda.empty_cache()释放。
解决OOM的手段按优先级排序:
- 减小Batch Size
- 开启混合精度训练
- 使用梯度累积
- 使用梯度检查点(Gradient Checkpointing)
- 使用ZeRO优化器做参数分片
- 上模型并行
梯度检查点是我常用的手段。它的原理是不保存中间激活值,反向传播时重新计算。这样显存能省60%以上,代价是训练速度慢20%左右。PyTorch里用torch.utils.checkpoint.checkpoint包装一下就行。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| GPU利用率低 | 数据加载瓶颈 | nvidia-smi看利用率,iostat看磁盘IO | 优化数据管道,用WebDataset |
| 多机训练慢 | 网络带宽不足 | NCCL Tests测带宽 | 换InfiniBand,调NCCL参数 |
| 训练Loss不收敛 | 学习率太大、数据有问题 | 打印梯度范数 | 调小学习率,检查数据 |
| 推理延迟高 | Batch太大、KV Cache不足 | 看TTFT和TPOT | 调小Batch,开PagedAttention |
| 显存OOM | Batch太大、模型太大 | 打印显存快照 | 混合精度、梯度检查点 |
| NCCL超时 | 网络不通、MTU不匹配 | NCCL_DEBUG=INFO | 检查防火墙、统一MTU |
| GPU掉卡 | 硬件故障、散热不良 | dmesg看Xid错误 | 重置GPU、改善散热 |
6.5 几个让我印象深刻的踩坑经历
坑一:DataLoader的num_workers设成0。刚开始做训练时,我没注意DataLoader的num_workers参数,默认是0,意味着数据加载在主进程里做,GPU算的时候CPU在等数据,GPU算完了CPU还在加载。后来设成8,训练速度直接翻倍。这个参数对训练效率的影响被严重低估了。
坑二:Checkpoint存到NFS上。有一次训练一个13B模型,Checkpoint有50GB,存到NFS上花了20分钟,期间训练完全阻塞。后来改成先存本地NVMe,再异步同步到NFS,阻塞时间降到1分钟以内。
坑三:vLLM的gpu_memory_utilization设成0.95。想着多用点显存提高吞吐,结果服务跑了一段时间后OOM。原因是KV Cache是动态分配的,高峰期显存需求超过预期。后来改成0.85,留出15%的缓冲,再也没OOM过。
坑四:混合精度训练时Loss Scale频繁调整。GradScaler的默认初始Scale是65536,如果模型梯度本身很小,Scale会频繁减半,影响训练稳定性。解决办法是手动设置init_scale=1024,让Scale在一个合理的范围内。
这些坑看起来都是小问题,但每一个都让我花了半天到一天的时间排查。AI-Infra这个方向就是这样,理论不难,难的是工程细节。每一个参数背后都有它的道理,不理解原理就调不好参数。
我个人在实际操作中的体会是:AI-Infra工程师的核心竞争力不是会多少工具,而是遇到问题时能快速定位到根因。这需要你对整个技术栈有深入的理解——从GPU硬件到CUDA驱动,从通信库到调度器,从存储系统到推理框架。每一个环节都可能成为瓶颈,而你的价值就是找到那个瓶颈并解决它。
最后分享一个我常用的排查思路:从下往上查。先看硬件层(GPU、网卡、磁盘),再看系统层(驱动、内核参数),再看框架层(PyTorch、NCCL),最后看应用层(模型、数据)。大部分问题都出在下面两层,但很多人一上来就查应用层,浪费了大量时间。