开源模型挤着发了一圈,先别比榜单,算算显存
上周四到周五,开源模型挤着发了一圈。月之暗面放了 Kimi K3.1 Pro 的完整权重(2.8T,100 万上下文),百度开源文心 5.5 Flash Base(280B MoE,每 token 激活 14B),字节开源 Axion 1.0(35B / 122B,Apache 2.0,训练代码和数据一起给),还有 Ricursive 的 Ripple-65B。
选题会上有人问该上哪个。我说先别比榜单,先算能不能装下。
这篇就是算的过程,代码都跑过。
总参数和显存之间隔了两个东西
最常见的误解是拿总参数量直接换算显存。
权重占用确实只跟总参数量和精度有关:
python
GIB = 1024 ** 3
def weight_gib(total_params_b: float, bits: int) -> float:
"""权重占用,GiB。total_params_b 单位是十亿参数。"""
if total_params_b <= 0:
raise ValueError("total_params_b 必须为正")
if bits not in (4, 8, 16, 32):
raise ValueError("bits 只支持 4/8/16/32")
return total_params_b * 1e9 * bits / 8 / GIB
文心 5.5 Flash Base 按 BF16 算是 521.5 GiB,按 FP8 算是 260.8 GiB。差了整整一倍。这就是大 MoE 一定要上低精度的原因,不是什么潮流,是没得选。
但真正决定你跑不跑得起来的,是 KV 缓存。这个量随序列长度线性长:
python
def kv_cache_gib(layers, kv_heads, head_dim, seq_len, batch=1, bytes_per_elem=2):
"""KV 缓存,GiB。2 表示 K 和 V 各一份。"""
elems = 2 * layers * kv_heads * head_dim * seq_len * batch
return elems * bytes_per_elem / GIB
这里有个参数特别容易搞错:kv_heads 是 GQA 之后的头数,不是注意力头总数。我第一次写成总头数,算出来比实际大八倍,白紧张了半天。反过来也有人写成 1,那就小八倍,等上机 OOM 了才发现。
按 80 层、8 个 kv 头、head_dim 128、BF16 跑一遍:
yaml
32000 tokens: 9.8 GiB
128000 tokens: 39.1 GiB
1000000 tokens: 305.2 GiB
100 万上下文,单条请求的 KV 就要 305 GiB。并发四条就是 1.2 TB。这比很多模型权重本身还大。
所以"支持 100 万上下文"和"100 万上下文下能好好用",中间隔着很远的距离。谁在做 KV 量化、谁在做 PagedAttention、谁在做前缀共享,这些工程细节比标称长度更能说明问题。
活性参数小,不一定便宜
MoE 的另一个反直觉之处。总参数决定装不装得下,激活参数决定跑多快,这两件事是分开的。文心 5.5 Flash Base 总参 280B、每 token 激活 14B,算力大概相当于 14B 稠密模型,但知识容量接近 280B。
听起来很美。但 Agent 场景下有个陷阱。
我拿同一个网页填表任务做过对比,五次取中位:
| 档位 | 单步延迟 | 平均步数 | 总耗时 |
|---|---|---|---|
| Flash(小激活) | 1.1s | 14 | 15.4s |
| Pro(大激活) | 3.2s | 5 | 16.0s |
单步快三倍,总耗时几乎一样。多出来的九步把省下的时间全吃回去了。
所以评估指标应该是这个:
python
def task_time(step_latency_s: float, expect_steps: float, success_rate: float) -> float:
"""任务完成时间 = 单步延迟 × 期望步数 ÷ 成功率"""
if not 0 < success_rate <= 1:
raise ValueError("success_rate 必须在 (0, 1]")
return step_latency_s * expect_steps / success_rate
成功率在分母,这一项最容易被忽略。上面那个 Flash 档如果成功率掉到 0.6,成本立刻从 15.4 变成 25.7。一次失败重试,前面省的全白省。
这也是我一直坚持给 Agent 建回归用例集的原因。没有成功率数字,选型就是碰运气。
落成一张能算的表
把上面几段拼起来:
python
from dataclasses import dataclass
@dataclass
class ModelSpec:
name: str
total_b: float
active_b: float
layers: int
kv_heads: int
head_dim: int
def footprint(spec, bits, ctx, concurrency=1,
overhead_ratio=0.15, card_gib=80.0, usable_ratio=0.95):
w = weight_gib(spec.total_b, bits)
kv_bytes = max(1, bits // 8)
kv = kv_cache_gib(spec.layers, spec.kv_heads, spec.head_dim,
ctx, batch=concurrency, bytes_per_elem=kv_bytes)
overhead = (w + kv) * overhead_ratio # 框架、激活值、碎片
total = w + kv + overhead
usable = card_gib * usable_ratio # 80G 卡按 76G 可用算
cards = int(-(-total // usable)) # 向上取整
return {"model": spec.name, "weights_gib": round(w, 2),
"kv_gib": round(kv, 2), "total_gib": round(total, 2), "cards": cards}
跑一遍(80G 卡,预留 95% 可用,15% 余量):
ini
ERNIE-5.5-Flash-Base fp8 ctx= 32000 weights=260.8 kv= 4.9 total=305.5 cards=5
ERNIE-5.5-Flash-Base fp8 ctx=128000 weights=260.8 kv=19.5 total=322.4 cards=5
ERNIE-5.5-Flash-Base bf16 ctx=128000 weights=521.5 kv=39.1 total=644.7 cards=9
Axion-1.0-35B fp8 ctx= 32000 weights= 32.6 kv= 2.9 total= 40.9 cards=1
Axion-1.0-35B bf16 ctx=128000 weights= 65.2 kv=23.4 total=101.9 cards=2
Axion-1.0-122B fp8 ctx= 32000 weights=113.6 kv= 3.9 total=135.2 cards=2
Axion-1.0-122B bf16 ctx=128000 weights=227.2 kv=31.2 total=297.3 cards=4
这组数看着就清醒多了。35B 的 FP8 一张卡能跑,122B 要两张,文心那个 FP8 要五张、BF16 要九张。而 2.8T 的 Kimi K3.1 Pro 单机根本放不下,只能走 API 或者自建集群。到这一步,决策已经不是技术问题,是财务问题了。
这套代码我写了 14 个用例测过,覆盖精度翻倍、KV 线性增长、batch 放大、卡数向上取整、非法输入拦截:
test_weight_gib_bf16_280b .................. ok
test_weight_gib_fp8_halves ................. ok
test_weight_gib_rejects_bad_input .......... ok
test_kv_cache_linear_in_seq_len ............ ok
test_kv_cache_1m_budget .................... ok
test_kv_cache_scales_with_batch ............ ok
test_kv_cache_rejects_zero ................. ok
test_footprint_ernie_fp8_single_ctx ........ ok
test_footprint_small_model_fits_single_card ok
test_footprint_grows_with_context .......... ok
test_footprint_cards_round_up .............. ok
test_task_time_includes_success_rate ....... ok
test_task_time_rejects_bad_rate ............ ok
test_all_candidates_run_without_error ...... ok
而且,test_kv_cache_1m_budget 这个用例第一次是红的。我原来手算成 39 GiB,实际是 305 GiB,差了一个 kv_heads。写代码替我抓到了这个错。
实际的选型顺序
我现在按这个顺序走,能省掉大半无效比较。
先看能不能自部署。权重放得下就走自建,放不下就 API,这一步直接筛掉一半候选。
然后定上下文需求。代码补全、单轮问答 32K 就够,KV 小、并发能上去。长文档分析要 128K,得做 KV 量化或前缀共享。全库代码理解、长会话 Agent 是 100 万级别,这时候要么上专用方案,要么靠增量续接省传输。
接下来才是测。拿你自己业务里三十到五十个真实任务,跑几个候选,只记两个数:成功率和平均步数。这两个数比任何榜单都准,因为榜单里的任务是人家挑的。
然后算总账。单任务成本等于输入输出 token 乘单价,再除以成功率。成功率在分母,这一项经常是决定性的。
许可放最后看,但一定要自己翻原文。Axion 1.0 是 Apache 2.0,最宽松。Kimi K3.1 Pro 对连续十二个月营收超两千万美元的模型服务商有额外签约要求。新闻标题不会告诉你这些。
两个容易忽略的点
FP8 不是免费的。显存砍半、吞吐上去,但长链推理和数值敏感的代码生成这类任务质量会掉。必须拿自己的回归集验一遍,不能只看论文里的平均分。
开源权重的价值也不在分数上。Axion 1.0 这次把训练代码、评测脚本和一万八千条数据一起放出来了,这比多两个点的分数值钱得多,因为你能在自己的数据上接着训。而这恰恰是企业用开源模型的核心动机:数据不出门。
结尾
这一波表面在比参数,实际上比的是三件事:同样的卡能装下多大的有效容量,长上下文能不能真的用,训练链路开不开放。
所以选型的时候,把总参数从决策变量里删掉。真正该看的四个数是激活参数、KV 显存、你自己的任务成功率、许可条款。
前两个决定能不能跑,后两个决定该不该跑。