模型权重刚下载完,nvidia-smi也确认卡是空的,很多人第一反应就是复制一段启动命令直接跑——然后被 OOM 教育半小时。DeepSeek V4.1 Flash 这类带 MoE 结构的模型,部署难点从来不在“命令怎么写”,而在“显存这笔账你有没有提前算清楚”。这篇就把我这边从单卡验证到多卡上线的完整链路摊开讲:显存怎么估、四条部署路线各自适合什么场景、vLLM 和 SGLang 两套启动命令里每个参数到底在调什么、压测拐点怎么找,以及那几个反复出现、文档里基本不提的坑。不管你手上是一张 48GB 卡想先跑个 demo,还是八卡机器要接生产流量,下面这些内容都能直接拿去对照着改。
1. 先把显存这笔账算清楚,再谈启动命令
部署任何大模型,第一步永远是做显存预算,而不是试命令。原因很朴素:启动失败和启动成功但跑不满,是两种完全不同的故障,前者看日志就能定位,后者往往要压测半天才发现瓶颈在 KV Cache 上。提前把账算明白,能省掉大量试错。
1.1 权重、KV Cache、激活峰值三段式拆解
推理时的显存占用可以拆成三段来看,这三段的性质完全不同,压缩手段也不一样。
第一段是权重显存,它是静态的、一次加载后就不再变化的。占用大小等于参数量乘以每参数字节数:FP16/BF16 是 2 字节,FP8 是 1 字节,INT4 量化后大约 0.5 字节出头(还要算上量化 scale 的额外开销,通常按 0.55 估更稳妥)。这里有个容易踩的点:MoE 模型的权重显存要按总参数量算,不是按激活参数量算。激活参数只影响算力开销,不影响权重驻留显存。很多人拿激活参数去估显存,估出来的数字乐观得离谱。
第二段是KV Cache,它是动态的、随并发和上下文长度线性增长的。计算公式是:单 token 占用 = 2 × 层数 × KV 头数 × head_dim × 每元素字节数。系数 2 来自 K 和 V 两份缓存。以一组典型的 MoE 配置为例——61 层、8 个 KV 头(GQA)、head_dim 128、KV Cache 用 FP8 存——单 token 就是 2 × 61 × 8 × 128 × 1 ≈ 122KB。听起来很小,但乘上 32K 上下文就是单个请求接近 4GB,乘上 32 并发直接飙到 120GB 以上。这就是为什么“模型能加载”和“模型能服务”完全是两码事。
第三段是激活峰值,它在一次前向计算中瞬时产生。这部分不好精确计算,跟 batch size、序列长度、是否开启 CUDA Graph、算子实现都有关系。经验值是按权重显存的 5%~15% 预留,长上下文场景往 15% 靠,短请求高并发往 5% 靠。
提示:
config.json里的num_hidden_layers、num_key_value_heads、head_dim这三个字段是算 KV Cache 的全部依据,先把它们抄出来再动手。
1.2 一张可套用的显存估算表
下面这张表是我自己常用的估算模板,把公式填进去就能得到大致区间。注意表里是方法论,具体数字必须用你手上的模型配置替换。
| 组成项 | 计算方式 | 影响因素 | 可压缩性 |
|---|---|---|---|
| 权重 | 总参数量 × 每参数字节数 | 精度格式(BF16/FP8/INT4) | 高,量化可直接减半甚至更多 |
| KV Cache | 2 × 层数 × KV头数 × head_dim × 字节 × token 数 × 并发 | 上下文长度、并发数、KV 精度 | 中,可量化 KV、可限制长度 |
| 激活峰值 | 权重显存 × 5%~15% | batch、序列长度、CUDA Graph | 低,主要靠调 batch |
| 框架开销 | 固定 2~4GB | 进程、通信缓冲、图捕获 | 低 |
举个具体演算过程,方便你对着改。假设模型 FP8 权重约 95GB,卡是 4 张 48GB 共 192GB,扣掉框架开销 4GB、激活预留按 12GB 算,剩给 KV Cache 的预算是 192 - 95 - 4 - 12 = 81GB。按单 token 122KB 计,81GB 能撑大约 66 万个 token 的缓存总量。如果平均请求是 8K 上下文,理论并发上限约 80 路;如果平均 32K,就只有 20 路左右。这个换算出来的数字,才是你设置--max-model-len和--max-num-seqs的真实依据。
1.3 为什么“能加载”和“能跑满”是两回事
我见过太多情况是模型加载成功、健康检查通过、发一个请求也正常返回,但一上并发就开始排队甚至超时。根因几乎都是 KV Cache 的 block 分配策略在作祟。
vLLM 默认会按--gpu-memory-utilization去切分可用显存,把权重之外的部分尽量划给 KV Cache。这个参数设成 0.9 意味着允许占用 90% 显存,剩下的留给激活和系统。而--max-model-len决定了单请求最长能吃多少 token,它和--max-num-seqs之间存在隐性的资源竞争:你把单请求长度拉到 128K,框架就要为每个并发槽位预留足够长的缓存空间,实际可用并发数会被压得很低。
所以正确的顺序是:先定业务上真正需要的最大上下文长度,再反推能支撑多少并发,最后用压测验证。反过来做——先拍一个很大的并发数,再发现长请求 OOM——就是纯粹的返工。
2. 四条部署路线,各自的适用边界
同样一个模型,部署方式的选择空间其实比想象中大。我把实际用过的四条路线按“从验证到生产”的顺序列出来,每条都说清楚它解决什么问题、代价是什么。
2.1 路线一:单卡 FP8 直载,最小验证面
这条路线的目标只有一个:尽快确认权重完整、tokenizer 正常、输出质量符合预期,不考虑吞吐。
适用条件是单卡显存能装下 FP8 权重加一个很小的 KV 预算。如果装不下,就别硬试,直接跳到路线二,否则你会陷入“加载到 80% 报 OOM”的反复。
具体做法上,把--gpu-memory-utilization压到 0.85 左右,--max-model-len设成一个保守值(比如 8K),--max-num-seqs设成 4 或 8,先跑通再说。这个阶段不要开 CUDA Graph 之外的花哨特性,也不要接任何网关。
我自己的习惯是在这条路线里加一个小脚本,跑十几条覆盖不同任务类型的输入,把输出存下来当基线。后面换路线、换量化格式、升级框架版本时,重新跑一遍对比,能快速发现质量回退。这个基线集比任何 benchmark 数字都有用,因为它对的是你的真实业务分布。
这条路线的时间成本通常在一小时内,但它排除掉的变量最多——权重损坏、配置字段不匹配、驱动版本不对,这些问题都会在这一步暴露。
2.2 路线二:多卡张量并行,吞吐优先
当单卡装不下,或者单卡吞吐不够时,就进入张量并行(TP)阶段。核心参数是--tensor-parallel-size,它必须能整除模型的注意力头数,否则启动时会直接报错。
这里有个反直觉的经验:TP 不是越大越好。TP 每加一级,就要多做一次 all-reduce 通信,通信开销随卡数上升。在单机内用 NVLink 互联的情况下,TP=4 到 TP=8 通常是性价比区间;如果卡之间只能走 PCIe,TP 超过 4 之后的收益衰减非常明显,有时候 TP=8 的吞吐还打不过两组 TP=4 各跑一份。
我的一般做法是在目标硬件上做一次 TP 扫描:固定并发和输入长度,分别测 TP=2、4、8 的 tokens/s 和首 token 延迟,画个简单的对照表。多数情况下会看到一个明显的拐点,拐点之后再加卡就是浪费。
# 多卡张量并行启动骨架(参数按实际硬件调整) python -m vllm.entrypoints.openai.api_server \ --model /models/deepseek-v4.1-flash \ --served-model-name deepseek-v4.1-flash \ --tensor-parallel-size 4 \ --dtype bfloat16 \ --quantization fp8 \ --max-model-len 32768 \ --max-num-seqs 32 \ --gpu-memory-utilization 0.90 \ --enable-prefix-caching \ --port 8000要注意--dtype和--quantization的关系:前者描述计算精度,后者描述权重的存储与反量化方式。FP8 权重配 BF16 计算是常见组合,能拿到接近一半的权重显存节省,同时避免 FP8 计算在某些算子上的精度抖动。
2.3 路线三:量化压缩加单机多卡,成本敏感型
如果硬件预算卡得紧,又想上更大参数量的模型,量化就是唯一出路。常见格式有 INT4 的 GPTQ、AWQ,以及 FP8 的动态量化。三者的取舍逻辑不一样:
- AWQ对激活值敏感通道做保护,在小模型上质量保持较好,推理速度也不错,工程上最省心;
- GPTQ生态成熟、工具链全,但不同校准集出来的效果差异较大,需要自己验;
- FP8几乎无损,压缩比只有 2 倍,适合“差一点显存但不想牺牲质量”的场景。
量化的真实代价不在精度指标上,而在长尾输入上。基准测试集上的分数可能只掉零点几个点,但你自己业务里那些结构复杂、术语密集的输入,退化会明显得多。所以走这条路线的团队,一定要用路线一里攒的基线集做回归,而且要看生成结果的完整性和格式遵循情况,不能只看困惑度。
另一个容易被忽略的点是量化后的反量化开销。INT4 权重在计算前要反量化成 BF16,这会带来额外的算力消耗,在算力本来就紧张的卡上,吞吐提升可能没有预期那么大。实测下来,INT4 相比 FP8 的吞吐优势,在长序列场景会被摊薄不少。
2.4 路线四:容器化加推理网关,生产托管
前三条路线解决的是“怎么把模型跑起来”,这条解决的是“怎么让它稳定地对外提供服务”。
容器化的关键不是写 Dockerfile,而是把模型权重和镜像分离。权重动辄几十上百 GB,打进镜像会让拉取时间和存储成本失控。正确做法是把权重挂载成只读卷,镜像里只放运行时依赖,通过环境变量注入模型路径。这样升级框架版本时只需要换镜像,权重不用重拷。
网关层要处理的事情包括:请求排队与限流、多副本之间的负载均衡、超时与重试、流式输出的连接保持、以及 token 计量的埋点。这里有个细节值得单独说——流式请求的超时设置和普通请求不一样。一个 4K 输出的流式请求可能持续几十秒,如果网关按普通 HTTP 的 30 秒超时去掐,用户会在生成到一半时断流。正确做法是设置首字节超时加空闲超时,而不是总时长超时。
再往上一层是 PD 分离(Prefill 与 Decode 分到不同实例)。这个方案在长输入短输出的场景收益明显,因为 Prefill 是算力密集、Decode 是访存密集,混在一起会互相拖累。但它的复杂度也高一个量级:需要 KV Cache 的跨实例传输、需要两套实例的配比调优。我的建议是,日均请求量没到一定规模之前不要碰,先用统一实例把业务跑顺。
2.5 四条路线的横向对照
| 路线 | 硬件门槛 | 典型吞吐 | 落地成本 | 适用阶段 |
|---|---|---|---|---|
| 单卡 FP8 直载 | 单卡可容纳 FP8 权重 | 低 | 极低 | 验证、离线批处理 |
| 多卡张量并行 | 2~8 卡同机 | 中高 | 中 | 主力在线服务 |
| 量化加多卡 | 卡数可减少一半 | 中 | 中(含质量验证成本) | 预算受限的在线服务 |
| 容器化加网关 | 需要编排能力 | 取决于副本数 | 高 | 多团队共用、对外服务 |
选路线的时候别只看吞吐数字,要把“验证成本”和“运维成本”也算进去。量化路线看着省卡,但你得为质量回归付出人力;容器化路线看着重,但它是唯一能支撑多副本弹性伸缩的方案。
3. vLLM 启动命令:每个参数都不是随手写的
vLLM 的参数表很长,但真正影响部署成败的就那么十来个。这一节把常用参数按“必调、该调、看情况调”三档拆开讲。
3.1 最小可跑通的命令骨架
先给一个能直接抄的骨架,然后逐条解释:
python -m vllm.entrypoints.openai.api_server \ --model /models/deepseek-v4.1-flash \ --served-model-name deepseek-v4.1-flash \ --tokenizer /models/deepseek-v4.1-flash \ --trust-remote-code \ --dtype bfloat16 \ --quantization fp8 \ --tensor-parallel-size 4 \ --max-model-len 32768 \ --max-num-seqs 32 \ --gpu-memory-utilization 0.90 \ --kv-cache-dtype fp8 \ --enable-prefix-caching \ --enable-chunked-prefill \ --swap-space 8 \ --disable-log-requests \ --host 0.0.0.0 \ --port 8000这段命令在实际环境里大概率需要改两三个参数,但结构是完整的。特别注意--trust-remote-code,只在模型仓库确实带了自定义建模代码时才需要开,它本质上是在服务进程里执行模型目录下的 Python 代码,生产环境应该评估这个风险,最好是把自定义代码审计一遍再固化到内部镜像里。
3.2 关键参数逐条拆解
--gpu-memory-utilization是最容易被滥用的参数。它的含义是“允许框架占用单卡总显存的比例”,不是“KV Cache 占多少”。设成 0.95 看起来很激进很划算,但留给激活峰值和 CUDA 上下文的余量就只剩 5%,在长序列或者大 batch 时极易触发 OOM。我的经验值是:短请求高并发场景 0.85~0.90,长上下文场景 0.80~0.85。宁可少划一点给 KV Cache,也不要让进程在高峰期被打死。
--max-num-seqs控制同时在跑的序列数上限。它和--max-num-batched-tokens配合决定了一次前向能塞多少 token。这两个参数是吞吐和延迟的调节旋钮:调大吞吐上升但单请求延迟变差,调小反之。生产环境一般先按显存预算算出理论上限,再打七折设置,留出应对突发长请求的空间。
--max-model-len必须小于等于模型本身支持的最大长度,但它更大的作用在于限制 KV Cache 的单请求预留。把它设成业务实际需要的长度,而不是模型支持的最大长度,能显著提升可用并发。比如模型支持 128K,但你业务里 95% 的请求都在 16K 以内,那把上限设成 32K 是合理的,剩下的用拒绝策略或截断策略兜底。
--enable-chunked-prefill值得单独说。它把长请求的 Prefill 切成小块,和其他请求的 Decode 交错执行,能明显改善混合负载下的尾延迟。代价是稍微复杂一点的调度开销。只要你的业务里存在长输入和短输出混跑的情况,这个开关基本都该打开。
--kv-cache-dtype fp8是把 KV Cache 用 FP8 存储,显存直接减半,代价是极小的精度损失。在显存紧张时这是性价比最高的一个开关,实测对生成质量的影响远小于权重量化。
3.3 长上下文与 KV Cache 相关的调参
长上下文场景有一套单独的调参逻辑,和普通对话场景不一样。
第一,--max-num-seqs要显著调低。32K 上下文下单请求就要吃掉近 4GB 的 KV Cache,如果你还按 64 并发去设,框架会在启动时就把缓存池撑爆,或者运行中频繁触发抢占(preemption),表现为请求突然变慢甚至重算。
第二,关注抢占日志。vLLM 在 KV Cache 不足时会打印preemption相关信息,一旦出现就说明并发设高了或者长度设长了。这个信号比任何监控指标都直接。
第三,--swap-space的作用是把被抢占的序列换出到 CPU 内存。设大一点(8~16GB)能缓解抢占带来的重算,但换入换出的开销不小,它只是缓冲,不是解决方案。真正解决问题还是要回到降并发或降长度。
# 长上下文专用配置片段 --max-model-len 131072 \ --max-num-seqs 8 \ --max-num-batched-tokens 16384 \ --kv-cache-dtype fp8 \ --enable-chunked-prefill \ --swap-space 163.4 常见报错与对应处理
启动阶段最常见的几类报错,我整理成对照表,方便直接查:
| 报错关键词 | 根因 | 处理方式 |
|---|---|---|
No available memory for the cache blocks | KV Cache 预算不足 | 降max-model-len或降max-num-seqs,或调低gpu-memory-utilization让权重加载更宽松 |
The number of heads is not divisible | TP 数与注意力头数不整除 | 换 TP 值,选头数的因数 |
torch.OutOfMemoryError出现在加载阶段 | 权重加载峰值超限 | 加--load-format相关选项,或用 CPU 卸载过渡 |
Cannot find the model config | 路径或配置字段不匹配 | 检查 config 里的architectures字段与框架版本是否对得上 |
启动后卡在Capturing CUDA graphs | 图捕获阶段显存不足 | 降 batch 上限,或关闭部分图捕获 |
还有一类不算报错但很烦人的情况:进程启动成功,日志显示一切正常,但第一个请求要等很久才返回。这通常是编译或图捕获在首次请求时懒加载导致的,属于正常现象,但会让健康检查误判。解决办法是把健康检查的打点放在真正发过一次 warmup 请求之后。
4. SGLang 侧:RadixAttention 在什么场景下真的赚
SGLang 和 vLLM 的核心差异在于它的 RadixAttention——用基数树管理 KV Cache,让多个请求共享相同的前缀缓存。这个机制在特定场景下收益极大,在另一些场景下几乎没感觉。
4.1 启动命令与目录结构
python -m sglang.launch_server \ --model-path /models/deepseek-v4.1-flash \ --served-model-name deepseek-v4.1-flash \ --tp-size 4 \ --dtype bfloat16 \ --quantization fp8 \ --context-length 32768 \ --mem-fraction-static 0.85 \ --max-running-requests 32 \ --chunked-prefill-size 8192 \ --schedule-policy lpm \ --enable-cache-report \ --host 0.0.0.0 \ --port 30000参数命名和 vLLM 不完全一样,需要对应着看:--tp-size对应--tensor-parallel-size,--context-length对应--max-model-len,--mem-fraction-static对应--gpu-memory-utilization,--max-running-requests对应--max-num-seqs。习惯了一套之后换另一套,最容易错的就是这几个名字。
--schedule-policy是 SGLang 的特色参数。lpm是最长前缀匹配优先,它会把共享前缀多的请求排在一起执行,最大化缓存命中。这个策略在多轮对话、few-shot 模板调用、RAG 固定检索头这类场景下效果显著。代价是可能让某些短请求排队变久,所以如果你的业务对尾延迟极度敏感,要实测一下再决定。
4.2 前缀缓存的收益边界
RadixAttention 的收益完全取决于请求之间的前缀重合度。说人话就是:如果一百个请求开头都是同一段系统提示词,那这段提示词的 Prefill 只需要算一次,后面九十九次直接命中缓存。如果每个请求开头都不一样,那这个机制就退化成普通缓存,收益接近于零。
所以判断要不要用 SGLang,先问自己三个问题:
- 系统提示词是不是固定的、且足够长(比如超过 500 token)?
- 是不是多轮对话,后续轮次能复用前面的上下文?
- 是不是同一个文档被反复提问(RAG 场景)?
三个里有两个答案是“是”,SGLang 基本稳赚。如果一个都不是,那用 vLLM 更省心,毕竟生态和周边工具更成熟。
--enable-cache-report这个开关建议在压测阶段打开,它会输出缓存命中率。我一般会把这个数字当成选型的关键依据——命中率低于 20% 就说明这个业务形态不适合前缀缓存路线。
4.3 vLLM 与 SGLang 的选型判断
两套框架并不是互斥的,很多团队实际上是在不同业务线上分别用。我整理的判断逻辑如下:
| 判断维度 | 偏向 vLLM | 偏向 SGLang |
|---|---|---|
| 请求前缀重合度 | 低,每请求独立 | 高,固定模板或多轮 |
| 周边生态需求 | 需要丰富插件、指标、集成 | 相对简单 |
| 调度策略需求 | 标准 FIFO 够用 | 需要前缀感知调度 |
| 团队熟悉度 | 已有 vLLM 经验 | 可以投入学习成本 |
| 长上下文占比 | 中等 | 高且前缀可复用 |
有一个实践中的注意点:SGLang 的缓存是内存级的、有容量上限的,树结构会随着请求多样性增长而膨胀,需要设置合理的容量。如果放任不管,在长时间运行后可能出现缓存管理本身的开销上升。压测的时候要跑够长时间,别只测前十分钟。
5. 上线前的压测与调优闭环
命令跑通只是起点,真正决定服务质量的是压测环节。这一节讲我常用的压测方法和调优顺序。
5.1 压测指标到底该看哪几个
很多人压测只看 tokens/s,这个指标单独看没有意义。至少要同时看四个:
- TTFT(首 token 延迟):用户感知最强的指标,决定“点了发送之后多久看到字”;
- TPOT(每 token 输出时间):决定输出速度是否流畅,超过某个阈值用户会觉得卡;
- 吞吐(tokens/s):决定单位硬件的服务能力,影响成本;
- P99 尾延迟:决定最差体验,也是最容易被平均值掩盖的问题。
这四个指标之间存在明确的权衡关系,不存在同时最优的配置。你要做的是根据业务形态确定优先级:交互式对话场景 TTFT 优先,离线批处理场景吞吐优先。
压测工具上,我用过一个自己写的小脚本,按指定并发和输入长度分布持续发请求,记录上面四个指标。比现成的 benchmark 工具麻烦一点,但它能复现你真实的输入长度分布——而输入长度分布恰恰是影响结果最大的变量。
5.2 并发扫描找出系统的拐点
单点测试没有价值,要做并发扫描。从并发 1 开始,按 2、4、8、16、32、64 逐级加压,每级跑够稳定时间,记录四个指标。
扫描出来通常会看到这样的曲线:并发上升初期,吞吐线性增长、TTFT 基本不变;到某个点之后,吞吐增长放缓甚至持平,TTFT 开始明显上扬。这个点就是系统的拐点。拐点之前的并发是安全区,拐点之后每加一路并发都是在牺牲延迟换微乎其微的吞吐。
我一般的做法是把生产环境的并发上限设在拐点的 70% 左右,留出应对突发流量的余量。这个比例不是拍脑袋,是因为排队系统在接近饱和时延迟会非线性上升,留余量比追求极致利用率更划算。
5.3 调优的先后顺序不能乱
调参最忌讳东一榔头西一棒子。我总结的顺序是:
第一步,固定模型和精度,先把max-model-len按业务需求定死。这个参数一变,后面所有结论都作废。
第二步,调gpu-memory-utilization,找到不 OOM 的前提下能给出的最大 KV Cache 预算。
第三步,调并发上限,通过扫描找拐点。
第四步,开chunked-prefill、prefix-caching这类特性,看指标变化,有收益就留,没收益就关。
第五步,最后才考虑量化和换框架。前面四步做完还没达到目标,再动这两个大变量。
这个顺序的逻辑是:前面的参数影响的是资源盘子有多大,后面的特性影响的是盘子里的东西怎么分配。先定盘子再分配,效率最高。反过来先调特性再改盘子,等于白调。
6. 踩坑实录:几类反复出现的问题
这一节列的都是我在实际部署中反复遇到、但官方文档里基本不会写的问题。每个都给出排查链路,方便你按图索骥。
6.1 OOM 的三种形态与区分方法
OOM 不是一个问题,是三种完全不同的问题,处理方式天差地别。
形态一:加载期 OOM。特征是进程在打印模型加载进度时挂掉,通常发生在加载到 70%~90% 的位置。根因是权重加载时的临时峰值——框架可能先把权重读进 CPU 内存再逐层搬到 GPU,中间有短暂的显存占用叠加。处理方式是降低gpu-memory-utilization给加载留余量,或者用分片加载选项。
形态二:启动后立即 OOM。特征是服务起来了,日志显示Application startup complete,但紧接着就崩溃。根因通常是 CUDA Graph 捕获阶段申请的显存超过了预算。处理方式是降低 batch 相关的上限,或者减少图捕获的规模。
形态三:运行中随机 OOM。特征最隐蔽,跑几个小时甚至一天后才崩,而且崩的时候并发并不高。根因往往是显存碎片化——长时间运行后,KV Cache 的块分配产生了大量碎片,虽然总空闲显存够,但没有连续的大块可用。处理方式包括:定期重启(最土但最有效)、调整块大小、或者用支持分页管理的框架版本。
区分这三种形态,看崩溃发生的时机就够了。加载期崩是形态一,启动后秒崩是形态二,跑了很久才崩是形态三。
6.2 启动卡住不动,日志停在某一行
这个问题的排查链路比较长,我按经验排序:
先看是不是在下载或校验权重。有些框架会在启动时做完整性校验,大模型这个步骤可能耗时几分钟,表现就是日志不动。用strace或者看磁盘 IO 能确认。
再看是不是在编译算子。部分框架会针对当前硬件做算子编译,第一次启动特别慢,第二次就快了。如果有编译缓存目录,确认它是不是可写。
然后看是不是 NCCL 通信卡住。多卡场景下,如果卡之间网络配置有问题,初始化会长时间无响应。这时候看nvidia-smi上的进程状态和 GPU 利用率,如果所有卡都停在某个固定值不动,基本就是通信问题。
最后看是不是资源竞争。同一台机器上如果还有别的进程占着显存或端口,也会表现成卡住。这种情况最冤枉,但也最常见。
6.3 版本与算子不匹配的隐蔽表现
这类问题最恶心的地方在于它不一定报错,可能只是输出质量变差。
我遇到过一次:升级框架版本后,服务一切正常,但生成结果的格式遵循度明显下降,该输出 JSON 的时候经常多带一段解释。排查了两天才发现是新版本对某个注意力算子的实现变了,在当前精度组合下有数值偏差。回退版本后问题消失。
所以升级框架版本这件事,一定要配回归测试。我现在的做法是维护一个几十条的固定测试集,覆盖格式遵循、长文生成、多轮上下文、边界输入这几类,每次升级前后各跑一遍,对比差异。这套流程看着笨,但它拦下过好几次“看起来正常实际有问题”的升级。
另外提醒一点:量化格式和框架版本之间也有耦合。某些量化格式需要特定版本的推理内核支持,版本不匹配时可能加载成功但推理结果完全错乱。判断方法很简单,用路线一里的基线集跑一遍,如果输出是乱码或者重复循环,基本就是这类问题。
6.4 输出重复、循环、或者突然截断
输出重复通常和采样参数有关。temperature设太低(接近 0)、repetition_penalty没开,长文本生成就容易陷入循环。这个不是部署问题,但经常被误认为是部署问题。
如果是突然截断,先确认是不是撞到了max_tokens或者max-model-len。这两个限制在框架层面和网关层面可能各有一套,容易只改了一处。
还有一种更隐蔽的截断:流式输出时,如果网关或客户端设了读超时,连接会在生成中途被断开,表现就是输出到一半没了,但服务端日志显示正常完成。这种问题查起来很费劲,因为服务端看不到任何异常。排查方法是看客户端侧的连接关闭时间和生成 token 数是否对得上。
7. 我个人的几条经验补充
部署这件事,工具和参数都在其次,真正拉开差距的是对失败模式的熟悉程度。同样一个 OOM,有经验的人看一眼崩溃时机就知道该调哪个参数,没经验的人要试半天。这种熟悉度只能靠踩坑积累,但可以加速——方法就是每次出问题后,把“现象、排查路径、根因、修复方式”记下来,形成自己的故障库。我现在的故障库里有四十多条记录,新机器新环境部署时基本能覆盖八成的问题。
另一个体会是,不要在没做基线的情况下换方案。量化、换框架、升版本,这三件事任何一个都可能引入质量问题,如果你手上没有基线集做对比,就只能靠感觉判断“好像变差了”,这种判断既不可靠也无法说服别人。花半天时间攒一套测试集,是投入产出比最高的一件事。
最后一个技巧,关于显存估算的验证:启动服务后,用nvidia-smi观察稳定运行时的显存占用,和你的静态估算对比。如果实际占用明显高于估算,说明激活峰值或者框架开销比预期大,要相应调低并发预期。这个动作只要一分钟,但能让你对整机的余量有个准确认知,比任何纸面计算都可靠。