长上下文的成本结构:为什么 128K 不等于能用 128K

举报
yd_232401777 发表于 2026/10/27 08:35:14 2026/10/27
【摘要】 上下文窗口从 8K 涨到 128K,很多人第一反应是"终于可以把整个文档库塞进去了"。但长上下文有三笔账要算:prefill 的平方项、decode 的线性累积、以及 KV Cache 的内存占用。这三笔账决定了长上下文在什么场景下划算、什么场景下反而是负担。

前言

上下文窗口扩大是这两年最直观的进步。8K 到 32K 到 128K 甚至更长,看起来只是"能装更多"。

但把它当成"可以少做检索"的理由,通常会在两个地方吃亏:账单和效果。

账单的问题在于长上下文不是线性变贵的,效果的问题在于"装得下"和"用得上"是两件事。这篇文章把这两点拆开算。

一、成本的三个来源

一次大模型推理的成本,由三部分组成,它们的增长规律完全不同。

1.1 Prefill:平方项

Prefill 是把输入的 prompt 编码成 KV Cache 的过程。这里面有两块计算:

  • 线性部分:每个 token 都要过一遍所有参数,成本正比于 序列长度 × 参数量
  • 平方部分:自注意力里每个 token 要和其他所有 token 算相关性,成本正比于 序列长度²

短上下文时线性部分占主导,所以感觉上"成本随长度线性增长"。但到了长上下文,平方项开始吃掉主要成本。

粗略地说:

序列长度注意力相对成本
1K1
8K64
32K1024
128K16384

注意这只是注意力部分的相对增长,不是端到端成本。但因为线性部分同时也在涨,总成本的增长曲线比线性陡得多。

这就是为什么长上下文的输入价格通常有明显的分档——不是定价策略,是成本结构决定的。

1.2 Decode:线性但累积

生成阶段,每生成一个 token 都要和所有已存在的 token 做一次注意力。所以单步成本正比于当前总长度,而生成长度累积起来,总成本是 n × m(n 是输入长度,m 是输出长度)。

这带来一个反直觉的结论:长输入 + 长输出是最贵的组合。

如果你的场景是"塞 100K 上下文,让模型输出一份详细的 5K 报告",成本会是"塞 10K 上下文,输出 5K 报告"的数倍到十几倍——远不止 10 倍输入量的差距。

1.3 KV Cache:内存账

这是最容易被忽略的一笔。

模型在生成时要把每一步的 Key 和 Value 缓存下来,避免重复计算。这部分占用的显存可以这么算:

每 token 占用 = 2 (K 和 V) × 层数 × KV 头数 × 每个头的维度 × 字节数

以一个常见的 8B 级别、采用 GQA 的模型为例(32 层、8 个 KV 头、每头 128 维、fp16):

2 × 32 × 8 × 128 × 2 字节 = 131,072 字节 ≈ 128 KB / token

那么 128K 上下文需要:

131,072 token × 128 KB ≈ 16 GB

光 KV Cache 就要 16GB 显存,还不算模型权重本身(8B 模型 fp16 约 16GB)。

对比一下:如果这个模型用的是传统多头注意力(32 个头而不是 8 个),每 token 占用会变成 512KB,128K 就要 64GB。这就是 GQA 这类技术存在的意义——它不是为了效果,是为了让长上下文跑得动。

二、为什么"装得下"不等于"用得上"

2.1 位置偏差

模型对上下文不同位置的利用效率不均匀。有一个被反复验证的现象:开头和结尾的信息被利用得更好,中间的信息容易被忽略。

这个现象和上下文长度有关——短上下文时差异不明显,长上下文时中间区域几乎成了"盲区"。

对你的应用意味着:把关键信息塞在 100K 上下文的中间位置,模型很可能看不见它。 这不是模型偷懒,是注意力机制在长序列上的固有特性。

2.2 名义长度 vs 有效长度

现在很多评测会区分这两个概念:

  • 名义长度:模型声明支持的最大窗口
  • 有效长度:在这个长度下,模型的实际任务表现仍能维持在可接受水平

这两者往往有明显差距。一些针对长上下文的评测(比如在大海捞针类任务上做多针、多跳的变体)显示,很多模型在名义长度的一半甚至三分之一处,准确率就开始明显下降。

也就是说,一个标称 128K 的模型,在处理 128K 输入时可能远不如它处理 32K 时可靠。"能读进去"和"能理解好"不是一回事。

2.3 干扰项效应

长上下文里必然包含大量不相关内容。这些内容不只是"浪费 token",它们会主动干扰。

典型表现:你问 A 相关的问题,上下文里同时有 B、C、D 的描述,模型有时会把 B 的属性套到 A 上。信息越密集、越相似,干扰越强。

这一点在 RAG 场景里特别明显:召回 20 条文档塞进去,效果经常不如召回 5 条精排后的。

三、工程上的应对

3.1 上下文压缩

不是所有内容都值得原样塞进去。常见的压缩手段:

手段做法适用
摘要压缩先让模型把长文档摘要成要点文档型输入
结构化提取只抽需要的字段,丢掉原文表单、合同、日志
段落筛选先做一轮粗筛,只保留相关段落多文档问答
去重合并相似内容只留一份检索结果

其中"结构化提取"收益最大——因为它是任务导向的。如果下游只需要 5 个字段,把 20 页原文塞进去是纯粹的浪费。

