Hy3 单卡跑得动吗 / 显存先卡权重还是先卡 KV 缓存?

文章导读
单卡能不能跑 Hy3 这一类模型,通常取决于两笔账之和:权重常驻显存,加上 KV 缓存的峰值占用。空载就报显存不足,说明权重这头已经压到上限;加载正常、一上长上下文或一加并发就崩,通常是 KV 缓存先把显存吃光。判断方法不复杂:模型加载完成后采一次显存,发一次固定长度请求后再采一次,两次差值大致对应缓存与运行时开销,据此决定是先降量化档位还是先降上下文与并发。
📋 目录
  1. A 先量空载权重占用,再量加了一次请求后的增量
  2. B 用上下文长度和层数写出 KV 缓存估算骨架
  3. C 在配置里分别改上下文长度和量化档位做对照
  4. D 从加载失败的日志行判断是权重还是缓存超限
  5. E 按测量结果决定是先降档位还是先降并发
A A

单卡能不能跑 Hy3 这一类模型,通常取决于两笔账之和:权重常驻显存,加上 KV 缓存的峰值占用。空载就报显存不足,说明权重这头已经压到上限;加载正常、一上长上下文或一加并发就崩,通常是 KV 缓存先把显存吃光。判断方法不复杂:模型加载完成后采一次显存,发一次固定长度请求后再采一次,两次差值大致对应缓存与运行时开销,据此决定是先降量化档位还是先降上下文与并发。

适用场景:单卡部署,加载阶段或运行阶段报 CUDA out of memory。操作动作:加载完成时采一次显存,发出一次固定 prompt、固定 max_tokens 的请求后再采一次,把空载值与增量分开看。验证方式:对照模型 config.json 里的层数、KV 头数、head_dim 估算 KV 量级,看是否与增量吻合。风险边界:多数推理框架启动时就预留 KV 块池,空载值里已含这部分,需结合框架日志中的 KV 块数一起判断,最终以本机复测为准。

先量空载权重占用,再量加了一次请求后的增量

第一个采样点要卡在“模型已加载完成、但还没有任何请求”这一刻。用 nvidia-smi 看进程级占用,同时看框架启动日志里是否已经打印了 KV 块池大小。

nvidia-smi `--query-gpu`=memory.used,memory.total `--format`=csv,noheader

# 也可以在引擎进程里挂一个调试入口
import torch
print('allocated GiB:', torch.cuda.memory_allocated() / 2**30)
print('reserved  GiB:', torch.cuda.memory_reserved() / 2**30)

然后发一次可控请求:prompt 短、max_tokens 固定,方便重复。端口和模型名按你的部署替换。

import requests
r = requests.post('http://127.0.0.1:8000/v1/completions',
    json={'model': 'hy3', 'prompt': '今天天气不错', 'max_tokens': 512, 'temperature': 0})
print(r.json()['usage'])

请求结束后再采一次同样的指标,如果有 max_memory_allocated 这类峰值计数器,优先读峰值而不是瞬时值。两次差值大致对应这一条请求的 KV 缓存、激活和临时 workspace:差值随输出长度线性上涨,说明缓存开销可观;增量很小但空载值已经贴着上限,瓶颈就在权重。

有一个容易误判的地方:不少框架在启动阶段就按 gpu_memory_utilization 把剩余显存划成 KV 块池,所以 nvidia-smi 看到的空载值里可能已经含了 KV 池,后续增量显得很小。这种情况下改看日志里的 KV cache blocks 或可容纳 token 数,用块数乘每块字节数反推 KV 池规模,再和权重占用对比。

用上下文长度和层数写出 KV 缓存估算骨架

KV 缓存的通用算式是:层数 × KV 头数 × head_dim × 序列长度 × 每元素字节 × 2(K 和 V 各一份),并发请求再乘 batch。各字段从模型目录的 config.json 读取,字段名不同模型可能略有差异,以本地文件为准。

  • num_hidden_layers → 层数
  • num_key_value_heads → GQA/MQA 的 KV 头数;没有这一项时退回 num_attention_heads
  • head_dim,或 hidden_size ÷ num_attention_heads → 每个头的维度
  • torch_dtype、quantization_config → 决定权重精度;KV 缓存的字节数看框架里 kv cache dtype 的配置,fp16/bf16 按 2 字节算,fp8/int8 按 1 字节算
  • max_model_len 或 max_seq_len → 序列长度上限
