【Bug已解决】Out of Resource / block size error Kimi K2.7 on SM120 Blackwell with kv_cache_dtype fp8 解决方案
一、现象长什么样
在 SM120 架构的 Blackwell GPU(如 B200 / B300 类)上,用 vLLM 跑 Kimi K2.7,并把 KV 缓存数据类型设为fp8(--kv-cache-dtype fp8)时,启动或刚开始推理就报两类错误之一:
Out of Resource: KV cache allocation failed on SM120 (Blackwell) RuntimeError: block size 16 is not supported for fp8 KV cache on this device或者更笼统:
Out of Resource / block size error Kimi K2.7 on SM120 Blackwell with kv_cache_dtype fp8几个关键特征,帮助你判断是不是同一个坑:
- 报错明确提到Out of Resource / block size,且上下文是SM120 / Blackwell / fp8 KV cache。
- 只要把
--kv-cache-dtype改回默认的auto(通常是 fp16/bf16),错误立刻消失,模型能正常跑——说明问题出在「fp8 KV 缓存」这一特定路径,而非模型或权重。 - 调小
block_size(比如从 16 调到 8)有时能缓解,但换个--gpu-memory-utilization又复发,说明是「fp8 块大小约束」与「显存预算」共同作用。 - 同样的配置在老架构(如 SM90 / H100)上能跑,一到 SM120 Blackwell 就挂,说明 fp8 KV 缓存在 Blackwell 上有额外的块大小/对齐要求。
二、背景
要理解这个误报,得先搞清楚「fp8 KV 缓存」在 vLLM 里到底意味着什么,以及为什么它在 Blackwell 上更挑剔。
KV 缓存是什么:每个序列的 K/V 张量按block_size(每块 token 数,常见 16)切成块,所有块共享一块连续显存池。num_blocks由可用显存和单块大小决定。单块大小 =block_size × num_kv_heads × head_dim × dtype_bytes × 2(K和V)。
fp8 KV 缓存的意义:把 K/V 从 fp16(2 字节)压成 fp8(1 字节),单块显存减半,能塞下更多序列、提升吞吐。代价是精度略降,对多数模型可接受。
为什么 Blackwell(SM120)更挑剔:fp8 在不同架构上的实现细节不同。SM120 引入了新的 fp8 指令与张量核心布局,vLLM 在 Blackwell 上对 fp8 KV 缓存的「块大小」往往有更严格的对齐约束——某些block_size值在 SM120 上无法被 fp8 内核高效处理,或需要特定的块大小才能满足张量核心的 shape 要求。如果传入的block_size不满足,内核要么拒绝(block size error),要么为了对齐偷偷 padding 导致实际需要的显存远超预算(Out of Resource)。
还有一层:Kimi K2.7 是 MoE 模型,num_kv_heads与head_dim的组合,乘以 fp8 的单块尺寸,再乘以num_blocks,在 Blackwell 的显存颗粒布局下,可能因为对齐而比 H100 上「膨胀」出一块额外的 padding。当gpu-memory-utilization设得偏高时,这点膨胀就足以让分配器报 Out of Resource。
三、根因
根因有两支,常常同时出现:
block_size不满足 SM120 上 fp8 KV 缓存的对齐约束:vLLM 在 Blackwell 上对 fp8 块大小有最小/推荐值(往往要求block_size是某个数的倍数,或必须取特定值如 16/32),你传的block_size落在「不支持区间」,内核直接拒绝 →block size error。- fp8 块对齐 padding 撑爆显存预算:即使
block_size被接受,SM120 的 fp8 块因为张量核心对齐,单块实际占用比理论block_size × dtype_bytes大(多了 padding)。当分配器按「理论尺寸 × num_blocks」预留显存、实际却要更多时,就会Out of Resource。
核心矛盾:fp8 KV 缓存在 Blackwell 上的「真实单块成本」与「分配器预估的单块成本」不一致——要么因为block_size不被支持直接拒,要么因为对齐 padding 让预估失准而超预算。而错误信息把两者都笼统叫成 "Out of Resource / block size error",没有告诉你是「块大小不支持」还是「显存不够」。
四、最小可运行复现
下面用纯 Python 模拟「fp8 块对齐 padding 导致预估显存失准」的情形,不依赖 GPU:
# reproduce_kvcache_fp8.py # 复现:SM120 上 fp8 KV 块因对齐 padding 实际占用 > 预估,导致 OOR BLOCK_SIZE = 16 NUM_KV_HEADS = 64 HEAD_DIM = 128 def theoretical_block_bytes(dtype_bytes): return BLOCK_SIZE * NUM_KV_HEADS * HEAD_DIM * dtype_bytes * 2 # K+V def real_block_bytes_sm120(dtype_bytes, align=256): """SM120: fp8 块需按 align 字节对齐,不足补 padding。""" raw = BLOCK_SIZE * NUM_KV_HEADS * HEAD_DIM * dtype_bytes * 2 return ((raw + align - 1) // align) * align def plan(num_blocks, dtype_bytes, device="sm120"): if device == "sm120" and dtype_bytes == 1: # fp8 on Blackwell per = real_block_bytes_sm120(dtype_bytes) else: per = theoretical_block_bytes(dtype_bytes) needed = per * num_blocks budget = 6 * 1024**3 # 假设 6GB 预算 return needed, budget, needed > budget if __name__ == "__main__": # 同样 num_blocks,fp16 vs fp8(SM120) n = 40000 need_fp16, bud, oor_fp16 = plan(n, 2, "sm90") need_fp8, _, oor_fp8 = plan(n, 1, "sm120") print(f"fp16(H100) 需要 {need_fp16/1024**3:.2f}GB, OOR={oor_fp16}") print(f"fp8(SM120) 需要 {need_fp8/1024**3:.2f}GB, OOR={oor_fp8}") # 若分配器按理论 fp8(无padding)预留,就会低估 -> OOR运行后你会看到:fp8 在 SM120 上因为 padding,实际占用比「理论 fp8」大,若分配器按理论值预留显存,就会低估并触发 Out of Resource。
五、解决方案(第一层:最小直接修复)
最小修复两招,按需取用:
招式 A——把block_size调到 SM120 支持的合法值:
# fix_layer1_blocksize.py def supported_block_size_sm120(requested: int) -> int: """Blackwell SM120 fp8 KV 缓存只支持 16/32;否则回退到最近合法值。""" allowed = (16, 32) if requested in allowed: return requested # 选不小于 requested 的最小合法值;若超过 32 则取 32 for a in allowed: if a >= requested: return a return allowed[-1] if __name__ == "__main__": for bs in (8, 16, 20, 32, 64): print(f"requested={bs} -> use {supported_block_size_sm120(bs)}")招式 B——降低gpu-memory-utilization,给 fp8 对齐 padding 留余量:
# fix_layer1_util.py def safe_utilization(theoretical_util: float) -> float: """fp8 在 SM120 有对齐膨胀,预留 10% 余量。""" return max(0.5, theoretical_util - 0.10) if __name__ == "__main__": print("原 0.90 -> 建议", safe_utilization(0.90)) # 0.80这两招不动模型,只调启动参数,能最快让 Kimi K2.7 在 Blackwell 上用 fp8 KV 跑起来。
六、解决方案(第二层:结构性改进)
把「KV 缓存规划」做成自适应模块,根据设备架构 + dtype 自动选block_size、算真实显存、并给出回退建议:
# fix_layer2_planner.py from dataclasses import dataclass @dataclass class DeviceProfile: arch: str # "sm90", "sm120" fp8_block_align: int # SM120 上 fp8 块对齐字节 fp8_allowed_block_sizes: tuple @classmethod def for_arch(cls, arch: str) -> "DeviceProfile": if arch == "sm120": return cls("sm120", fp8_block_align=256, fp8_allowed_block_sizes=(16, 32)) return cls("sm90", fp8_block_align=1, fp8_allowed_block_sizes=(8, 16, 32)) def real_block_bytes(profile: DeviceProfile, block_size, num_kv_heads, head_dim, dtype_bytes): raw = block_size * num_kv_heads * head_dim * dtype_bytes * 2 if dtype_bytes == 1 and profile.fp8_block_align > 1: a = profile.fp8_block_align return ((raw + a - 1) // a) * a return raw def plan_kv_cache(profile, block_size, num_kv_heads, head_dim, dtype_bytes, budget_bytes, utilization): if dtype_bytes == 1 and block_size not in profile.fp8_allowed_block_sizes: block_size = max(profile.fp8_allowed_block_sizes) # 自动回退到最大合法值 per = real_block_bytes(profile, block_size, num_kv_heads, head_dim, dtype_bytes) usable = int(budget_bytes * utilization) num_blocks = usable // per return { "block_size": block_size, "per_block_bytes": per, "num_blocks": num_blocks, "estimated_bytes": per * num_blocks, "fits": per * num_blocks <= usable, } if __name__ == "__main__": prof = DeviceProfile.for_arch("sm120") res = plan_kv_cache(prof, block_size=16, num_kv_heads=64, head_dim=128, dtype_bytes=1, budget_bytes=6 * 1024**3, utilization=0.80) print(res)这样:换架构(sm90↔sm120)自动适配,block_size非法自动回退,显存按「真实对齐后」的尺寸预留,不再低估。
七、解决方案(第三层:断言 / CI 守护)
把「fp8 块大小合法性 + 显存预估」钉进断言和 CI:
# fix_layer3_guard.py # ---- pytest 用例,进 CI ---- def test_fp8_blocksize_legal_on_sm120(): from fix_layer2_planner import DeviceProfile, plan_kv_cache prof = DeviceProfile.for_arch("sm120") for bs in (8, 20, 64): r = plan_kv_cache(prof, bs, 64, 128, 1, 6 * 1024**3, 0.8) assert r["block_size"] in (16, 32), r assert r["fits"], r def test_fp8_padding_not_underestimated(): from fix_layer2_planner import DeviceProfile, real_block_bytes prof = DeviceProfile.for_arch("sm120") real = real_block_bytes(prof, 16, 64, 128, 1) naive = 16 * 64 * 128 * 1 * 2 assert real >= naive, "SM120 fp8 块真实占用不应低于理论值" def test_fallback_to_fp16_when_oor(): from fix_layer2_planner import DeviceProfile, plan_kv_cache prof = DeviceProfile.for_arch("sm120") # 极小预算下 fp8 也放不下 -> 业务层应回退 fp16 r_fp8 = plan_kv_cache(prof, 16, 64, 128, 1, 1 * 1024**3, 0.8) r_fp16 = plan_kv_cache(prof, 16, 64, 128, 2, 1 * 1024**3, 0.8) assert (r_fp8["fits"] or r_fp16["fits"]), "至少 fp16 应能放下"再加启动断言:
def assert_kv_plan(plan: dict): assert plan["fits"], ( f"KV 缓存放不下: 需要 {plan['estimated_bytes']} 但可用 " f"{int(plan['estimated_bytes']/plan['num_blocks']*plan['num_blocks'])}," "请降低 utilization 或调大 block_size 到 SM120 合法值" )八、排查清单
看到 Kimi K2.7 + SM120 + fp8 KV 的 Out of Resource / block size error,按序查:
- 先退回 fp16:把
--kv-cache-dtype设回auto,能跑说明问题只在 fp8 路径。 - 查
block_size合法性:SM120 上 fp8 KV 通常只接受 16/32,避开 8/64 这类值。 - 算真实单块字节:fp8 在 Blackwell 有对齐 padding,单块实际 > 理论
block×heads×dim×1×2,据此重算num_blocks。 - 降
gpu-memory-utilization:给对齐膨胀留 5%~10% 余量,常能直接消除 OOR。 - 看设备架构:确认
--device/ 驱动识别到 sm_120;某些环境误识别成 sm_90 会走错对齐规则。 - MoE 头维影响:Kimi K2.7 的
num_kv_heads、head_dim直接决定单块大小,核对 config 与加载是否一致。 - 升级 vLLM:SM120 的 fp8 KV 支持在新版本才完善,老版本可能块大小约束未实现。
- 监控峰值显存:用
--gpu-memory-utilization渐降法定位「临界点」,找到能容纳的最大预算。 - 必要时放弃 fp8:Blackwell 上若 fp8 KV 持续不稳,退回 fp16 KV 仍可享受 NVFP4 权重量化收益。
- 别超配
num_blocks:确认分配器按「真实对齐尺寸」而非「理论尺寸」预留,防止预估失准。
九、小结
Kimi K2.7 在 SM120 Blackwell 上用 fp8 KV 缓存报 Out of Resource / block size error,根子是fp8 块在 Blackwell 上有额外对齐 padding,使真实单块成本高于分配器预估;同时某些block_size值不被 SM120 的 fp8 内核支持。修复三层:第一层直接把block_size调到 16/32 合法值、给 utilization 留 10% 余量;第二层做自适应KVCachePlanner,按架构 + dtype 自动选块大小、按真实对齐尺寸预留显存;第三层用 pytest 把「fp8 块大小合法」「预估不低估」「fp8 放不下时回退 fp16」钉进 CI。核心认识——fp8 省显存的前提是「对齐成本被正确计入」;在 Blackwell 上永远按真实对齐后的块尺寸规划,而不是按理论字节数乐观预估。