DeepSeekV4之CSA+HCA

鱿鱼圈 Lv4

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 压成"一个块"。压多狠、压完怎么用,分两档 = CSAHCA


1. CSA 和 HCA 是什么

compressor.py:86 直接写死:

1
2
assert compress_ratio in (4, 128), \
"DSV4 supports CSA(4x) and HCA(128x) only"
  • 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
2
3
compress_ratio = config.compress_ratios[layer_id]  # 逐层配置
assert compress_ratio in [0, 4, 128] # 0=稠密, 4=CSA, 128=HCA
self.compress_ratio = compress_ratio

所以 43 层里,有的层是 CSA、有的是 HCA、少数是 0(不压缩的稠密层)。


2. 压缩器 Compressor 的数学(CSA/HCA 共用)

CSA 和 HCA 用同一个 Compressor 类,只是 ratio 不同。干的事:把连续 个 token 的 KV 压成 1 个压缩块。

重要订正:读了真正的池化 kernel(c4.cuhc4_forward)后确认——它不是"sigmoid 门控加权求和 (GLU)",而是一个小型 attention:在一个窗口上做 softmax 加权平均,权重来自 token 自带的 score 通道 + 位置偏置 APE。下面给正确形式,逐步过程见第 7 节。

代码结构(compressor.py:344-462):

1
2
3
4
5
wkv_gate = Linear(dim -> 2 * coff * head_dim)   # 值 + 打分, 见下
ape = Parameter[ratio, coff * head_dim] # 块内位置偏置(可学习)
# _apply_ape_hotfix reshape 成 [8, head_dim]
norm = RMSNorm(head_dim) # head_dim = 512
coff = 1 + (ratio == 4) # CSA coff=2(块间重叠), HCA coff=1(不重叠)

反推公式(设块 覆盖 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)只有 ,没有 overlap 份。

(C-2) 窗口内 softmax 注意力池化c4.cuh:176-209),对 head_dim 的每个通道

加权平均,不是固定 的平均池化;APE 给"窗口内第 个位置"一个可学习偏置,注入块内顺序先验。

(C-3) 归一化 + RoPE(池化之后的独立 kernel,compressor.py:143-160):

forward_compress = compress_forward(池化)再 compress_fused_norm_rope_inplace

得到的 = 一个压缩块的 KV latent(512 维),写进 KV cache。CSA 的窗口是 8 个 token(本块 4 + 邻块 4,overlap),HCA 窗口就是块内的 128 个。逐步、带数字的完整过程见第 7 节。


3. CSA 独有的 top-k:C4Indexer

HCA 压到 128x,块少,全读即可。CSA 只压 4x,块还很多,要再选 top-512。这个筛选器 C4Indexer 本质就是 V3.2 的 DSA lightning indexer,只是打分对象换成了"4x 压缩块"。

代码结构(indexer.py:655-688):

1
2
3
4
5
6
7
n_heads      = index_n_heads  = 64
head_dim = index_head_dim = 128
index_topk = 512
wq_b = Linear(q_lora_rank -> n_heads * head_dim) # 复用 MLA 的 q_lora!
weights_proj = Linear(dim -> n_heads) # 每头权重 w
compressor = Compressor(ratio=4, is_in_indexer=True, rotate=True)
# indexer 自己独立的一份 4x 压缩 key

