200K Context 到底需要多少显存?从 KV Cache 算到 Qwen3.8 的真实部署

结合 Qwen3.8-27B 的 GQA 与 Hybrid Attention 结构重新计算 KV Cache,并解释为什么理论 200K 可算、实际生产配置最终停在 100K FP8 KV。

📚 双 RTX 5060 Ti 部署 Qwen3.8 系列 · 5/6

💡 导读

长上下文显存最容易算错的地方,是把 Hybrid Attention 模型直接当成传统 64 层 Full Attention。Qwen3.8-27B 实际只有 16 层传统 Full Attention,这会把 200K 的 FP8 KV 理论值从约 12.2 GiB/GPU 改写为约 3.05 GiB/GPU,但真实生产边界仍然要看完整 Runtime。

前面已经把模型权重和 GPU 执行链基本处理清楚了。

接下来真正开始和长上下文抢显存的,就是 Cache。

但这里有一个非常容易算错的问题。

如果把 Qwen3.8-27B 当成一个传统的 64 层 Full Attention 模型,那么在 TP2 + FP8 KV 下,200K Context 会算出大约:

12.2 GiB / GPU

对于一张 16GB 的 RTX 5060 Ti,这几乎意味着根本没有正常部署空间。

但 Qwen3.8-27B 实际不是这种结构。

它采用混合 Attention:

48 × Gated DeltaNet + 16 × Full Attention

真正随着 Context 长度线性增长的传统 KV Cache,主要来自其中 16 个 Full Attention Layer。

重新计算以后:

  • 100K ≈ 1.53 GiB / GPU
  • 200K ≈ 3.05 GiB / GPU

差了整整四倍。

不过这仍然不意味着我的双 5060 Ti 最终能稳定跑 200K。

实际最终采用的是:

2 × RTX 5060 Ti 16GB
Qwen3.8-27B NVFP4
TP2
FP8 KV
CUDA Graph ON
gpu_memory_utilization = 0.97
max-model-len = 100K

vLLM Profile 最终显示:

Maximum Concurrency ≈ 1.07x

这篇真正想回答的就是两个看起来有点矛盾的问题:

为什么 200K 的传统 KV Payload 理论上只有约 3.05GiB / GPU,但最终生产配置却停在了 100K?

以及:

为什么我最后选择 FP8 KV,而没有继续把 KV 压到 INT4?


1. 先把 Qwen3.8 的 KV Cache 算对

自回归生成时,前面 Token 已经算出来的 Key 和 Value 不需要在每一步重新生成,因此会被缓存下来。

概念上可以理解成:

Token 1 → K1 / V1
Token 2 → K2 / V2
Token 3 → K3 / V3
...

对于一层传统 Full Attention,每个 Token 的 KV Cache 可以近似写成:

KV Heads
× Head Dim
× 2(K + V)
× dtype bytes

整个模型则是:

Tokens
× Full Attention Layers
× KV Heads
× Head Dim
× 2
× dtype bytes

因此对纯 Full Attention 模型来说,Context 翻倍,KV Cache 基本也会跟着翻倍。

但这个公式有两个很重要的前提:

  1. 模型到底有多少层真正使用 Full Attention;
  2. 每层到底需要缓存多少个 KV Head。

Qwen3.8-27B 在这两个地方都不能按“传统 64 层 Transformer”直接套公式。


GQA 先把 KV Head 数降了下来

Qwen3.8-27B 的 Full Attention 部分大致是:

  • Query Heads = 24
  • KV Heads = 4
  • Head Dim = 256

虽然 Query 有 24 个 Head,但真正需要长期缓存的 K/V 只有 4 组。

传统 MHA 更接近:

24 Q / 24 K / 24 V

GQA 则是:

24 Q
 ↓ 共享
4 K
4 V

所以算 KV Cache 时,真正需要代入的是:

KV Heads = 4

而不是 24。

这也是 GQA 对长 Context 显存非常直接的价值。


如果错误地把 64 层全部当成 Full Attention

先故意按传统模型算一次。

条件:

64 Layers
KV Heads = 4
Head Dim = 256
FP8 KV
TP2

TP2 下,4 个 KV Head 分到两张 GPU,每张大致保存 2 个。

每层、每 Token、每 GPU:

