服务一起来就报显存不足,通常只有三个来源:权重本身没切分、单卡放不下;KV 缓存按上下文长度和并发预留,把剩余显存吃光;单独看都没超,但多个请求同时占用把峰值抬上去。这三类原因对应的处理动作完全不同——先改量化、先砍上下文、先降并发,改错方向只会白折腾一轮。可行的做法是按“日志定阶段 → 只加载权重 → 最小上下文 → 并发降到 1”的顺序做单变量复测,每一步只动一个参数,并记录峰值显存与是否仍然 OOM。
如果 OOM 发生在权重加载阶段、进程还没开始监听端口,问题在权重切分或单卡容量;如果日志已经出现服务就绪、在首条请求之后才报错,优先怀疑 KV 缓存预算和并发叠加。可以按这个顺序定位:只加载权重不跑推理,量出权重常驻占用;把上下文长度调到最小复测;把并发降到 1 复测。每步只改一个变量并记录峰值显存。边界:调小上下文和并发会限制可用场景,降精度会影响输出质量,具体取舍需要结合部署环境确认。
在日志里区分是加载阶段报错还是首条请求报错
先确认报错发生在哪一行日志,这一步决定了后面要不要动权重加载方式。加载阶段的特征是:还在打印权重加载、分片、显存分配相关的行,服务端口没有出现监听日志,进程从启动到报错通常在几秒到几十秒内。这类 OOM 说明权重常驻显存就已经不够。
首条请求阶段的特征是:已经打印服务就绪、监听端口,随后在收到请求、进入前向计算时才报错,堆栈里常出现 attention、kv、cache、forward 这类词。这类 OOM 说明权重放得下,是运行时缓存和请求占用的空间超了预算。多请求场景还要看报错的时机:单请求正常、并发上来才报,方向是叠加效应。
| 日志或报错特征 | 出现位置 | 优先怀疑 |
|---|---|---|
| loading weights、shard、allocating buffer 附近报 out of memory | 启动后、端口未监听 | 权重未切分或单卡容量不足 |
| 服务已就绪,请求进入前向计算时报 OOM | 请求处理路径 | KV 缓存预算、上下文长度 |
| 单请求正常,并发上来后报 OOM | 多个请求同时在线 | 并发叠加 |
| 报错提到 pinned memory 或 host memory 不足 | 加载或换入换出阶段 | 主机内存或卸载策略,不一定是纯显存问题 |
只加载权重、不跑推理,观察显存占用
把权重部分单独量出来,才能知道剩下的空间够不够开缓存。思路是用只加载权重、不接受请求的方式启动,参数名以该实现的 `--help` 为准,不同版本可能叫 load-only、no-serve、dry-run 之类。
# 通用骨架:只加载权重,不对外服务
serve `--model` naive-n0.5-flash \
`--load-only` \
`--device` cuda:0
加载过程中另开一个终端观察显存,读数取加载完成后的稳定值,不要取抖动峰值:
nvidia-smi `--query-gpu`=memory.used,memory.total `--format`=csv -l 2
# AMD 环境可用
rocm-smi `--showmeminfo` vram
判断方式很直接:如果稳定占用已经接近或超过单卡可用容量,权重没切分就是主因,后面调上下文和并发都不会有效果;如果加载后还剩比较宽裕的空间,权重这一关就过了,问题落在运行时缓存。多卡部署要逐卡看,只有卡 0 被占满、其他卡很空,通常说明切分没生效,或者只做了某一层的切分。
把上下文长度调到最小再跑一次
KV 缓存大小随上下文长度增长,这是最容易被忽略的一项预算。参数一般叫 max_model_len、max_seq_len、context_length 或 n_ctx,位置可能在启动命令、配置文件或单次推理参数里,具体以该实现的配置项为准。把它改到最小值——刚好覆盖系统提示加一次短问答即可,其他参数保持不变再跑一次。
| 复测记录字段 | 说明 |
|---|---|
| max_model_len | 本次设置的上下文长度 |
| max_num_seqs | 并发上限,本次保持不变 |
| gpu_memory_utilization | 显存预留比例,若该实现提供 |
| peak_vram | 运行期间峰值显存 |
| oom / error_stage | 是否仍然 OOM,报错在加载阶段还是请求阶段 |
结果解读:如果最小上下文下不再 OOM,且峰值显存明显低于设备容量,KV 缓存就是关键变量,接下来要按目标上下文长度估算每个并发请求需要的缓存;如果最小上下文下仍然 OOM,说明缓存不是主因,回到权重占用或并发叠加上继续查。上下文调到最小只是定位手段,会限制可用输入长度,不要把它直接当最终配置。
把并发降到 1 复测以排除叠加效应
并发参数通常叫 max_num_seqs、max_batch_size、max_concurrency,也可能体现为连续批处理开关,位置在启动参数或服务配置里。复测要保持上下文长度与上一次一致,只把并发上限设成 1,然后发一条请求。
# 对照复测:除并发外,其余参数与上一次相同
serve `--model` naive-n0.5-flash \
`--max-model-len` 2048 \
`--max-num-seqs` 1
对照读法很直接:单并发能跑通、恢复并发就 OOM,说明每个请求自身的占用没问题,是多请求峰值叠加把显存顶穿;单并发仍然 OOM,说明单个请求的最小占用就已经超了,问题在权重或单请求缓存,而不是叠加。这里有一个容易误判的点:部分实现会在启动时按最大并发预先预留缓存,并发参数一改,加载阶段的占用也会跟着变,因此要把加载后占用和请求后占用分开记录。
按结论选择降精度、切分权重或限制上下文
定位完成后,手段要和结论对应,否则只是把溢出点从一处挪到另一处。降精度和切分权重压缩的是权重常驻空间,限制上下文和并发压缩的是运行时缓存空间,这两组不是同一类问题。
| 定位结论 | 处置手段 | 代价 | 验证方式 |
|---|---|---|---|
| 权重加载阶段就超 | 切分权重到多卡,或换用更低精度权重 | 多卡要处理通信与切分开销;降精度可能影响输出质量 | 重复“只加载权重”的观测,看占用是否落到容量以内 |
| 权重够、KV 缓存超 | 限制上下文长度、限制并发上限,或调整显存预留比例 | 可用输入长度和并发吞吐被压缩,长上下文场景受限 | 在目标上下文长度下复测,记录峰值显存与是否 OOM |
| 单请求可行、并发叠加超 | 限制并发上限,或加卡分摊 | 并发上限直接决定同时在线请求数;加卡增加成本 | 从并发 1 逐步加档到上限,逐档记录是否稳定 |
稳妥做法是一次只上一种手段,改完就用前面那套复测记录跑一遍,确认 OOM 消失且峰值显存留有安全余量,再去调效果和速度相关的参数。如果服务是靠 swap 或缓存回退勉强跑起来的,那只是把问题推后,不要把它当成容量方案。