3.2 分层处理

对于超出窗口的文档,不要试图一次塞完。两种做法:

Map-Reduce 式:分块处理,每块产出一个中间结果,再汇总。适合"总结全文"类任务。

滑动窗口 + 状态累积:逐块处理,每块带着之前累积的状态。适合流式输入、长对话。

这两种都会增加调用次数,但每次的上下文长度可控——总成本经常比单次超长调用更低,效果也更稳。

3.3 缓存复用

如果多次请求共享同一段前缀(比如固定的系统提示 + 知识库内容),用 prompt caching 能显著降低成本。命中缓存的部分通常按正常输入价格的百分之十几计费。

但要注意缓存的两个约束:

  • 前缀必须完全一致。中间插入一个字符,后面全失效
  • 有存活时间。超过了就要重新计算

所以设计提示结构时,把稳定不变的部分放前面,变化的部分放后面。这个顺序调整是免费的,但能显著提高缓存命中率。

3.4 用检索替代全量塞

回到最初的问题:长上下文是否意味着可以不做检索?

答案基本是否定的。原因不是"塞不下",而是:

  • 检索能降低干扰项
  • 检索能把最相关的放在开头(利用位置优势)
  • 检索的成本远低于把全量内容送进 prefill

长上下文更适合解决"检索精度不够导致漏掉关键信息"的问题,而不是替代检索。

一个合理的组合是:检索取出 10-20 条候选,用 reranker 精排到 3-5 条,再给它们配上足够的周边上下文。总长度控制在几千 token,效果好于硬塞 100K。

四、成本优化清单

按收益从高到低排:

动作收益代价
减少输出长度(明确要求简洁)高可能丢细节
结构化提取替代原文塞入高需要写抽取逻辑
提示前缀稳定化,提高缓存命中高结构调整
检索精排替代全量塞高需要检索链路
分块处理超长文档中调用次数增加
换更小的模型做粗加工中需要任务拆分
调低 max_tokens 上限中效果风险

第一条经常被忽略。输出 token 比输入 token 贵得多(因为 decode 是逐 token 串行生成的),如果你的提示里没有明确"简洁回答",模型倾向于展开,这部分成本是白花的。

做了什么

  1. 把长上下文成本拆成 prefill 的平方项、decode 的线性累积、KV Cache 的内存三部分
  2. 以 8B 级别 GQA 模型为例算出 KV Cache:每 token 约 128KB,128K 上下文约 16GB
  3. 指出传统多头注意力下同样的上下文要 64GB,说明 GQA 是为长上下文服务的工程手段
  4. 区分名义长度与有效长度,指出长上下文存在位置偏差与干扰项效应
  5. 给出四种应对:上下文压缩、分层处理、缓存复用、检索替代
  6. 列出七项成本优化动作,按收益排序

一些体会

长上下文是"减少信息损失"的工具,不是"省掉检索"的工具。 这两个定位差别很大。前者解决的是"检索漏了一条关键信息",后者要面对干扰项、位置偏差、成本三座山。

算成本要算输出。 大家习惯关注输入有多长,但 decode 是逐 token 串行的,单位成本更高。一份啰嗦的长回答,账单可能比长输入更吓人。

KV Cache 决定了长上下文能不能落地。 128K 在纸面上是个数字,落到显存上就是十几 GB。做私有化部署时,这个数字直接决定了你要什么卡、能开多少并发。算清楚再谈功能。

"有效长度"这个概念值得记住。 厂商宣传的是名义窗口,你实际能依赖的是有效长度。做设计时按有效长度的一半去规划,通常比较稳。

代码

KV Cache 占用的估算函数:

def kv_cache_bytes(seq_len, n_layers, n_kv_heads, head_dim, dtype_bytes=2, batch=1):
    """返回 KV Cache 字节数"""
    return 2 * n_layers * n_kv_heads * head_dim * dtype_bytes * seq_len * batch

def human(n):
    for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
        if n < 1024:
            return f"{n:.1f} {unit}"
        n /= 1024
    return f"{n:.1f} PB"

# 8B 级别,GQA(8 个 KV 头)
cfg = dict(n_layers=32, n_kv_heads=8, head_dim=128)

for L in [8_000, 32_000, 128_000]:
    size = kv_cache_bytes(L, **cfg)
    print(f"{L:>7,} tokens → {human(size):>10}")

# 对比:传统多头注意力(32 个头)
for L in [32_000, 128_000]:
    size = kv_cache_bytes(L, n_layers=32, n_kv_heads=32, head_dim=128)
    print(f"[MHA] {L:>7,} tokens → {human(size):>10}")

跑出来的量级大致是:GQA 下 8K 约 1GB、32K 约 4GB、128K 约 16GB;换成 MHA 后 128K 要 64GB。这个对比最能说明为什么现在的模型普遍用 GQA。

提示结构对缓存命中率的影响:

# ❌ 变化的内容在前,缓存永远不命中
prompt_bad = f"""当前时间:{now}
用户问题:{question}
[以下为固定知识库内容...]"""

# ✅ 稳定内容在前,变化内容在后
prompt_good = f"""[以下为固定知识库内容...]

当前时间:{now}
用户问题:{question}"""

这个改动只有几行,但它是长上下文应用里性价比最高的优化之一。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。