2 KV Heads
× 256
× 2(K + V)
× 1 Byte

= 1024 Bytes

64 层:

1024 × 64 = 64 KiB / Token / GPU

200K Context:

64 KiB × 200,000 ≈ 12.2 GiB / GPU

如果 Qwen3.8-27B 真的是 64 层全部 Full Attention,单是 200K 的 FP8 KV 就接近吃掉一张 16GB 卡的大部分显存。

这时候再讨论 23GB 权重、CUDA Graph、Runtime,基本就没有意义了。

问题在于:

Qwen3.8-27B 不是这种结构。


Qwen3.8-27B 只有 16 层传统 Full Attention

Qwen3.8-27B 的 64 层按照下面的模式循环:

3 × Gated DeltaNet + 1 × Full Attention

一共循环 16 次。

最终就是:

  • 48 × Gated DeltaNet
  • 16 × Full Attention

于是传统 KV 重新计算:

2 KV Heads / GPU
× 256
× 2(K + V)
× 1 Byte
× 16 Layers

=
16 KiB / Token / GPU

对应不同 Context:

Context FP8 Full-Attention KV / GPU
32K ≈0.49 GiB
64K ≈0.98 GiB
100K ≈1.53 GiB
128K ≈1.95 GiB
200K ≈3.05 GiB
256K ≈4.00 GiB

所以同样是 200K:

错误按 64 层 Full Attention:
≈ 12.2 GiB / GPU

按实际 16 层 Full Attention:
≈ 3.05 GiB / GPU

正好差了接近四倍。

这也是为什么面对 Hybrid Attention 模型时,不能只知道“它有 64 层”,就直接套传统 KV 公式。


另外 48 层并不是完全没有状态

Gated DeltaNet 不需要像 Full Attention 一样,为每个历史 Token 保存完整 K/V。

它更接近维护 recurrent state:

历史信息
不断更新 State
当前 State

Full Attention 的传统 KV 是:

Context 越长 → KV 越大

而 Gated DeltaNet 的 recurrent state 本身不是这种“每多一颗历史 Token 就追加一份完整 K/V”的线性增长方式。

因此 Qwen3.8-27B 的 Cache 更准确地理解为:

16 × Full Attention
→ 随 Context 增长的传统 KV Cache

48 × Gated DeltaNet
→ per-sequence recurrent state

这解释了为什么它在超长 Context 下,比“64 层全部 Full Attention”的模型省很多传统 KV。

但同时也提醒我们:

3.05GiB 只是 Full Attention KV Payload,不是整个 Engine 最终的显存成本。


2. 1.53GiB 理论 KV,不等于只多占 1.53GiB 显存

100K FP8 Full-Attention KV 的理论值只有:

≈ 1.53 GiB / GPU

但 GPU 上同时还需要存在:

  • NVFP4 Weights
  • Gated DeltaNet State
  • CUDA Runtime
  • CUDA Graph
  • Peak Activation
  • Workspace
  • Allocator
  • KV Block / Metadata
  • 通信 Buffer
  • 框架其他 Buffer

所以实际绝对不能直接做:

16GB
-
权重
-
1.53GB
=
剩余显存

理论公式真正回答的是:

Context 每增加一段,传统 Full Attention KV Payload 大概增长多少?

而真实部署要回答的是:

模型加载、Graph、Activation、State 和所有 Buffer 都存在以后,到底还能留多少 Cache Capacity?

前者靠公式。

后者靠实际 Profile。


3. 从理论 Payload 到真实 Runtime 边界

最终跑通并采用的配置是:

GPU:
2 × RTX 5060 Ti 16GB

Model:
Qwen3.8-27B NVFP4

Parallel:
TP2

KV Cache:
FP8

CUDA Graph:
ON

gpu_memory_utilization:
0.97

max-model-len:
100K

vLLM Profile 给出的:

Maximum Concurrency ≈ 1.07x

更准确的理解不是简单说:

100K × 1.07 = 107K

然后把 107K 当成一个精确的“总 Cache Token 数”。

它更适合被理解为:

在当前 max-model-len = 100K 和当前 Cache 配置下,Profile 估算只能同时容纳大约 1.07 条达到最大长度的 Sequence。

也就是说,这套配置对:

