DeepSeekV4之CSA+HCA
DeepSeek-V4 的 CSA + HCA 详解
说明
- 本文不基于 DeepSeek-V4 正式论文(作者手上没有该论文)。所有"公式"都是从 sglang 源码的计算语义反推、并逐条标注
文件:行号,可对照代码核验。 - 公式用 LaTeX 书写(在支持数学渲染的 Markdown 预览里查看)。
- 记号: 点积, 逐元素乘, 对 求和。
- 姊妹文档:
docs/mla_explained.md(MLA)、docs/dsa_explained.md(V3.2 的 DSA)。 - 技术报告:arxiv.org/pdf/2606.19348
0. 血缘:三代注意力在解决什么
| 版本 | 机制 | 痛点 | 做法 |
|---|---|---|---|
| V2 | MLA | KV 太占显存 | 每个 token 的 KV 压成 512 维 latent |
| V3.2 | DSA | 长序列算不动 | 轻量 indexer 选 top- token,只算这些 |
| V4 | CSA + HCA | 上面两个一起做 | 把历史 KV 压成"块",再分两档处理 |
V4 的新思路:不再以"单个 token"为单位存 KV,而是把连续若干 token 压成"一个块"。压多狠、压完怎么用,分两档 = CSA 和 HCA。
1. CSA 和 HCA 是什么
compressor.py:86 直接写死:
1 | assert compress_ratio in (4, 128), \ |
- CSA = Compressed Sparse Attention,压缩比 4x(代码里叫
c4) - HCA = Heavy Compressed Attention,压缩比 128x(代码里叫
c128)
分工(runtime.py:11-12, paged_prefill.py:17):
| CSA (ratio=4) | HCA (ratio=128) | |
|---|---|---|
| 压缩比 | 4 token → 1 块 | 128 token → 1 块 |
| 保真度 | 高(压得轻) | 低(压得狠) |
| 用法 | 稀疏:块多,选 top-512 | 稠密:块少,全读 (all-committed) |
| 带 indexer? | 带 C4Indexer |
不带 |
直觉:CSA 压得轻但块多 → 必须挑(top-)才划算;HCA 压得狠但块少 → 干脆全看也不贵。本质是"压缩率 vs 是否稀疏"的权衡。
关键:每一层只用其中一档(deepseek_v4.py:296-302):
1 | compress_ratio = config.compress_ratios[layer_id] # 逐层配置 |
所以 43 层里,有的层是 CSA、有的是 HCA、少数是 0(不压缩的稠密层)。
2. 压缩器 Compressor 的数学(CSA/HCA 共用)
CSA 和 HCA 用同一个 Compressor 类,只是 ratio 不同。干的事:把连续 个 token 的 KV 压成 1 个压缩块。
重要订正:读了真正的池化 kernel(
c4.cuh的c4_forward)后确认——它不是"sigmoid 门控加权求和 (GLU)",而是一个小型 attention:在一个窗口上做 softmax 加权平均,权重来自 token 自带的 score 通道 + 位置偏置 APE。下面给正确形式,逐步过程见第 7 节。
代码结构(compressor.py:344-462):
1 | wkv_gate = Linear(dim -> 2 * coff * head_dim) # 值 + 打分, 见下 |
反推公式(设块 覆盖 token , 或 ):
(C-1) 投影,对每个 token (compressor.py:418-424):
CSA(coff=2)劈成 4 段(c4.cuh:43-46 布局),每段
半 = 这个 token 的 value(被池化的量) 半 = 这个 token 的 softmax logit(当权重用) = 同一 token 的第二套独立投影,被邻块借用时才用(见第 7 节)
HCA(coff=1)只有
(C-2) 窗口内 softmax 注意力池化(c4.cuh:176-209),对 head_dim 的每个通道
即
(C-3) 归一化 + RoPE(池化之后的独立 kernel,compressor.py:143-160):
forward_compress = compress_forward(池化)再 compress_fused_norm_rope_inplace。
得到的
3. CSA 独有的 top-k:C4Indexer
HCA 压到 128x,块少,全读即可。CSA 只压 4x,块还很多,要再选 top-512。这个筛选器 C4Indexer 本质就是 V3.2 的 DSA lightning indexer,只是打分对象换成了"4x 压缩块"。
代码结构(indexer.py:655-688):
1 | n_heads = index_n_heads = 64 |
公式(走 fp8_paged_mqa_logits + topk_transform_512, indexer.py:14,49):
(CSA-1) 对 query token
的第 头(indexer.py:703) = indexer 自己那份 4x 压缩块(indexer.py:678) ,带缩放 (indexer.py:714,691)
这和 DSA 的
(CSA-2) 选 top-512 块(topk_transform_512, indexer.py:14):
4. 一层内怎么合并 KV(重要订正:每层两路,不是三路)
订正:每个"压缩层"内部是两个来源,不是三个。
- 近处:SWA 滑窗 → 最近 window_size=128 个 token,不压缩,精确
- 远处:压缩历史 → 更早的 token,压成块。这一档由本层
compress_ratio决定:- 本层是 CSA → 远处是 c4 块,且要选 top-512
- 本层是 HCA → 远处是 c128 块,全读
证据(runtime.py:7-12,描述同一层的 unified_kv 缓冲区布局):
1 | unified_kv[L] 布局: |
一层的缓冲区 = SWA ring + 压缩块,压缩块只会是 c128 或 c4 之一。
关于 paged_prefill.py:17 那句 "(a) SWA, (b) CSA topk, (c) HCA all-committed":那是这个统一 kernel 能处理的三种索引来源(跨不同层类型的能力清单),不是"某一层同时用三个"。CSA 层走 (a)+(b),HCA 层走 (a)+©。
每层分类(最终结论):
| ratio | 类型 | KV 来源 |
|---|---|---|
| 0 | 稠密层 | 全历史精确注意力;仍是 MQA(num_key_value_heads==1, deepseek_v4.py:305),不是经典 MHA |
| 4 | CSA 层 | SWA 近处(精确)+ c4 远处(top-512 筛选) |
| 128 | HCA 层 | SWA 近处(精确)+ c128 远处(全读) |
为什么每层都要配 SWA 近处? 压缩会丢信息,最近的 token 对当前预测最关键,绝不能压。所以无论 CSA/HCA,都保留最近 128 个 token 的精确 KV;只有更早的历史才拿去压缩。近处保真、远处压缩,是长上下文压缩的通用套路。
一个 CSA 层 query paged_prefill.py:36-38):
HCA 层则是 SWA + 全部 c128 块。两路共享同一个在线 softmax 累加器
attn_sink(V4 特有,deepseek_v4.py:385):每头一个可学习的 softmax 分母偏置:
分母多出的
5. 主注意力仍是 MLA 骨架
deepseek_v4.py:283-305:
1 | num_key_value_heads == 1 # 还是 MQA, 所有 query 头共享一份 KV |
所以 CSA/HCA 存进 cache 的那个
6. 三句话总结
- Compressor(CSA/HCA 共用)把连续
个 token 用"窗口 softmax 注意力池化(权重 = score+APE)+ norm+rope"压成 1 个 512 维块 (C-1~C-3,详见第 7 节), 是 CSA、 是 HCA。CSA 窗口带 overlap(本块 4 + 邻块 4)。 - CSA(4x)压得轻块多 → 用和 DSA 同构的 lightning indexer 选 top-512 块(CSA-1/2);HCA(128x)压得狠块少 → 不筛全读。每层二选一(另有极少数 ratio=0 稠密层)。
- 每个压缩层用一个在线 softmax + attn_sink,合并两路 KV:SWA 精确近处 + 本层单一档压缩远处(CSA 或 HCA);底座仍是 MLA 的 num_kv_heads=1 MQA + decoupled RoPE。
7. CSA 压缩的逐步过程(overlap softmax 池化,kernel 级)
这是第 2 节 (C-1)~(C-3) 的展开,回答"4 个 token 怎么合成 1 个块、overlap 到底是啥"。依据:c4.cuh 的 c4_forward(L114-212)与 buffer 布局注释(L43-46)。
7.1 先破除一个直觉:不是相加平均
很多人以为压缩是
7.2 每个 token 先被投影成 4 段(wkv_gate,CSA coff=2)
kv_score_input shape = c4.cuh:71),最后一维:
同一个 token wkv_gate 线性层,产出:
| 段 | 含义 |
|---|---|
| kv | 它作为"本块成员"时的 value |
| score | 它作为"本块成员"时的 softmax logit |
| kv_overlap | 它被"邻块借用"时的 value(另一套独立投影) |
| score_overlap | 它被邻块借用时的 logit |
注意:overlap 那份不是复制,是独立学出来的第二套表示。coff=2 就是这个意思。HCA(coff=1)只有
7.3 overlap 的真身:窗口 = 8 个 token = 本块 4 + 邻块 4
c4_forward 循环 c4.cuh:138-164):
,is_overlap=true→ 读上一个块的 4 个 token,用其 overlap 投影 ,is_overlap=false→ 读当前块的 4 个 token,用其 normal 投影
关键行:
1 | const bool is_overlap = i < 4; // L144 |
前 4 个来自上一块存储页(load_first_page / kv_score_overlap_buf),后 4 个来自当前块页(kv_buf)。所以第
相邻两个压缩块共享中间 4 个 token → “块间重叠”,压缩后边界信息不被硬切断。
第一个块没有上一块:seq_len==4 特判(c4.cuh:166-174)把
7.4 APE:块内位置偏置([4,2d]->[8,d] 那个 reshape)
score_bias(=ape)shape = c4.cuh:52,407 强校验 8 行)。compressor.py 里 ape 原始 _apply_ape_hotfix:
1 | ape = chunk(ape, 2, -1); ape = cat([ape0, ape1], 0) # [4,d] + [4,d] -> [8,d] |
8 行 = 窗口内 8 个位置各自的可学习 logit 偏置,softmax 前加到 score 上(c4.cuh:187):
作用:让模型学"块内靠后 token 更重要"之类的位置先验。这是"非平均"的第二个来源。
7.5 池化数学(c4.cuh:176-209,安全在线 softmax)
对 head_dim 每个通道
即 softmax 加权平均:权重来自内容 score + 位置 APE,分母是 softmax 归一(非固定 8)。
7.6 归一化 + RoPE 是之后的独立 kernel
c4_forward 只输出 forward_compress = compress_forward(池化)再 compress_fused_norm_rope_inplace。最终
1 | (1) wkv_gate 线性: x_t -> [kv_overlap | kv | score_overlap | score] 每 token |
7.7 数字例子(head_dim 取 2 维、只看通道 0)
生成块
| token | score | token | score | ||||
|---|---|---|---|---|---|---|---|
| 邻块 (overlap) | t0 | 1.0 | -2.0 | 本块 (normal) | t4 | 1.0 | 1.0 |
| t1 | 0.5 | -1.0 | t5 | 0.0 | 2.0 | ||
| t2 | 0.0 | 0.0 | t6 | 3.0 | 0.0 | ||
| t3 | 2.0 | 0.5 | t7 | 1.0 | 1.5 |
APE 通道 0 =
对比简单平均
一句话:CSA 的 4→1 压缩 = 一个 8-key 注意力池化(本块 4 + 邻块 4),权重 =
,最后 norm+rope 定型。 每 的 块 内 位 置
8. 分层 KV cache:同一段历史,多份不同精度
"分层"指的是:同一段历史 token,在不同的池里以不同精度/粒度各存一份,供不同层按需取用。不是一段历史只存一次。
三种精度(由近到远、由精到糙):
| 池 | 粒度 | 谁用 | 特点 |
|---|---|---|---|
| SWA 精确池 | 最近 window_size=128 个 token,1 token 1 条 | 所有压缩层的近处 | 不压缩,最贵最准 |
| c4 压缩池 | 4 token → 1 块 | CSA 层 | 块多、需 top-512 筛 |
| c128 压缩池 | 128 token → 1 块 | HCA 层 | 块少、全读 |
为什么要多份? 因为不同层的 compress_ratio 不同(0/4/128),各层要读的粒度不同:CSA 层读 c4 块、HCA 层读 c128 块、任何压缩层的近处 128 token 读 SWA 精确池。每个压缩层 = SWA 近处(精确)+ 本层那一档远处(压缩),见第 4 节。
每 token 精确 KV 的字节账(deepseek_v4_memory_pool.py:109 断言):
压缩后一个块也是 head_dim=512 级别的一条 latent,但代表 4 或 128 个 token → 这就是省显存的来源。
两种物理摆放:
- 分离池:SWA / c4 / c128 各是独立 tensor
- 统一池
unified_kv(DeepSeekV4UnifiedKVPool):一层一个大 buffer
1 | rows[0, swa_pages) = SWA ring |
举例:一条 700 token 的序列,某 CSA 层要为最新 query 取 KV:
- 最近 128 token(573…700)→ SWA 精确池,逐 token 精确
- 更早 0…572 → c4 压缩池,压成
个块,再用C4Indexer从中选 top-512(此处不足 512 则全取) - 最终该层对 [128 精确 + 选中的 c4 块] 做一次在线 softmax
9. 状态环 state ring:块还没攒满时,压缩状态存哪
问题:压缩要"攒够
类比:传送带 + 记账本
- 每来一个 token,就把它的
记进"记账本"的下一格; - 记满
格(一块),就结算一次(做第 7 节的 softmax 池化),产出 写进压缩池; - 记账本这
格清空,循环复用(所以叫 ring)。
代码(CompressStatePool, deepseek_v4_compress_state.py):
1 | # 非 online: |
ring_size 就是"记账本一圈几格":c4 一圈 8 格(含 overlap 那 4 格),c128 一圈 128 格。online c128 优化:不留 128 格 ring,只滚动维护
10. 地址翻译链:为什么 SWA 里的地址能算出 CSA 块地址
这是核心一问。块地址不是另存一张表,而是纯算术从位置推出来。
类比:同一个人在不同系统里有不同工号,但工号之间有固定换算公式。token 的"绝对位置"就是身份证号,各池里的地址是不同工号。
翻译链(create_paged_compressor_data, compressor.py:263-341):
其中对齐到块起点:clip_down(pos) = pos // ratio * ratio。
为什么 swa_loc 能算出 state_loc、再算出块号? 两条性质:
- 块号
是位置对齐的确定规则,同一块的位 置 个 token 落同一行; - 状态环是 SWA 环的整数细分:当
时 ,于是 就是纯除法,不需要任何单独的分配表。
所以"SWA 地址 → CSA 块地址"只是几步整除/取模,
地址翻译相关源码:
| 函数 | 位置 |
|---|---|
translate_loc_from_full_to_swa |
deepseek_v4_memory_pool.py:640-642 |
translate_from_swa_loc_to_state_loc |
deepseek_v4_compress_state.py:146-152 |
translate_loc_from_full_to_compressed |
deepseek_v4_memory_pool.py:165-246 |
附:关键源码索引
| 主题 | 位置 |
|---|---|
| CSA/HCA 只支持 4x/128x 断言 | compressor.py:86 |
| compress_ratio 逐层配置 | deepseek_v4.py:296-302 |
| Compressor 结构 (wkv_gate/ape/norm) | compressor.py:344-462 |
| 门控投影 kv_score | compressor.py:418-424 |
| 压缩池化 + norm+rope 融合 | compressor.py:143-160 |
| C4Indexer 结构 | indexer.py:641-688 |
| indexer query = wq_b(q_lora) | indexer.py:703 |
| indexer 权重 = weights_proj(x) | indexer.py:714 |
| top-512 变换 | indexer.py:14 (topk_transform_512) |
| unified_kv 缓冲区布局 (SWA+压缩块) | runtime.py:7-12 |
| 三种索引来源说明 (跨层能力) | paged_prefill.py:7-17,36-38 |
| attn_sink 参数 | deepseek_v4.py:385 |
| MLA 骨架维度 (MQA/nope-rope/latent) | deepseek_v4.py:283-305 |
| 第 7 节: overlap softmax 池化 kernel | |
| c4_forward 池化主体 | c4.cuh:114-212 |
| buffer 布局 |kv_overlap|kv|s_o|score| | c4.cuh:43-46, 66-69 |
| 8 槽窗口 本块4+邻块4 (is_overlap) | c4.cuh:138-164 |
| score+APE 后 softmax 加权平均 | c4.cuh:181-209 |
| 首块 seq_len==4 mask overlap | c4.cuh:166-174 |
| window_len 上界 ratio*(1+overlap) | compress.cuh:18-23 |
| ape reshape [4,2d]->[8,d] | compressor.py:390-406 (_apply_ape_hotfix) |
| 第 8 节: 分层 KV cache | |
| 每 token 584 字节断言 | deepseek_v4_memory_pool.py:109 |
| ring_size 表 (c4=8/c128=128, spec×2) | deepseek_v4_memory_pool.py:30-44 |
| 统一池 unified_kv / K_PER_BLOCK | deepseek_v4_memory_pool.py:379-435 |
| 第 9 节: 状态环 state ring | |
| CompressStatePool / KVAndScore | deepseek_v4_compress_state.py:16-159 |
| 第 10 节: 地址翻译链 | |
| full→swa | deepseek_v4_memory_pool.py:640-642 |
| swa→state | deepseek_v4_compress_state.py:146-152 |
| full→compressed | deepseek_v4_memory_pool.py:165-246 |
| paged compressor 地址生成 | compressor.py:263-341 |
- 标题: DeepSeekV4之CSA+HCA
- 作者: 鱿鱼圈
- 创建于 : 2026-07-05 23:50:00
- 更新于 : 2026-07-05 23:22:23
- 链接: https://yuyanqi.com/2026/07/05/DeepSeekV4之CSA+HCA/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。