import json
cfg = json.load(open('config.json'))
L   = cfg['num_hidden_layers']
kvh = cfg.get('num_key_value_heads', cfg['num_attention_heads'])
hd  = cfg.get('head_dim', cfg['hidden_size'] // cfg['num_attention_heads'])
bpe = 2  # KV 缓存每元素字节:fp16/bf16 为 2,fp8/int8 为 1

def kv_bytes(seq_len, batch=1):
    return L * kvh * hd * seq_len * bpe * 2 * batch

for n, b in ((4096, 1), (32768, 1), (32768, 8)):
    print(n, 'batch', b, '%.2f GiB' % (kv_bytes(n, b) / 2**30))

算出来的是理论下界,实际占用通常更高,因为分页块对齐、前缀缓存、激活和临时 buffer 都会额外占一些。它的用途是判断量级:同样 32k 上下文,估算从几 GiB 涨到几十 GiB,就说明只靠压并发很难救回这个长度。把估算值和第 1 节的增量对照,两个数量级对得上,判断才站得住。

在配置里分别改上下文长度和量化档位做对照

单变量对照的意思是每次只改一处,其余保持不动,重启后按同样方式加载、发同一条请求。

  1. 固定量化档位不变,把 max_model_len(或框架里的 max_seq_len、上下文长度上限)从目标值往下调一两档,每改一档重启一次,看能否加载成功。
  2. 再把上下文固定在小值不变,把量化档位往压缩更少的方向恢复,同样逐档重启,看还能不能加载。
  3. 前两步做完之后再单独动并发相关项:max_num_seqs、max_num_batched_tokens、gpu_memory_utilization。这三项决定 KV 块池留多大,属于最后一步微调。

每次实验记录三个观测值,缺一个都会让下一档变得没方向:

Hy3 单卡跑得动吗 / 显存先卡权重还是先卡 KV 缓存?
  • 加载是否成功:进程有没有在权重绑定阶段退出,还是能正常起服务。
  • 首 token 延迟:同一条 prompt 的 TTFT,记录下来,不要凭感觉估。
  • 显存峰值:nvidia-smi 轮询或框架指标里的最大显存,取整次请求过程的最大值。

降上下文能加载、降量化也能加载,说明两侧都紧,优先选代价更小的那一侧;只有降量化有效,瓶颈在权重;只有降上下文有效,瓶颈在 KV 缓存。

从加载失败的日志行判断是权重还是缓存超限

两类失败的时机不同,日志位置也不同,看关键词比反复试参数更快。

权重超限通常发生在加载、初始化阶段,请求还没进来:

[rank0]: torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB ...
[rank0]: RuntimeError: CUDA error: out of memory
# 关键特征:报错出现在 loading checkpoint shards、权重绑定阶段,没有任何 request 日志

对应处置:降低量化档位、换更小的权重切分或张量并行度,必要时换更小参数规模的模型。这类失败重启几次不会自己变好。

缓存超限通常出现在运行期,服务已经能起来:

ValueError: The model's max seq len (32768) is larger than the maximum number of tokens
that can be stored in KV cache (12288). ...

# 或者
CUDA out of memory. Tried to allocate ... during forward,且报错前已处理若干请求
# 关键特征:报错前有 request 日志,长度或并发接近设定上限

对应处置:先降 max_model_len,再降 max_num_seqs,把 gpu_memory_utilization 的余量留大一些,或启用 KV 缓存量化、chunked prefill 这类降低缓存占用的选项。提示文案和数字随框架版本变化,认关键词即可:max seq len、KV cache、tokens 容量这类字眼指向缓存;loading、checkpoint、weight 这类字眼指向权重。

按测量结果决定是先降档位还是先降并发

把两次采样和单变量对照的结论对到下面这张表,先选一个方向动手,不要两边同时改,否则下一次又分不清是哪边起的作用。

增量主要来自权重增量主要来自 KV 缓存
现象:空载已接近上限,或加载阶段就 OOM,增量占比小现象:空载正常,加长上下文或加并发后运行中 OOM
动作:先降权重精度档位(fp16/bf16 → fp8/int8/int4),或改权重切分与张量并行度,必要时换更小参数规模的模型动作:先降 max_model_len 和单条请求长度上限,再降 max_num_seqs 并发,最后才考虑 KV 量化、chunked prefill
代价:量化可能影响输出稳定性和长尾任务表现,需要用业务样例对比;加载时间也可能变化代价:上下文变短会截断长文档,并发下降会让排队时间变长
验证:同一条长上下文请求能跑完,显存峰值低于上限且首 token 延迟可接受验证:同一并发下不再 OOM,首 token 延迟没有明显变差

如果增量主要来自权重、但业务必须保留长上下文,方向是压权重而不是压上下文;如果增量主要来自 KV 缓存、又必须保留并发,通常先压单条长度、保留并发,比反过来体验更稳。每调一档都用同一条 prompt、同一组并发复测加载是否成功、首 token 延迟和显存峰值,确认都在可接受范围内再定下一档。