1 × 100K 长上下文

已经比较接近当前 Profile 给出的容量边界,余量并不算大。

这里还要再加一层保险:Qwen3.8-27B 属于 Full Attention + Gated DeltaNet 的 Hybrid Model,vLLM 的 Maximum Concurrency 是当前版本 Cache Manager 根据实际 Cache 配置给出的 Profile 指标,不应该被当成一个脱离版本、Backend 和 Hybrid Cache 实现后仍然精确成立的物理常数。

所以我把:

Maximum Concurrency ≈ 1.07x

理解成:

当前这套 Runtime 配置已经非常接近 1 条最大长度 Sequence 的容量边界。

而不是拿它反推出一个精确的“总 Cache Token 数”。

所以:

200K 传统 KV Payload ≈ 3.05 GiB / GPU

并不能直接推出:

200K 一定能稳定启动并保留 CUDA Graph 等运行配置

决定最终 Context 上限的是整套 Runtime 显存,而不是传统 KV 这一项。


max-model-len 不是“每个请求固定预留这么多 Cache”

设置:

--max-model-len 100000

表示的是:

单条 Sequence 最大允许达到 100K。

它不等于“每来一个请求,就静态给它预留完整 100K KV”。

vLLM 实际维护的是共享 Cache Pool。

概念上,如果当前几个请求分别占用:

请求 A:40K
请求 B:30K
请求 C:20K
请求 D:10K

它们会共同消耗 Engine 的 Cache Capacity。

而不是:

4 × 100K = 400K

全部提前保留。

所以生产 Serving 不能只问:

“我设置 100K,可以并发几条?”

还要看实际请求长度分布、Block 使用情况以及 Hybrid State 的 per-sequence 成本。


Hybrid Model 下,总 Token 相同也不代表显存完全相同

只考虑 Full Attention KV:

1 × 100K

和:

5 × 20K

总 Token 数一样,传统 KV Payload 也接近。

但 Qwen3.8-27B 还有 per-sequence recurrent state。

一条 100K Sequence 只需要一套 Sequence State。

五条 20K Sequence 则需要五套。

除此之外还有:

  • Scheduler State
  • Output Buffer
  • Block Fragmentation
  • 其他 per-request allocation

所以在 Hybrid Model 下:

总 Token 数相同,不代表最终显存占用完全相同。

这也是为什么 Maximum Concurrency 和实际并发能力,最终还得通过真实 workload 测。


4. KV 精度怎么选:BF16、FP8 与 INT4

对于这 16 个 Full Attention Layer,TP2 下:

100K

BF16 KV
≈ 3.05 GiB / GPU

FP8 KV
≈ 1.53 GiB / GPU

200K

BF16 KV
≈ 6.10 GiB / GPU

FP8 KV
≈ 3.05 GiB / GPU

对一张只有 16GB 的 5060 Ti 来说,每卡节省 1.5GB 或 3GB 已经足以直接改变最终可用 Context。

所以我的实际组合是:

  • Weights:NVFP4
  • KV:FP8

权重和 KV 根本没有必要追求同一种 bit 数。

它们走的是不同的数据路径,也有不同的性能瓶颈。


INT4 KV 确实更省,但不能只看 bit 数

只从数据量来看:

  • FP8 = 1 Byte / element
  • INT4 ≈ 0.5 Byte / element

所以理论上传统 KV Payload 还能再减半。

例如 100K:

  • FP8 ≈ 1.53 GiB / GPU
  • INT4 ≈ 0.76 GiB / GPU

200K:

  • FP8 ≈ 3.05 GiB / GPU
  • INT4 ≈ 1.53 GiB / GPU

容量收益确实很诱人。

但 KV Cache 和“硬盘压缩”不一样。

Attention 每一轮都要频繁读取这些数据。

INT4 KV 往往还会涉及:

packed INT4 read
unpack
scale / dequant
格式转换
Attention

所以:

Memory Traffic ↓

并不自动等于:

Latency ↓

额外 unpack、scale 和反量化本身也要花算力与 Kernel 开销。


为什么这套 5060 Ti 最后还是选 FP8 KV?

RTX 5060 Ti 属于 Blackwell。

在这套硬件上,我更关心的是:

