第 7 章 长上下文:先撞算力,还是先撞容量
先修:第 1–6 章 · 深度:L1 · 预计阅读:25 分钟
一句话:上下文翻四倍,缓存翻四倍,注意力翻十六倍。
合同从 8K 变成 32K
产品说,客户要传整本合同,三万字,模型得读完再答。
你手里还是那张 80GB 卡,还是 7B。8K 时能并 14 条,现在呢?
两笔账同时变坏,而且变坏的倍数不一样。
缓存:翻四倍
一个 token 的缓存是 512 KiB,跟长度无关。长度翻四倍,缓存就翻四倍。
8K → 4 GiB
32K → 16 GiB
80GB 卡:56 GiB 可用给缓存,除以 16,只剩 3 条。
二十个人传合同进来,三条在跑,十七条在等。首字延迟从"一秒"变成"不知道"。
24GB 卡更直接:6 GiB 可用,16 GiB 一条都放不下。不是慢,是跑不了。
算力:翻四倍,注意力部分翻十六倍
prefill 要一次算完整段输入。运算量里有一项跟长度成正比,有一项跟长度的平方成正比。
线性那部分是逐层的矩阵乘:长度翻四倍,它翻四倍。
平方那部分是注意力:每个 token 都要和前面所有 token 比一次,比较次数是 T × T。
8K → 64M 次比较
32K → 1024M 次比较
长度翻四倍,比较次数翻十六倍。
注意力只占整体运算的一部分,所以实际涨幅在两者之间,长度越长越靠近平方项。
粗算 32K 的一次 prefill:2 × 7 × 10⁹ × 32768 ≈ 459 TFLOP。按 A100 的 312 TFLOPS,理想情况 1.5 秒。实际做不到这个数,再算上排队,用户等三五秒是常态。
两笔账谁先到
短上下文时,容量先到:缓存涨得快,算力还有余量。
长上下文时,两笔一起到,而且算力那边更难救——缓存可以量化、可以分页、可以共享,算力只能换卡或者改模型。
有个粗略的分界:当一条请求的 prefill 时间(秒)超过你容忍的首字延迟时,算力就是主瓶颈;当并发数掉到个位数时,容量是主瓶颈。32K 这个尺度上,两个都会到。
位置编码:模型没见过这么长
还有一个不是资源的问题。
模型训练时见过的最大长度,往往就是 4K 或 8K。位置编码是按那个范围设计的,硬拉到 32K,模型会"分不清"位置。
现在的做法是旋转位置编码(RoPE),把位置信息编进向量的旋转角度里。超出训练长度后角度会绕圈,效果就散了。补救的办法是调整旋转的尺度,让远距离的位置差别不至于糊成一团。YaRN、NTK 插值都是这类调整。
调过之后,模型能在 32K 上跑,但"能在"不等于"好用"。中间段落的信息容易被忽略,位置越靠中间越明显。这不是显存问题,压多少比特都解决不了。
压缩缓存的三种路子
滑窗:只保留最近 N 个 token 的缓存。省得最多,也最容易忘事。
量化:缓存压到 INT8,直接省一半。上一章说过,这是风险最高的一种压法。
驱逐:算出哪些 token 的历史不重要,扔掉它们的缓存,需要时重算。省得多,实现复杂,效果依任务而定。
三种都在做同一件事:承认"全部记住"太贵,然后选择忘掉哪部分。选择本身有代价,只是代价从显存转移到了回答质量上。
什么时候值得上长上下文
只有当答案真的藏在长文末尾或中间时。
一份合同,用户问"总价多少",答案往往在最后几页,值得。
一份合同,用户问"这是什么类型的合同",前两百字就够,把 32K 全塞进去是浪费。
一个实用做法:先检索,只把相关段落塞进去。把上下文从 32K 降到 2K,前面所有的账都好算。这也是检索增强存在的现实理由,不只是"模型记不住"。
经验:先问用到第几个 token
看到"要支持 32K",先问一句:用户的问题,答案平均在第几个 token?
如果答案集中在前 10%,那就先做截断或者检索,别急着上长上下文。省下的显存和算力,比任何调度都多。
练习
六道题,先闭卷,再翻 ANSWERS.md。
- 32K 请求的缓存多少 GiB?80GB 卡能并几条?
- 为什么注意力是平方的,而矩阵乘是线性的?
- 24GB 卡配 7B,8K 能跑 1 条,32K 为什么一条都跑不了?
- 找错:"上下文长度翻倍,成本也翻倍。"
- 滑窗、量化、驱逐,各自把代价转移到了哪里?
- 拿一份长文档,分别在只喂前 2K 和喂满上下文的情况下问同一个问题,比较答案质量和 TTFT。
这一章就记一句
上下文从 8K 到 32K,缓存从 4 GiB 到 16 GiB,并发从 14 掉到 3;prefill 的运算量涨四倍,注意力部分涨十六倍。位置编码还要额外调,调完也不等于好用。
真只记一句:长上下文的第一问不是"卡够不够",是"答案在第几个 token";答得出来,就先截断或检索,答不出来才谈扩容。
下一章把前面所有的账合起来用一次:同样是这二十个同事,四个引擎怎么选。