公式(走 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 的 完全同构,唯一区别:DSA 的 是单个 token,CSA 的 是 4x 压缩块。

(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
2
3
4
5
unified_kv[L] 布局:
rows [0, swa_pages) = SWA ring <- 近处,精确
rows [swa_pages, ...) = compressed K <- 远处,压缩
HCA(ratio 128): c128_page_indices (二选一,取决于本层 ratio)
CSA(ratio 4) : c4_sparse_page_indices

一层的缓冲区 = 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 的输出(online-softmax 合并,paged_prefill.py:36-38):

HCA 层则是 SWA + 全部 c128 块。两路共享同一个在线 softmax 累加器 ,合并顺序不影响正确性。

attn_sink(V4 特有,deepseek_v4.py:385):每头一个可学习的 softmax 分母偏置:

分母多出的 相当于给 softmax 加一个"虚拟 token",允许某些 query"谁都不太看",长上下文里稳定数值,避免被迫把权重摊到无关的压缩块上。


5. 主注意力仍是 MLA 骨架

deepseek_v4.py:283-305

1
2
3
num_key_value_heads == 1                       # 还是 MQA, 所有 query 头共享一份 KV
qk_nope_head_dim = 448, qk_rope_head_dim = 64 # 还是 nope/rope 分离(decoupled RoPE)
q_lora_rank = 1024, kv_lora_rank = 512 # 还是低秩 latent

所以 CSA/HCA 存进 cache 的那个 ,就是 MLA “压缩 latent + 共享 MQA” 在 V4 的载体,只不过 cache 单位从"1 token 1 条"变成"1 块(4 或 128 token)1 条"。MLA 管"每条 KV 怎么省着存/算",CSA/HCA 管"多少 token 打包成一条、远处怎么筛"。


6. 三句话总结

  1. Compressor(CSA/HCA 共用)把连续 个 token 用"窗口 softmax 注意力池化(权重 = score+APE)+ norm+rope"压成 1 个 512 维块 (C-1~C-3,详见第 7 节), 是 CSA、 是 HCA。CSA 窗口带 overlap(本块 4 + 邻块 4)。
  2. CSA(4x)压得轻块多 → 用和 DSA 同构的 lightning indexer 选 top-512 块(CSA-1/2);HCA(128x)压得狠块少 → 不筛全读。每层二选一(另有极少数 ratio=0 稠密层)。
  3. 每个压缩层用一个在线 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.cuhc4_forward(L114-212)与 buffer 布局注释(L43-46)。

7.1 先破除一个直觉:不是相加平均

很多人以为压缩是 。错。真实是一个 8-key 的小 attention:每个 token 自带一个 score(logit),过 softmax 得到数据依赖的权重,再对 value 加权平均。所以叫"注意力池化"。

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)只有 ,没有 overlap。

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
2
const bool is_overlap = i < 4;                       // L144
src += (is_overlap ? 0 : overlap_stride); // 选 kv_overlap 段 或 kv 段 (L161)

前 4 个来自上一块存储页(load_first_page / kv_score_overlap_buf),后 4 个来自当前块页(kv_buf)。所以第 个压缩块实际"看"的原始 token 是:

相邻两个压缩块共享中间 4 个 token → “块间重叠”,压缩后边界信息不被硬切断。

第一个块没有上一块:seq_len==4 特判(c4.cuh:166-174)把 的 kv 清零、score 填 → overlap 4 槽权重为 0 → 退化成只看本块 4 个。

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 只输出 。RMSNorm 和 RoPE 在另一个 kernel:forward_compress = compress_forward(池化)再 compress_fused_norm_rope_inplace。最终 写回压缩池。完整三段流水线:

1
2
3
(1) wkv_gate 线性:   x_t -> [kv_overlap | kv | score_overlap | score]   每 token
(2) c4_forward: 8-token 窗口 softmax(score+APE) 加权平均 -> c_raw
(3) fused_norm_rope: RMSNorm + RoPE -> c_b

7.7 数字例子(head_dim 取 2 维、只看通道 0)

生成块 (覆盖 token 4…7,借用 token 0…3)。各 token 的

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 =

对比简单平均 。可见权重被 score+APE 拉偏、分母是 softmax 归一、邻块也参与(权重小)。 再过 RMSNorm+RoPE 得

一句话: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_kvDeepSeekV4UnifiedKVPool):一层一个大 buffer
1
2
3
rows[0, swa_pages)   = SWA ring
rows[swa_pages, ...) = 压缩块(c4 或 c128, 取决于本层)
K_PER_BLOCK = {0:0, 4:32, 128:1} # 一个物理块装几个逻辑压缩块

举例:一条 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 才能出 1 个块"。可是解码是一个 token 一个 token 来的,攒到一半的中间状态放哪?答案:一个每 slot 的循环缓冲(ring),攒满一块就 flush。

类比:传送带 + 记账本

  • 每来一个 token,就把它的 记进"记账本"的下一格;
  • 记满 格(一块),就结算一次(做第 7 节的 softmax 池化),产出 写进压缩池;
  • 记账本这 格清空,循环复用(所以叫 ring)。

代码(CompressStatePool, deepseek_v4_compress_state.py):

1
2
3
4
5
# 非 online:
last_dim = 2 * (1 + overlap) * head_dim # CSA overlap -> 4*head_dim
ring_size = c4 -> 8, c128 -> 128 # spec 时 16/256 [memory_pool.py:30-44]
# KVAndScore: 前半 kv, 后半 score; clear() 把 score 填 -inf(=还没写的位置权重0)
# 写状态: set_state_by_state_loc(state_loc, value) 然后把哨兵行[-1]清空

ring_size 就是"记账本一圈几格":c4 一圈 8 格(含 overlap 那 4 格),c128 一圈 128 格。online c128 优化:不留 128 格 ring,只滚动维护 聚合量,ring_size 塌成 1。


10. 地址翻译链:为什么 SWA 里的地址能算出 CSA 块地址

这是核心一问。块地址不是另存一张表,而是纯算术从位置推出来。

类比:同一个人在不同系统里有不同工号,但工号之间有固定换算公式。token 的"绝对位置"就是身份证号,各池里的地址是不同工号。

翻译链(create_paged_compressor_data, compressor.py:263-341):

其中对齐到块起点:clip_down(pos) = pos // ratio * ratio

为什么 swa_loc 能算出 state_loc、再算出块号? 两条性质:

  1. 块号 位置对齐的确定规则,同一块的 个 token 落同一行;
  2. 状态环是 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 进行许可。
评论