当前框架的 FP8 Attention / Cache 路径能不能稳定、高效地运行。

而不是单纯追求最低的 KV bit 数。

INT4 KV 的优势很明确:

  • 更省显存
  • 更大的理论 Context

但代价也同样明确:

  • packed read / unpack
  • dequant / scale
  • Kernel 路径更复杂
  • 可能增加 Decode 计算开销
  • 精度风险也更激进

FP8 KV 虽然占用比 INT4 大一倍,但在当前硬件和推理框架里,是一个更自然的平衡点:

  • 容量已经明显低于 BF16
  • Kernel 路径更成熟
  • 不需要为了进一步压缩增加过多低 bit 解码工作
  • 精度选择也更保守

更重要的是,这套机器使用 FP8 KV 已经可以做到:

100K Context + Maximum Concurrency ≈ 1.07x

对我的真实代码任务来说,这已经够用了。

所以没有必要为了“Context 数字更大”而继续把 KV 压到 INT4,最后反而让 Decode 更慢。


为什么是 NVFP4 权重 + FP8 KV?

表面上看:

  • Weights:4bit
  • KV:8bit

好像精度“不统一”。

但实际非常合理。

NVFP4 权重

主要目标是:

  • 让 27B 权重完整 GPU Resident
  • 减少 Weight Traffic
  • 利用 Blackwell 对 NVFP4 的低精度 GEMM 路径

FP8 KV

主要目标是:

  • 降低长 Context 显存
  • 保持更直接的 Attention 执行路径
  • 避免更低 bit 带来的额外 unpack / dequant 成本
  • 保持长上下文稳定性

所以实际部署并不是:

“整个模型统一选一个 bit 数。”

更合理的思路是:

权重、KV、Activation 分别选择当前 GPU 和当前 Kernel 最合适的数据格式。

这也是低精度推理真正进入工程阶段以后,和“模型量化成几 bit”这种单一描述最大的区别之一。


5. 最终配置

最后我实际采用的是:

2 × RTX 5060 Ti 16GB

Qwen3.8-27B NVFP4

TP2

FP8 KV

CUDA Graph ON

gpu_memory_utilization = 0.97

max-model-len = 100K

结果:

Maximum Concurrency ≈ 1.07x

对这套硬件来说,它形成了一个比较合理的平衡:

27B 模型质量
+
权重完整 GPU Resident
+
TP2 Decode 性能
+
100K Context
+
FP8 KV
+
CUDA Graph

而不是为了追求一个单独的:

200K

把 KV 精度、Decode 性能和显存稳定性同时推到极限。


6. 结论

Qwen3.8-27B 的长 Context 显存不能按照:

64 Layers × 传统 KV Cache

直接计算。

它实际是:

48 × Gated DeltaNet + 16 × Full Attention

再加上:

24 Query Heads / 4 KV Heads

的 GQA。

因此 TP2 + FP8 KV 下,仅计算传统 Full-Attention KV Payload:

100K
≈ 1.53 GiB / GPU

200K
≈ 3.05 GiB / GPU

但真实运行还必须容纳:

  • Weights
  • Gated DeltaNet State
  • Runtime
  • Activation
  • CUDA Graph
  • Workspace
  • Allocator
  • 其他 Buffer

所以最终实际采用的是:

100K Context
FP8 KV
gpu_memory_utilization = 0.97
Maximum Concurrency ≈ 1.07x

这给出了一个我现在很喜欢的判断方式:

公式负责解释显存模型,vLLM Profile 负责找边界,真实长上下文压测负责定生产配置。

NVFP4 Weights + FP8 KV 也不是精度选择混乱。

它只是分别给不同数据路径选择更合适的格式。

到这里,“100K 能不能装下”的问题基本结束了。

接下来的问题变成:

100K Context 真正跑起来以后,怎么避免每一轮 Agent 都重复计算几十 K、甚至接近 100K 的相同 Prompt?

下一篇进入整个系列最后一层:

《长上下文推理优化:Prefix Cache、调度与 MTP》


系列导航: ← 上一篇:为什么生产推理我默认 vLLM:从 NVFP4、Kernel 到 CUDA Graph · 下一篇:长上下文推理优化:Prefix Cache、调度与 MTP →

使用 Hugo 构建
主题 StackJimmy 设计