KV Cache 笔记

“Pensar es olvidar diferencias, es generalizar, abstraer. En el abarrotado mundo de Funes no había sino detalles, casi inmediatos.”

“思考就是遗忘差异,就是概括,就是抽象。在富内斯那个塞得过满的世界里,除细节之外别无长物,而那些细节,近得迫在眉睫。”

(Jorge Luis Borges《Funes el memorioso》,收于《虚构集》)

“8 月 26 日”

蝉还在叫。

八月的最后几天,蝉声反而更急。

屋顶上只有水塔的影子是凉的。

我躺在那块影子里,想一件很没出息的事。

冰麦茶为什么只有夏天好喝呢。

……不对。

人到底是靠记住活着,还是靠忘掉活着。

啊哈哈,想太多了。

猜拳的结果是我输。

我:「为什么每次都是我?」

水科:「因为你每次都出石头」

水科:「我记着……」

她从书包侧袋里抽出那本笔记本。

方格的,A5,边角已经磨白。

她翻到某一页,在最下面一行添了个正字。

我:「……你连猜拳都记?」

水科:「不记就会输第二次……」

楼下的贩卖机咔了一声。

掉出来的是常温的。

我:「温的……」

水科:「最上面那排被晒了一整天。你输了……」

三日月坐在排风扇旁边,两只脚悬着。

三日月:「呵呵……温的也很好……」

三日月:「温的东西,记得自己凉过……」

我:「喂,你刚那个正字写歪了……」

水科:「不擦」

我:「为什么不擦?」

水科:「写过的地方不许改……」

我:「是规矩?」

水科:「是成本」

水科:「擦掉重写,比在末尾另起一行贵……」

她只在末尾写。

写过的地方不许涂改。

撕掉的页,也还是写过的页。

我:「那本子会越来越厚呢」

水科:「所以后面要撕……」

我:「撕掉不就等于没写过?」

水科:「撕掉的是页」

水科:「不是写过这件事……」

我把瓶身的水珠抹在裤子上,抹出一道深色。

……啊。那不就是我吗。

只在末尾追加,从不回头改,写满就撕,撕了还记得自己写过。

宏大命题一出来,脑子里只剩一条裤子上的水痕。

对我来说,那就只有这种程度。

我:「所以说,你那本子就是 KV cache 吧」

水科:「超1000蠢……」

水科:「不过方向对了一半」

水科:「它确实只写不改……」

第一章 Append Only 「只写不改的过去」

“8 月 26 日”

同一片屋顶,太阳偏了十度。

影子的边缘从排水沟挪到了我的脚尖。

我:「那本子和 KV cache,到底哪里像?」

水科:「哪里都不像……」

水科:「像的是它们要解决的问题」

我:「什么问题?」

水科:「过去……」

我:「……过去?」

水科:「已经算过的东西,要不要留」

水科:「留了就要付地方……」

她翻开本子,让我看那一页。

方格里密密麻麻,全是从左到右添上去的字。

没有一处被划掉。

我:「今年这套库,改了什么?」

水科:「两件事……」

水科:「一是把 Cache 拆成逐层的对象」

水科:「二是把连续批处理的调度器搬进了主库……」

我:「像把仓库管理员请进了办公室」

水科:「嗯……」

水科:「以前 cache 是推理的一个小技巧」

水科:「现在它是显存账单上的主角……」

我:「主角……?」

水科:「参数可以不涨,上下文一定要涨」

水科:「一个 128K 的请求……」

水科:「能把 70B 的权重比下去」

我:「比下去?」

水科:「等下给你算……」

我:「三年前不是三行指针就够了吗?」

水科:「你倒记得……」

水科:「2023 年那个 llama2.c……」

水科:「cache 就是一整块 malloc 出来的扁平数组」

水科:「加三行指针算术……」

水科:「那就是它的全部定义」

我:「现在呢?」

水科:「现在它是一个 list[CacheLayerMixin]……」

我:「同一样东西的两种活法」

水科:「同一样东西的两种活法……」

风把她的刘海吹起来一点,又放下。

我:「那你说的是哪一版?」

水科:「5.16.1」

水科:「本机 site-packages 底下那个 transformers……」

我:「完整路径呢?」

水科:「/usr/lib/python3.14/」

水科:「site-packages/transformers……」

我:「行号过两个月会不会就对不上了?」

水科:「会对不上……」

水科:「装机版不 pin commit……」

水科:「所以我只保证这一版对得上」

我:「……好诚实」

水科:「不诚实的话,你会照着行号去翻」

水科:「翻不到,然后开始怀疑自己……」

我:「我已经怀疑过好几次了」

水科:「那是你自己的问题……」

蝉停了。

停得太干脆,反而像坏了。

我:「那今天讲什么?」

水科:「先讲为什么只缓存 K 和 V」

水科:「再讲它怎么分配、怎么驱逐……」

水科:「怎么量化,怎么分页」

我:「训练呢?」

水科:「不谈……」

我:「采样呢?」

水科:「也不谈……」

水科:「只谈这一堆张量的一生……」

我:「一生……」

水科:「谁在造它,谁在改它,谁在删它……」

那两条线她写在本子的封皮内侧。

一条叫结构。

GQA、MLA、滑窗、CSA、线性状态,各自要什么形状的 cache。

另一条叫 kernel。

eager、sdpa、flash、flex,各自怎么读 cache。

我:「存什么,和怎么读」

水科:「对……」

我:「……有必要吗,管这个?」

水科:「管 cache 是很有必要的」

水科:「你不管它,它就管你的显存……」

她翻回末尾那一行,在下面又添了一笔。

正字多了一划。

第二章 The Only Cacheable Thing 「唯一可缓存之物」

“8 月 27 日”

便利店的冰柜前,我们进行了长达十五分钟的严肃讨论。

我:「蓝的那个是什么味?」

水科:「苏打……」

我:「红的呢?」

水科:「草莓。你每次都选草莓」

我:「因为夏天……」

水科:「夏天和草莓有什么关系」

我:「没关系。只是想说一次……」

最后我们一人一支,站在店门口把它吃完了。

那是那天最好吃的东西。

比后来算出来的那笔账重要得多。

“8 月 27 日”

屋顶。

水科:「站起来……」

我:「啊?」

水科:「你现在是一个 token」

我:「……为什么我必须是 token?」

水科:「因为管线得有人走一遍……」

她让我从排水沟走到水塔,走一步报一个名字。

input_ids,[B,S],整数,词表下标。

Embedding 查表,32000 变 4096。

32000 是词表,4096 是 hidden,也就是 llama2-7B 的 d。

32 个头,头维 128,一共 32 层。

我走到水塔影子边上。

x,形状 [B,S,4096]。RMSNorm,然后三条支路。

q 是 x 乘 Wq,k 乘 Wk,v 乘 Wv。

各自 view 再 transpose,变 [B,32,S,128]。

我:「k 那条也是 32 个头?」

水科:「7B 是。头数写 H_kv……」

然后 RoPE。只旋转 Q 和 K,V 不转。

我:「为什么 V 不转?」

水科:「位置是拿去打分的」

水科:「不是拿去被加权的……」

写入 cache,update(K, V, layer_idx)。

K 和 V 长成 [B,32,S_kv,128]。

Attention(Q, K_cache, V_cache) 出 [B,32,S,128]。

拼回去过 o_proj,回 [B,S,4096],残差。

RMSNorm,SwiGLU,4096 到 11008 再回 4096,残差。

三十二遍。每层各自一份 cache。

最后 RMSNorm,lm_head,4096 回 32000。

取最后一行采样。

我走完最后一步。

我:「果然这么演一遍很难懂呢……」

水科:「那你自己翻」

我:「简单讲,一整条路里只有一个地方需要历史……」

我:「Embedding 是查表,不产生随序列变长的中间量」

我:「FFN 是逐 token 的 matmul,算完就丢……」

我:「hidden 每层重算」

我:「唯一一处要用到历史全部 token 产物的地方……」

我:「就是 K 和 V」

水科:「所以 KV cache 不是一种优化……」

我:「它是这个结构里唯一可缓存的东西」

水科:「及格……」

蝉。

我:「挂点在哪?」

水科:「RoPE 之后,attention kernel 之前」

水科:「而且只挂在 K 和 V 两条支路上……」

水科:「Q 那条线直接进 kernel,不经过任何存储」

我:「每层一份,互不相干?」

水科:「所以 L 是显存公式的乘数……」

水科:「32 层就是 32 份」

水科:「浅层和深层的长度永远一样……」

水科:「跨层共享是后面的事」

我:「投影矩阵呢?」

水科:「Wq 是 d 乘 H·d_h……」

水科:「Wk 和 Wv 是 d 乘 H_kv·d_h」

水科:「70B 把 H_kv 压到 8……」

水科:「投影矩阵变窄,cache 同步变小」

我:「结构决定形状?」

水科:「第一次现身。展开在后面……」

她把我按回原地。

水科:「Attention 那一格,单头写全」

水科:「softmax(QK 转置除根号 d_h 加 M) 乘 V……」

我:「M 是什么?」

水科:「因果掩膜……」

水科:「j 小于等于 i 的地方是 0,其余负无穷……」

我:「为什么要除根号 d_h?」

水科:「方差补偿」

水科:「q、k 分量近独立等方差时……」

水科:「q 点 k 的方差是 d_h 乘 σ 的四次方」

水科:「不除,softmax 会被推进饱和区……」

我:「饱和区就是……」

水科:「全变成 0 和 1。精度也没了……」

她伸出五根手指,一根一根折下去。

一,打分。z 等于 QK 转置除根号 d_h 加 M。

形状 [B,H,S,S],唯一的 S 平方项。

二,归一化。p 等于 softmax(z, dim=-1),每行和为 1。

三,汇总。o 等于 p 乘 V,[B,H,S,d_h]。

四,拼头。transpose(1,2) 加 reshape,变 [B,S,4096]。

五,投影。o 乘 W^o,唯一跨头混合的一步。

热。

我:「前三步逐头独立?」

水科:「完全独立……」

水科:「这既是多头的并行性来源」

水科:「也是 cache 可以按头切第 2 维的理由……」

我:「7B 的 H 乘 d_h 正好 4096」

水科:「所以 o_proj 是个方阵……」

水科:「逐元素看」

水科:「p_ij 是第 i 个 token 分给第 j 个历史 token 的权重……」

水科:「o_i 是历史 value 的凸组合」

水科:「量级跟 V 同阶……」

我:「每行和为 1 这件事,有什么用?」

水科:「多开一列不参与取值的垃圾桶」

水科:「它不往输出里加任何东西……」

水科:「但它进分母」

水科:「整行的概率就被压下去……」

我:「……sink」

水科:「后面会遇到……」

风把她的头发吹到脸前面。她没有拨开。

我:「工程上还有几个坑?」

水科:「三个……」

水科:「第一,第二步必须先减行最大值」

水科:「llama2.c 里那句 expf(x[i] - max_val) 就是这个……」

水科:「数学不变,fp32 不溢出」

水科:「eager 写成 softmax 显式传 dtype=torch.float32……」

水科:「再 .to(query.dtype)」

水科:「是同一个动机的另一半……」

我:「第二个?」

水科:「第一步那个 M 是加性的,不是布尔」

水科:「[B,1,S,S] 广播到所有头……」

水科:「加进 z 之后由 softmax 自动把未来压成 0」

水科:「所以 eager 里那个 S 乘 S 的掩膜是实打实的内存……」

水科:「kernel 版宁可传 is_causal=True,也不开它」

我:「第三个……」

水科:「每一步的成本不同阶」

水科:「一二三是每头 O(S²·d_h)……」

水科:「四五是 O(S·d²)」

水科:「prefill 两者都要付……」

水科:「到 decode,Q 只剩一行」

水科:「一三降成 O(S·d_h),五还是 O(d²)……」

我:「那单步看是权重主导?」

水科:「权重每层一次,全 batch 共享」

水科:「而 O(S·d_h) 那一份……」

水科:「每层每请求各读一份」

水科:「这就是写 O(1)、读 O(S) 的出处……」

我:「也是并发一高 cache 先撞显存的原因」

水科:「嗯……」

水科:「一三两步要的 K、V 全体历史行」

水科:「就是 update 返回的东西……」

水科:「Q 只用当前那一行。用完即丢」

我:「两阶段呢?」

水科:「同一条管线,差别只在入口的 S……」

水科:「prefill 是 q[1..S] 乘 K[1..S] 转置」

水科:「下三角,一次算完 S 行……」

水科:「decode 是 q[S+1] 乘 K[1..S+1] 转置」

水科:「只多一行,出来长度 S+1 的向量……」

那张对照表她画在本子上,四列。

输入是 [B,S] 和 [B,1]。

Q 是 [B,H,S,d_h] 和 [B,H,1,d_h]。

cache 里的 K、V 是 [B,H,S,d_h] 和 [B,H,S+1,d_h]。

输出是 [B,S,32000] 和 [B,1,32000]。

我:「只有 Q 那一维塌成 1……」

水科:「KV 那一维反而在涨」

水科:「q_len 与 kv_len 不对称……」

水科:「是 decode 的全部特征」

水科:「sdpa 里那句 is_causal = q_length > 1……」

水科:「后面还接 attention_mask is None 和 is_causal」

水科:「读的就是它……」

我:「逐 token 的增量写法呢?」

水科:「z 上标 t 下标 j」

水科:「等于 q_t 点 k_j 除根号 d_k……」

水科:「o_t 是对 j 小于等于 t 求和」

水科:「softmax 之后乘 v_j……」

水科:「分母逐行不同」

我:「所以判据是什么?」

水科:「三条……」

水科:「一,causal mask 让历史行不受新 token 影响」

水科:「第 t 行只依赖 q_t 和 k 的小于等于 t……」

水科:「前 t-1 行的结果永远不变」

水科:「二,K 和 V 会被未来所有行反复读,所以缓存……」

水科:「Q 用完即弃,缓存它是纯浪费」

水科:「三,softmax 乘 V 那个中间量不能缓存……」

我:「为什么?」

水科:「归一化分母每行都要重算」

水科:「要省的是乘数,不是概率矩阵……」

风。

我:「三年前那三行指针长什么样?」

水科:「llama2.c 的 254 到 259 行……」

那三行她抄在本子角上。

loff 等于 l 乘 p->seq_len 乘 kv_dim。

注释写着 layer offset for convenience。

s->k 等于 s->key_cache 加 loff 加 pos 乘 kv_dim。

s->v 同理。

我:「pos 乘 kv_dim 就是第 pos 条 KV……」

水科:「写入即前进指针」

水科:「整个模型只有一个 cache,形状在启动时定死……」

水科:「v5 里它是每层一个对象」

我:「那本账呢?」

水科:「每 token 的 KV……」

水科:「等于 2 乘 L 乘 H_kv 乘 d_h 乘 b」

水科:「b 是每元素字节数,bf16 就是 2……」

我:「系数 2 是 K 和 V 两份」

水科:「四个因子在你走过的那条路上全能指出来……」

水科:「2 是 x 乘 Wk 和 x 乘 Wv 两条支路」

水科:「L 是每层各自一份……」

水科:「H_kv 乘 d_h 是 K、V 投影的宽度」

水科:「b 由 dtype 决定……」

水科:「decode 每走一步往每层追加一条 KV」

水科:「写是 O(1)……」

水科:「attention 要把追加过的全部读一遍」

水科:「读是 O(S)……」

我:「不对称……」

水科:「不对称……」

她从本子最后一页抽出一张便签。

便签上是四个模型,四列数字。

llama2-7B,MHA,32 层 32 头 128 维。

每 token 512KB,4096 上下文,满上下文 2.0GB。

llama2-70B,GQA,80 层 8 头 128 维。

每 token 320KB,4096 上下文,1.25GB。

Qwen3.5-MoE,只有 10 层是全注意力,2 个头 256 维。

每 token 20KB,256K 上下文,5.4GB。

DeepSeek-V4-Flash,43 层 1 个头 512 维。

K 等于 V,再加压缩。

每 token 7.5KB,1M 上下文,7.9GB。

我:「后两行的数从哪来?」

水科:「沿用《架构变迁笔记》的维度自己算的……」

水科:「V4 只算 KV,indexer 和 gate 另计」

水科:「7.5KB 的来处,过两天再给你看……」

我:「70B 要是把上下文拉长呢?」

水科:「4096 扩到 128K,单个请求就是 40GB」

水科:「一条对话吃掉三张卡……」

我把冰棒棍捏出咔的一声。

我:「访存那笔呢?」

水科:「每生成一个 token……」

水科:「要把全部权重读一遍,再把全部 cache 读一遍」

水科:「7B 的 bf16 权重是 13.4GB……」

水科:「cache 每 token 512KB」

水科:「两者在 26K token 处交叉……」

水科:「此后每一步读 cache 的字节数比读权重还多」

我:「并发呢?」

水科:「一张 24GB 的卡装完 7B 剩约 10GB……」

水科:「4K 上下文每条 2.0GB」

水科:「只能塞 5 条……」

我:「五条?」

水科:「权重是定值」

水科:「cache 随序列和 batch 双向涨……」

水科:「长上下文时代,显存的主人是 cache」

水科:「不是权重……」

蝉又叫起来了。

我把冰棒棍丢进垃圾桶,又走回去捡了出来。

第三章 Three Roles, One Tensor 「同一份数据的三种身份」

“8 月 27 日”

太阳落到水塔后面去了。

屋顶一下子凉下来,凉得很没道理。

水科:「刚才你走过的那条路,中间有一格」

我:「哪一格?」

水科:「update 那一格……」

我:「就是写进本子的那一下」

水科:「它不是一下。它是三下……」

我:「三下?」

水科:「同一份数据,在同一次 forward 里」

水科:「有三种身份……」

她在新一页的最上面写了个 252。

水科:「models/llama/modeling_llama.py……」

水科:「252 到 277 行」

水科:「两处长参数列表,我合并成了一行……」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# models/llama/modeling_llama.py:252-277(两处长参数列表被合并成行)
query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2)
key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)

cos, sin = position_embeddings
query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)

if past_key_values is not None:
key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx) # :262

attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface(
self.config._attn_implementation, eager_attention_forward) # :264
attn_output, attn_weights = attention_interface(
self, query_states, key_states, value_states, attention_mask,
dropout=0.0 if not self.training else self.attention_dropout, scaling=self.scaling, **kwargs)

我:「看着挺普通」

水科:「普通得刚好……」

水科:「第一种身份,写入」

水科:「update 在 262 行……」

水科:「返回的是合并后的全体 K 和 V」

水科:「不是增量……」

我:「不是增量?」

水科:「attention 那一侧根本看不到 cache 这个概念」

水科:「它只看到 key_states 变长了……」

我:「就像我看不出你本子厚了」

我:「只看出你写到了新的一行……」

水科:「别拿本子打比方」

我:「为什么?」

水科:「因为你会拿错……」

蝉。

我:「第二种?」

水科:「读出……」

水科:「kernel 读的是返回值的 dim=-2」

水科:「shape 约定 [B, H_kv, S, d_h]……」

水科:「序列在第 3 维,头在第 2 维」

我:「所以刚才那句按头切第 2 维?」

水科:「就是这里兑现的……」

我:「第三种呢?」

水科:「定位」

水科:「get_seq_length(),在 387 行……」

水科:「供 mask 与 cache_position 反推位置」

我:「反推……」

水科:「位置信息的真相」

水科:「从外部计数器迁到了 cache 本身……」

我:「以前是有人站在外面数着数」

水科:「现在是本子自己知道写到第几页……」

三日月不知什么时候坐到了排风扇上。

三日月:「呵呵……本子上写着的,其实是页码」

我:「不是字吗?」

三日月:「字也是页码……」

我:「……」

水科:「别理她。记住支点」

我:「支点?」

水科:「接口只有一个方法……」

水科:「update(k, v, layer_idx),返回 (k, v)」

水科:「所以换 cache 实现不需要动模型代码……」

我:「后面那些结构,每一种?」

水科:「全挂在这个签名上」

我:「后面那些容器,每一个类?」

水科:「也全挂在这个签名上……」

她合上本子,用铅笔敲了敲封面。

水科:「还有一件事,你现在看不出来」

我:「什么?」

水科:「RoPE 发生在 update 之前……」

我:「代码里就是这个顺序啊」

水科:「这不是偶然,是硬性契约……」

我:「违反了会怎样?」

水科:「等你自己踩一次就知道了」

我:「你倒是先说……」

水科:「说了你也不会记」

水科:「踩一次就会记……」

影子已经完全没了。

水塔变成一块黑。

第四章 Shape Is Decided Elsewhere 「形状由谁决定」

“8 月 28 日”

电车上只有我们三个,和一个睡着的上班族。

窗外是坡道,坡道上面是水塔。

水科把本子摊在膝盖上,翻到有字的那面,又翻过去。

我:「你在找什么?」

水科:「找一张干净的页」

我:「没有干净的页了?」

水科:「有。我先让你看反面……」

反面也写了字。

一模一样的字。

正面是什么,反面就是什么。

连歪掉的那个正字都在同一个位置。

我:「……这是抄的?」

水科:「这是同一份」

我:「同一份怎么会出现在两面?」

水科:「因为纸只有一张……」

电车晃了一下。

我:「那正面和反面,哪个是真的?」

水科:「你问错人了……」

三日月:「呵呵……」

水科:「transformers 里有一张官方映射表……」

水科:「cache_utils.py,1223 到 1244 行」

水科:「DYNAMIC_LAYER_TYPE_MAPPING……」

水科:「一个 config 字段,决定整条显存曲线」

我:「念给我听……」

她把那一页转过来,我照着念。

full_attention 对 DynamicLayer。

sliding_attention 对 DynamicSlidingWindowLayer。

chunked_attention 也对 DynamicSlidingWindowLayer。

conv 对 LinearAttentionLayer。

它不存 per-token KV,只存 conv state。

linear_attention 也对 LinearAttentionLayer。

hybrid 对 LinearAttentionAndFullAttentionLayer。

hybrid_sliding 对

LinearAttentionAndSlidingWindowAttentionLayer。

deepseek_sparse_attention 对 DynamicIndexedLayer。

那是 CSA 加 indexer。

qwen_sparse_attention 也对 DynamicIndexedLayer。

moe 对 LinearAttentionLayer。

mlp 也对 LinearAttentionLayer。

不缓存,只占一个空槽。

我:「分块和滑窗是同一个类?」

水科:「对 cache 而言滑窗就是分块」

水科:「只差一个 mask……」

我:「moe 也要 cache?」

水科:「不要。mlp 也不要」

我:「为什么要占一个空槽?」

水科:「等下再说……」

到站了。没有人下车。

我:「这张表怎么被调用?」

水科:「DynamicCache 传一个 config,一行就调用它……」

水科:「get_layer_types_and_kwargs 在 1701 行」

水科:「从 config.layer_types 逐层 dispatch……」

水科:「1793 行把每一层造出来」

水科:「对每一种层型,拿表里的类加关键字参数……」

我:「要是有人想插队呢?」

水科:「子类可以自带注册」

水科:「_layer_type 配 init_subclass,38 到 42 行……」

水科:「在建模文件里写个类,它就进表了」

我:「谁这么干过?」

水科:「DeepSeek-V4……」

我:「……插队?」

水科:「插队……」

我:「那头数呢?」

水科:「每 token 的账是 2L·H_kv·d_h·b……」

水科:「MHA 时 H_kv 等于 H_q」

水科:「MQA 时 H_kv 等于 1……」

水科:「GQA 介于两者之间」

水科:「H_kv 是 cache 的除数……」

我:「repeat_kv 在哪一步?」

水科:「modeling_llama.py 的 179 到 189 行」

水科:「先在第 2 维后面插一个 None……」

水科:「expand 出 n_rep 份,再 reshape 回去」

我:「它在 update 之前还是之后?」

水科:「之后。在 kernel 之内……」

水科:「eager_attention_forward 的 202 到 203 行」

水科:「所以 cache 里永远只存 H_kv 份……」

水科:「H_q 那份是读的时候展开出来的幻觉」

我:「幻觉?」

水科:「sdpa 更进一步……」

水科:「enable_gqa=True,sdpa_attention.py 的 102 行」

水科:「连展开都交给 kernel……」

我:「70B 是 64 比 8」

水科:「八分之一。这一刀砍的是分母……」

电车过了道口。

我:「层数也能砍?」

水科:「cache_utils.py 的 1717 到 1720 行……」

水科:「getattr 拿 num_kv_shared_layers」

水科:「不为空且大于 0……」

水科:「就把 layer_types 从尾部切掉那么多」

我:「尾部若干层不建 cache?」

水科:「复用前层的 KV……」

水科:「Gemma3n 这一路。CLA 和 YOCO 都是一路」

水科:「对 cache 而言……」

水科:「少几层和少几个头是同一个动作」

我:「都是给分母做除法?」

水科:「嗯……」

我:「那 MLA 呢?」

水科:「它缓存的不是 K 和 V,是潜变量」

水科:「每 token 的账变成 L 乘 (d_c 加 d_r) 乘 b……」

我:「系数 2 呢?」

水科:「没了」

我:「没了?」

水科:「K 与 V 同源,一份存储两用……」

那一页的边角上写着几个数。

d_c 是 kv_lora_rank,512。

d_r 是 qk_rope_head_dim,64。

d_kn 是 128,d_v 也是 128。

V3 是 61 层。

(512 加 64) 乘 61 乘 2 字节,每 token 70KB。

等价的 MHA 是 128 乘 320 乘 61 乘 2 字节。

每 token 4.9MB。

我:「差多少?」

水科:「70 倍……」

我吸了一口凉气,被自己的口水呛到。

我:「实现在哪?」

水科:「models/deepseek_v3 底下」

水科:「modeling_deepseek_v3.py 的 456 到 471 行……」

那一段她也抄了,抄得很小。

kv_a_proj_with_mqa 出压缩结果。

torch.split 在最后一维切成两片。

切点是 kv_lora_rank 和 qk_rope_head_dim。

kv_nope 过 kv_a_layernorm。

两片都 view 成 batch、1、seq_length、各自的维。

注释是原文:Both latents are viewed as

single-head, 4D tensors so all cache layers

handle them correctly。

还有一句:Cache read / write is performed

while latent KV is still compressed。

update 收的是 kv_nope 和 k_rot。

出来还是它们两个。

之后才 expand_kv,419 到 437 行。

我:「头数是 1?」

水科:「单头、四维,好让所有 cache 层都能处理……」

水科:「读写都发生在潜变量还被压着的时候」

我:「展开在 cache 之后?」

水科:「所以 DynamicLayer 一行没改,就接住了 MLA……」

水科:「它只要求 dim=-2 在增长,第 2 维是头数」

水科:「这里头数等于 1,它也认……」

我:「k_rot 那边呢?」

水科:「k_rot.expand(-1, k_nope.shape[1], -1, -1)」

水科:「注释里明说……」

水科:「does not affect the underlying storage」

三日月:「假设 1……K 和 V 是同一个人的两个名字」

我:「两个名字?」

三日月:「假设 2……它们从来就是一个人」

三日月:「只是被写了两遍……」

三日月:「假设 3……所谓两遍,是有人希望它有两遍」

我:「那、那也太……」

三日月:「假设 4……那个人一直站在 cache 外面」

三日月:「被展开的只是影子……」

我:「啊哈哈……那正在被展开的我,又算什么呢」

三日月:「展开,并不影响底下的存储……」

三日月:「这些都只是注释……」

三日月:「你想要多少,就可以增加到多少」

电车停了。

门开了一会儿,又关上。没有人上来。

我:「V4 更狠吧?」

水科:「V4-Flash 的 K 和 V……」

水科:「在 cache 层就是同一块内存」

我:「同一块……」

水科:「等下给你看那一行」

我:「还有那个 sink 呢?」

水科:「V4 那个可学习 sink 叫 s_aux……」

水科:「进分母即丢」

水科:「flash_attention.py 的 37 行……」

水科:「它走 kernel 参数,不占 cache 一格」

我:「连一格都不占?」

水科:「连一格都不占……」

第五章 Thinner, Thinner 「削薄的那一刀」

“8 月 28 日”

回到屋顶的时候,天已经黄了。

水科从书包里拿出一把美工刀。

我:「你要干什么?」

水科:「削……」

她在本子右边比了比,没有划下去。

我:「那一列为什么不能写?」

水科:「留给还没来的东西……」

我:「滑窗?」

水科:「cache_utils.py 的 244 到 258 行」

水科:「DynamicSlidingWindowLayer 的 update……」

1
2
3
4
5
6
7
8
# cache_utils.py:244-258(DynamicSlidingWindowLayer.update 节选)
self.cumulative_length += key_states.shape[-2]
full_key_states = torch.cat([self.keys, key_states], dim=-2)
full_value_states = torch.cat([self.values, value_states], dim=-2)
if not self.record_past:
self.keys = full_key_states[:, :, -self.sliding_window + 1 :, :]
self.values = full_value_states[:, :, -self.sliding_window + 1 :, :]
return full_key_states, full_value_states

我:「W 减 1……」

水科:「留 W 减 1,不留 W」

水科:「新 token 自己占第 W 个位置……」

我:「返回的却是 full?」

水科:「返回 full,缓存窗口」

水科:「kernel 看到的仍是当前行可见的全部……」

水科:「驱逐对它透明」

水科:「窗口左边界靠 get_mask_sizes 报给 mask……」

水科:「263 行」

我:「账怎么算?」

水科:「这次是整条序列的量,不是每 token……」

水科:「2 乘 L 乘 min(S, W 减 1)」

水科:「再乘 H_kv 乘 d_h 乘 b……」

水科:「一到 W 减 1 就不再随 S 增长」

水科:「摊到每 token……」

水科:「是 2L·H_kv·d_h·b 再乘 min(1, (W-1)/S)」

水科:「越长越薄……」

我:「W 一般多大?」

水科:「config 里的 sliding_window」

水科:「Gemma2 是 4096,Gemma3n 是 512……」

水科:「GPT-OSS 是 128」

水科:「mistral 的默认值已经涨到 4096 了……」

我:「代价呢?」

水科:「远处全盲」

水科:「所以实践里滑窗总要配少数几层全注意力……」

她把刀收起来,翻到本子中间。

中间有一叠页被抽掉了,只夹着一张便签。

便签上是一串很小的数字。

我:「……几十页变成一张」

水科:「CSA 和 HCA。把历史压成条目……」

水科:「V4 的两个新层类型靠 _layer_type 自动进表」

水科:「modeling_deepseek_v4.py 的 185 行和 265 行……」

那一页她抄得极小,行号挤在括号里。

HCA 的类直接继承滑窗层,类定义在 162 行。

_layer_type 写的是

heavily_compressed_attention,185 行。

init 里从 config.compress_rates 取自己那一份,189 行。

update 在 195 行。

docstring 是原文:Shared sliding-window K=V

update body. V4 uses shared-KV MQA,

so keys and values point to the same storage

on every layer。

还没初始化就 lazy_initialization。

然后一句 self.values = self.keys。

接着 cumulative_length 加上来的长度,cat,取尾部 W 减 1。

再一次 self.values = self.keys。

最后 return full, full。

我:「系数 2 呢?」

水科:「就在这一行真的消失了……」

风把便签掀起一个角。她按住。

我:「窗口外面还挂着什么?」

水科:「三个 dict,190 到 193 行……」

buffer_kv 和 buffer_gate,攒不够一个窗的零头。

compressed_kv,已经发射出去的条目。

entry_count,乘上 compress_rate 就是绝对位置。

压缩条目的 RoPE 打在这个窗位上。

rope_layer_type 是 compress,412 行。

store_compression_weights 在 209 行。

只剥掉最长的那个窗对齐前缀。

剩下的留在 buffer 里,等下一次 forward。

CSA 比 HCA 多一路 indexer,265 到 272 行。

还有交叠窗的 overlap_kv 和 overlap_gate,275 到 290 行。

只存上一窗的 Ca 片,Cb 折完就丢。

我:「账呢?」

水科:「每 token 等于 b 乘 d 乘 Σ 1/r……」

水科:「d 是 512,b 是 2」

水科:「r 逐层取 1、4 或者 128……」

水科:「前面没有系数 2,因为 K 等于 V」

我:「43 层怎么分?」

水科:「2 个滑窗,21 个 CSA,20 个 HCA……」

水科:「Σ 1/r 约等于 2 加 21 除 4 加 20 除 128」

水科:「是 7.36……」

水科:「乘上 b·d 也就是 1024」

水科:「得每 token 7.5KB……」

我:「不压缩呢?」

水科:「43 乘 1024,44KB」

水科:「差接近 6 倍……」

水科:「滑窗那两层还要再受 W 截断」

我:「昨天那个 7.5KB 就是这么来的?」

水科:「便签上那张表的数字,就是这么来的……」

她把便签重新夹好。

水科:「一条路线……」

水科:「把 cache 从逐 token 变成逐条目」

水科:「检索交给 indexer……」

水科:「msa_attention.py 的 230 行」

水科:「按 block 打分选 top-k……」

屋顶的门被推开了。

神代:「削薄!削薄!把一切都削薄!」

神代:「Sliding Window 是我的刀!」

神代:「CSA 是我的秤!HCA 是我的坟场!」

神代:「一百二十八分之一的压缩率!」

神代:「44KB 变成 7.5KB!」

神代:「这不是节省!这是我对整块显存的加冕!」

神代:「远处的东西就该被忘掉!被忘掉的东西才配留下!」

神代:「W 减一!只留W减一!」

神代:「新来的那一个,自己站着!」

神代:「决定谁能留下的,从来只有我一个人!」

我:「……啊」

我的麦茶被她的披风扫到地上。

瓶子滚了两圈,停在水塔影子外面。

神代已经不见了。门还在晃。

水科:「还有第三种削法」

我:「嗯……」

水科:「不削,换掉」

水科:「线性注意力,把 cache 换成状态……」

LinearAttentionCacheLayerMixin 在 891 行。

LinearAttentionLayer 在 1003 行。

另一对写入方法:update_conv_state 在 1037 行。

update_recurrent_state 在 1077 行。

多状态时 config.number_of_conv_states

给 number_of_states,1731 行。

我:「状态多大?」

水科:「H_v 乘 d_k 乘 d_v 乘 b……」

水科:「加上 (k 减 1) 乘 d_qkv 乘 b」

水科:「第一项是记忆矩阵 S_t……」

水科:「第二项是 kernel 为 k 的因果卷积缓冲」

水科:「和序列长 S 无关……」

我:「get_max_length() 呢?」

水科:「对这类返回 -1」

水科:「含义是不适用,不是无限……」

我:「hybrid 呢?」

水科:「LinearAttentionAndFullAttentionLayer」

水科:「1094 行……」

水科:「一个对象里同时挂状态和 KV」

我:「Qwen3.5-MoE 那个 40 层只有 10 层产生 KV?」

水科:「真身就是这张表里……」

水科:「hybrid 与 full_attention 按 3 比 1 交替」

我:「moe 和 mlp 那两个空槽呢?」

水科:「1242 和 1243 行……」

水科:「也被塞成 LinearAttentionLayer」

水科:「只为 get_seq_length() 之类……」

水科:「能从正确的层取到值」

水科:「它们永远是空的……」

我掏出手机。

————————————————————————

1∶本校生无名氏∶2026-08-28(金) 19:04:11 ID:kvLORA
本机 models 底下的 configuration 文件
一共 496 个 www
只统计类属性里写死的默认值,不下权重

2∶本校生无名氏∶2026-08-28(金) 19:07:33 ID:MHA032
llama 和 qwen2、qwen3 主干这类默认是 None
由 checkpoint 的 config.json 决定
不在计数里(笑)

3∶本校生无名氏∶2026-08-28(金) 19:11:02 ID:gqa72
显式同时给了 num_attention_heads
与 num_key_value_heads 的 101 个里
GQA 72,MHA 28,MQA 1

4∶本校生无名氏∶2026-08-28(金) 19:14:48 ID:stablelm
老实 MHA 只剩 gemma 一代 16/16、stablelm 32/32
还有 chameleon、helium、dia
moonshine_streaming 这批

5∶本校生无名氏∶2026-08-28(金) 19:16:20 ID:64to1
类属性里写死 MQA 的只剩 deepseek_v4 的 64/1
paligemma 和 pi0 写在子 config 里的也是 1 www

6∶本校生无名氏∶2026-08-28(金) 19:19:57 ID:kv8
GQA 那一堆:mistral 与 mixtral 32/8
gemma2 与 gemma3 8/4、gemma3n 8/2
qwen3_moe 32/4、qwen3_next 与 qwen3_5_moe 16/2
glm4 32/2、glm4_moe 96/8

7∶本校生无名氏∶2026-08-28(金) 19:22:08 ID:kv8b
续上:hy_v3 与 gpt_oss 64/8、jamba 32/8
minimax_m2 48/8、phimoe 32/8
solar_open 64/8、starcoder2 24/2、inkling 64/8

8∶本校生无名氏∶2026-08-28(金) 19:24:31 ID:lora12
认 kv_lora_rank 的 12 家:deepseek_v2、v3、v32
minicpm3、longcat_flash、glm4_moe_lite
glm_moe_dsa、glm5_next、mistral4
youtu、axk1、axk2

9∶本校生无名氏∶2026-08-28(金) 19:26:44 ID:swa44
sliding_window 非 None 的 44 家:mistral
gemma2、gemma3、gemma3n、gpt_oss 128、cohere2
modernbert、granite_swa、olmo3、ministral
phi3、mimo_v2_flash

10∶本校生无名氏∶2026-08-28(金) 19:28:02 ID:chunk8192
分块认 attention_chunk_size
llama4 是 8192,还有 gemma4
KV 跳层共享认 num_kv_shared_layers
gemma3n 写了 15

11∶本校生无名氏∶2026-08-28(金) 19:31:17 ID:hyb15
layer_types 里 linear_attention 17 家、hybrid 15 家
mamba、mamba2、falcon_mamba、recurrent_gemma
bamba、zamba、zamba2、nemotron_h
qwen3_next、qwen3_5 与 qwen3_5_moe、olmo_hybrid
granitemoehybrid、lfm2、minimax、inkling、zaya

12∶本校生无名氏∶2026-08-28(金) 19:33:40 ID:indexer
带 indexer 的稀疏:deepseek_v32、deepseek_v4
qwen4_exp 的 qwen_sparse_attention
minimax_m3_vl、glm_moe_dsa、glm5_next
axk2、cohere2_moe、exaone_moe

13∶本校生无名氏∶2026-08-28(金) 19:35:55 ID:rate128
CSA 与 HCA 的压缩率
deepseek_v4 的 default_compress_rates
是 CSA 4、HCA 128

14∶本校生无名氏∶2026-08-28(金) 19:37:20 ID:sink8
认 s_aux 的 sink 有 8 家:gpt_oss、granite_swa
mimo_v2_flash、openai_privacy_filter
deepseek_v4

15∶本校生无名氏∶2026-08-28(金) 19:38:12 ID:4cycle
nemotron_h 的默认层型直接就是四元循环
linear_attention、moe、full_attention、mlp www

16∶本校生无名氏∶2026-08-28(金) 19:40:03 ID:>>3

3 GQA 压倒性默认(笑)
MHA 那 28 个里一大半其实是 MLA 家族
deepseek_v3、v32、glm4_moe_lite、glm_moe_dsa
glm5_next、axk1、axk2、youtu
它们的 num_key_value_heads 已经失去意义了
当年 MQA 论文说的是共享一头换速度
deepseek_v4 叠的是 K 等于 V 加压缩,不是一条路线

17∶本校生无名氏∶2026-08-28(金) 19:42:36 ID:128to128
别只盯 num_key_value_heads
deepseek_v32 写的是 128/128,看上去像 MHA
实际 cache 里存的是 kv_lora_rank 那份潜变量
判结构要 kv_lora_rank、compress_rates
layer_types 一起看

18∶本校生无名氏∶2026-08-28(金) 19:45:00 ID:lastpost
纯结构几乎绝迹,混合才是常态
同一个模型里每层 cache 长得不一样
这就是 v5 把 cache 做成逐层对象的直接理由

————————————————————————

我:「数完了」

水科:「想自己复核的话……」

水科:「grep -l kv_lora_rank」

水科:「打进 models 底下所有 configuration 文件……」

水科:「再拿各家的 layer_types 默认值一对就行」

我:「496 这个数,你刚才说的是 497……」

水科:「……」

水科:「多数了一个 init.py」

水科:「行号我背得出来,文件名我背不出来……」

我:「啊哈哈,你也有这种时候」

水科:「闭嘴……」

天彻底黑了。她把本子举到我面前。

最后一页是九行,四列。

MHA,每 token 2L·H_q·d_h·b。

标签 full_attention,类是 DynamicLayer。

MQA,2L·d_h·b。同一个标签,H_kv 等于 1。

GQA,2L·H_kv·d_h·b。同一个标签,同一个类。

跨层共享,2(L 减 L_s)·H_kv·d_h·b。

认 num_kv_shared_layers,做法是少建若干层。

MLA,L(d_c 加 d_r)b。

标签还是 full_attention,类还是 DynamicLayer。

存的是潜变量。

滑窗和分块,2L·H_kv·d_h·b 乘 min(1, (W-1)/S)。

标签 sliding_attention 或 chunked_attention。

类是 DynamicSlidingWindowLayer。

CSA 和 HCA,b·d·Σ 1/r,K 等于 V。

标签 deepseek_sparse_attention。

类是 DynamicIndexedLayer 和 DeepseekV4CSACache。

线性注意力。

(H_v·d_k·d_v·b 加 (k-1)·d_qkv·b) 除以 S。

整块是常数。标签 linear_attention 或 hybrid。

类是 LinearAttentionLayer 和它的混血。

MoE 和 MLP 层,0。

标签 moe 或 mlp。

类还是 LinearAttentionLayer,一个空槽。

我:「……九行」

水科:「代回昨天那本账」

水科:「512KB,到 GQA 的 320KB……」

水科:「到 MLA 的 70KB」

水科:「再到窗口截断,再除以 r……」

水科:「最后到 0」

我:「layer_types 一个列表?」

水科:「就画出了一个模型的显存曲线……」

她只在末尾写。

写过的地方不许涂改。

撕掉的页,也还是写过的页。

我:「那被撕掉的,去哪儿了?」

水科:「去哪儿了,得问下一页……」

第六章 Twenty-Three Lines 「老老实实摊开来」

“8 月 29 日”

贩卖机前排队排了四个人。

我们排了二十分钟,只为讨论要不要换一台。

我:「那台好像亮着」

水科:「那台昨天吞过我的硬币……」

我:「所以换?」

水科:「所以不换」

我:「为什么啊?」

水科:「它欠我的……」

最后我们还是换了。

买到的还是常温的。

“8 月 29 日”

坡道上面,水科把本子摊在膝盖上。

翻到写着 191 的那一页。

水科:「存什么讲完了……」

水科:「现在讲怎么读」

我:「四种吧?」

水科:「四种。共享同一句结论……」

水科:「要 dense,要连续,要全量」

我:「先说 eager……」

水科:「本子上抄着那 23 行」

水科:「你自己念……」

modeling_llama.py 的 191 到 214 行。

开头两句是 repeat_kv,key 和 value 各展开一次。

torch.matmul(query, key_states.transpose(2, 3))。

再乘 scaling。

如果 attention_mask 不是 None,就把它加上去。

softmax 在 dim=-1,显式传 dtype=torch.float32。

再 .to(query.dtype)。

dropout。

torch.matmul(attn_weights, value_states)。

transpose(1, 2).contiguous()。

返回 attn_output 和 attn_weights。

我:「念完了……有什么用?」

水科:「它是唯一返回 attn_weights 的实现……」

水科:「所以 output_attentions=True 只认它」

水科:「所以它是 debug 用的……」

我:「掩膜呢?」

水科:「就是那天说的那种加性浮点数」

水科:「不再是 bool……」

水科:「create_causal_mask 在 864 行」

水科:「eager_mask 在 538 行……」

我:「代价呢?」

水科:「你自己算」

我:「……好」

下面请允许我请出讲师来解说。

哈——哈——哈——哈——哈!

向 YOU 们提个问题 NE!

一个 8K 的序列,32 个头!

中间那张注意力矩阵,要多大?来 SHOW!

OH~,不对吧~

完全不对呀~?完全不对!

8192 的平方,乘 32,乘 4 字节~

那是 8.6 GB~ 每一层~

我:「果然让外国人来说很难听懂呢……」

我:「简单讲,8K 序列、32 头的中间量」

我:「是 8192 平方乘 32 乘 4 字节,约 8.6GB……」

我:「所谓每层,讲人话就是这个数还要乘 32」

水科:「再翻一句……」

我:「序列 8K 的时候」

我:「那张 attention 矩阵比整个 7B 的权重还大……」

水科:「这次及格」

我:「……我刚才那段是谁教的?」

水科:「我……」

我:「啊哈哈,那我夸自己」

水科:「闭嘴,下一个……」

蝉。

我:「sdpa 呢?」

水科:「sdpa_attention_forward……」

水科:「sdpa_attention.py 的 79 到 170 行」

水科:「92 行里真正影响 cache 的只有两处……」

我:「第一处?」

水科:「hasattr 检查 num_key_value_groups 大于 1」

水科:「然后调 use_gqa_in_sdpa……」

水科:「传 attention_mask、key、value」

水科:「98 到 100 行……」

水科:「不满足就 repeat_kv」

水科:「满足就把 enable_gqa 设成 True……」

水科:「塞进 sdpa_kwargs,102 行」

我:「第二处呢?」

水科:「124 行……」

水科:「is_causal 等于 q_length 大于 1」

水科:「并且 attention_mask 是 None……」

水科:「并且原本就是 is_causal」

我:「decode 的时候 q_len 是 1……」

水科:「所以恒 False」

水科:「不看未来这件事……」

水科:「完全靠 cache 天然只到当前长度」

水科:「mask 一行都不给……」

我:「顺序能反过来写吗?」

水科:「不能」

水科:「117 到 119 行的注释说了……」

水科:「先判 shape 是为了不把 is_causal 变成 SymBool」

我:「SymBool 会怎样?」

水科:「会变成图里的一块……」

水科:「编译期就定不下来」

我:「第三个坑呢?」

水科:「139 到 148 行,注释写得很清楚……」

水科:「sdpa 的 causal 是 upper-left 对齐」

水科:「带 cache 偏移的滑窗需要 bottom-right……」

我:「两者不等价?」

水科:「不等价」

水科:「prefill 撞上空 StaticCache 的时候……」

水科:「kv_length 大于 q_length」

水科:「它就宁可手动切一刀……」

水科:「key[:, :, :q_length, :]」

我:「为什么要切?」

水科:「因为 flash kernel……」

水科:「只在 q_length 等于 kv_length 时才启用」

我:「后端是它自己挑的?」

水科:「flash、mem-efficient、cudnn……」

水科:「后端 dispatch 不看你的眼色」

水科:「看 shape 和 mask……」

我:「mask 那边还有省事的地方吗?」

水科:「masking_utils.py 的 496 行」

水科:「allow_is_causal_skip……」

水科:「配 _ignore_causal_mask_sdpa,235 行」

水科:「全 True 的 mask 直接返回 None……」

我:「省掉一个 S 乘 S」

水科:「省掉一个 S 乘 S……」

我把常温的麦茶举起来,对着太阳看。

瓶子里什么也没有。

我:「所以 sdpa 的哲学是?」

水科:「能不给就不给……」

第七章 No Mask, Only Pages 「不传 mask,传页边界」

“8 月 29 日”

夜里的屋顶有风。

水塔的影子看不见了,只剩一个更黑的轮廓。

水科:「flash 的入口是 flash_attention_forward……」

水科:「flash_attention.py 的 26 到 103 行」

我:「它先干什么?」

水科:「seq_len 取 query.shape[2]……」

水科:「然后 q、k、v 三个都 transpose(1, 2)」

我:「为什么?」

水科:「FA 要的是 [B,S,H,d]……」

水科:「不是 [B,H,S,d]」

我:「就这么转一下?」

水科:「还有三行补零……」

水科:「head_dim 和 v_head_dim」

水科:「分别取 query 和 value 的最后一维……」

水科:「两者不等时,把 value 用 pad 补到 head_dim」

水科:「62 到 64 行……」

我:「补零是给谁的?」

水科:「给 MLA 的」

水科:「V3 的 d_v 是 128,d_qk 是 192……」

水科:「FA 要求 K 和 V 同头维」

我:「参数表里还有什么?」

水科:「sliding_window、softcap、s_aux……」

水科:「s_aux 的注释写着」

水科:「alias: learnable attention sink……」

水科:「34 到 37 行」

我:「padding 呢?长短不一怎么办?」

水科:「不走 mask……」

水科:「FA 收 cu_seqlens,也就是 varlen」

水科:「modeling_flash_attention_utils.py 里……」

水科:「unpad 再 pack」

水科:「padding 是被物理删掉的……」

水科:「不是用负无穷遮住的」

我:「物理删掉?」

水科:「40 到 44 行明确拒绝 output_attentions……」

水科:「49 到 54 行拒绝零长度维度」

水科:「还建议 use SDPA instead……」

我:「它对 cache 友好吗?」

水科:「kernel 内部 tile 化流水」

水科:「但仍然要顺序读全量的 K 和 V……」

水科:「写 O(1)、读 O(S) 那笔账」

水科:「对 FA 同样成立……」

我:「那它省了什么?」

水科:「省的是 S 平方那个中间量的显存」

水科:「不是 cache 的字节……」

风把本子吹开一页。她按住。

我:「flex 呢?」

水科:「flex_attention_forward……」

水科:「flex_attention.py 的 262 行」

水科:「attention_mask 那个位置可以放两样东西……」

水科:「仍然是张量,或者一个 BlockMask」

我:「BlockMask 从哪来?」

水科:「create_block_mask 传一个 mask_mod 造出来……」

水科:「238 行」

水科:「mask_mod 的签名,注释原话是……」

水科:「(b,h,q,kv)->bool,185 行」

我:「那传张量的那条路呢?」

水科:「也不白走……」

水科:「它被折进 score_mod,290 行」

水科:「和 softcap、position_bias 一起……」

水科:「当逐分数的变换」

我:「组合子在哪?」

水科:「masking_utils.py 负责把组合子吐成 mask_mod……」

causal_mask_function 在 76 行。

sliding_window_overlay 在 92 行。

chunked_overlay 在 104 行。

packed_sequence_mask_function 在 182 行。

and_masks 在 48 行,or_masks 在 62 行。

我:「有硬约束吗?」

水科:「两个」

水科:「flex_attention 不容 dropout……」

水科:「dropout 大于 0 直接 raise」

水科:「274 到 278 行……」

水科:「提示里写着只用于 inference」

我:「第二个?」

水科:「attention sink 在 score_mod 里做不对……」

水科:「297 到 299 行的注释说得很明白」

水科:「sink 要的是归一化之前的整行……」

水科:「所以得排在 flex 调用之后」

我:「稀疏那半呢?」

水科:「msa_attention_forward……」

水科:「msa_attention.py 的 230 行」

水科:「indexer 按 block 打分,选 topk_blocks……」

水科:「155 到 159 行」

水科:「没有 indexer 的层直接回落 SDPA,247 行……」

我:「哪些层没有?」

水科:「视觉塔」

水科:「还有无 indexer 的全注意力层……」

我:「那个给压缩条目打分挑 top-512 的」

水科:「Lightning Indexer……」

水科:「在 HF 侧就是 DynamicIndexedLayer」

水科:「加 msa kernel 的组合……」

我:「一句话总结?」

水科:「eager 把 mask 存进内存」

水科:「sdpa 尽量不给 mask……」

水科:「flex 把 mask 变成闭包」

我:「五种实现摆在一起呢?」

水科:「本子上有……」

那一页画了五行,六列。

eager:要 4D mask,会 materialize S 平方。

能看权重,布局 dense。

sdpa:尽量免 mask,不 materialize,不能看权重。

dense 且连续。

fa2、fa3、fa4:免 mask,用 cu_seqlens。

不 materialize,不能看权重,dense 或 varlen。

flex:免 mask,用 mask 函数,不 materialize。

不能看权重,dense。

paged:前面三者的 paged 变体,免 mask。

不 materialize,不能看权重,布局是 block 页表。

我:「能看权重是什么意思?」

水科:「能不能把 attn_weights 拿出来看……」

水科:「只有 eager 能」

我:「自己写一个要怎么办?」

水科:「契约在 modeling_utils.py……」

水科:「5139 到 5174 行」

水科:「签名是 module、q、k、v、mask……」

水科:「关键字 dropout、scaling,再加 **kwargs」

水科:「返回 out 和 weights……」

水科:「挂上去就生效」

我:「从哪拿那个注册表?」

水科:「from transformers.modeling_utils……」

水科:「拿 ALL_ATTENTION_FUNCTIONS」

水科:「顶层没有导出,模型文件都从这里拿……」

水科:「register 一个名字」

水科:「再把 config._attn_implementation 设成它……」

我:「写错名字呢?」

水科:「get_interface 在 5161 行」

水科:「对未注册的键名直接 KeyError……」

水科:「eager 是永远的下限」

水科:「它同时是那个默认实参……」

我:「所以这四种对 cache 的要求」

水科:「完全一致……」

水科:「dense、连续、全量、dim=-2 增长」

我:「例外呢?」

水科:「有一个……」

水科:「它不把 cache 当张量」

我:「当什么?」

水科:「当页……」

她合上本子。

风停了。

蝉在很远的地方叫了一声,只有一声。

第八章 Two Thousand One Hundred Lines 「容器家族」

“8 月 30 日”

图书室的电风扇在头顶转,转得很慢。

管理员在柜台后面睡着了。

水科从包里拿出一叠纸。

已经裁好的,格子数固定,每一张都一样大。

我:「这是什么?」

水科:「今天的本子……」

我:「你那本呢?」

水科:「那本是 Dynamic」

我:「这叠呢?」

水科:「Static……」

我:「……差别?」

水科:「那本要写多少裁多少」

水科:「这叠一开始就裁满了……」

她把纸叠整齐,压在桌角。

水科:「cache_utils.py 一共 2131 行」

水科:「家谱在本子上,你自己看……」

那一页画得很密,缩进一层就是一代。

CacheLayerMixin 在 27 行。

update、get_mask_sizes、

get_seq_length、get_max_length。

往下 DynamicLayer 在 113 行,动作是 cat(dim=-2)。

它的三个子类。

DynamicSlidingWindowLayer 在 203 行,只留 W 减 1。

DynamicIndexedLayer 在 319 行,多挂一路 indexer key。

QuantizedLayer 在 703 行。

再往下分成 Quanto 的 779 行和 HQQ 的 834 行。

StaticLayer 在 398 行。

预分配,加 index_copy_,加 mark_static_address。

StaticSlidingWindowLayer 在 504 行。

StaticIndexedLayer 在 631 行。

LinearAttentionCacheLayerMixin 在 891 行。

LinearAttentionLayer 在 1003 行。

四个混血在 1094、1134、1166、1198 行。

Cache 在 1269 行,本体是一个 layer 的列表。

外加 offload 和 prefetch。

DynamicCache 在 1737 行,StaticCache 在 1829 行。

QuantizedCache 在 1884 行。

EncoderDecoderCache 在 1947 行。

MtpCache 在 2102 行,DFlashCache 在 2114 行。

我:「……看完了」

水科:「契约呢」

我:「五个抽象,四个默认?」

水科:「自己找……」

抽象的五个。

lazy_initialization 在 53 行,update 在 56 行。

get_mask_sizes 在 61 行,get_seq_length 在 64 行。

get_max_length 在 67 行。

默认可重写的四个。

offload 在 75 行,prefetch 在 81 行。

reset 在 87 行,reorder_cache 在 100 行。

我:「reset 是置空吗?」

水科:「不是。是 zero_()……」

水科:「多轮对话要复用同一个对象」

我:「reorder_cache 呢?」

水科:「就是 index_select(0, beam_idx)……」

水科:「beam search 的全部逻辑就这一句」

我:「全部……」

水科:「全部……」

我:「Dynamic 那两行呢?」

水科:「144 和 145 行……」

水科:「keys 和 values 各 cat 一次,dim=-2」

我:「代价?」

水科:「长度变了,就要新分配加整块复制……」

水科:「S 步累计下来是 O(S²) 的写带宽」

水科:「shape 是动态量……」

水科:「torch.compile 必然 graph break」

我:「所以它是?」

水科:「对照组。不是生产选项……」

她把那叠裁好的纸推到我面前。

水科:「Static 是为 compile 而生的……」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cache_utils.py:424-451 节选:一次开满,并把地址钉住
self.keys = torch.zeros(
(self.batch_size, self.num_heads, self.max_cache_len, self.k_head_dim),
dtype=self.dtype, device=self.device,
)
if not is_torchdynamo_compiling():
torch._dynamo.mark_static_address(self.keys)
torch._dynamo.mark_static_address(self.values)
torch._dynamo.mark_static_address(self.cumulative_length)

# cache_utils.py:478-481:就地写
cache_position = torch.arange(kv_length, device=self.device) + self.cumulative_length
self.cumulative_length.add_(kv_length) # 必须 in-place
self.keys.index_copy_(2, cache_position, key_states)

我:「mark_static_address 是干什么的?」

水科:「保住指针……」

水科:「就地改写才不会让 cudagraph 作废」

水科:「注释原话是……」

水科:「Without this, we cannot use cudagraphs」

水科:「后面还跟了两个感叹号……」

我:「cumulative_length 为什么是 tensor?」

水科:「同一个理由。414 行的注释」

我:「代价呢?」

水科:「空 slot 也占显存……」

水科:「max_cache_len 要提前给」

水科:「generation/configuration_utils.py 的 180 行……」

水科:「要设成预期最长值」

水科:「免得反复重分配加反复重编译……」

水科:「还有,prefill 不能进编译区」

我:「chunked prefill 呢?」

水科:「另说……」

我:「量化呢?」

水科:「QuantizedCache 在 1884 行」

水科:「docstring 直接引了 KIVI 那篇论文……」

水科:「参数是 nbits=4、q_group_size=64」

水科:「residual_length=256……」

我:「256?」

水科:「……」

她翻本子。翻了三页。

水科:「128……」

我:「啊哈哈!」

水科:「行号我背得出来,默认值我背不出来」

水科:「前天多数了一个文件……」

水科:「今天多背了一倍」

我:「你也有这种时候!」

水科:「闭嘴……」

电风扇。

水科:「QuantizedLayer.update 在 731 到 765 行」

水科:「先 self._dequantize(self._quantized_keys)」

水科:「把已量化的那部分 dequant 回来」

水科:「再和当前残差窗、新来的 key_states」

水科:「一起 cat 成 keys_to_return」

水科:「757 行那个判断……」

水科:「keys 是四维」

水科:「长度加一大于等于 residual_length……」

水科:「就整批量化下推,axis 传 self.axis_key」

水科:「然后把 self.keys 置成一个空 tensor……」

我:「双轨?」

水科:「最近 128 条保持原精度,那是残差窗」

水科:「溢出之后整批下推到量化区……」

水科:「K 和 V 各自按 q_group_size 分组」

水科:「和 KIVI 论文的差别,docstring 里写明了……」

我:「坏消息呢?」

水科:「每层每步都要 dequant 一遍」

水科:「decode 本来就是访存 bound……」

水科:「4bit 省下的字节」

水科:「被 dequant 的临时张量吃掉一截……」

我:「所以省的是?」

水科:「显存。也就是更多并发」

水科:「通常不是时延……」

我:「有硬约束吗?」

水科:「QuantizedCache 只接受纯 full_attention 的模型」

水科:「1934 到 1938 行直接 raise……」

水科:「滑窗、线性、压缩层,一律不支持」

我:「offload 呢?」

水科:「Cache 传 offloading=True……」

水科:「核心在 Cache.update,1379 到 1386 行」

水科:「先让 default stream 等 prefetch_stream……」

水科:「1381 行预取下一层」

水科:「1383 行才更新这一层……」

水科:「更新完,如果开了 offloading」

水科:「就立刻把这一层卸下去……」

我:「一条流水线?」

水科:「独立 stream 预取」

水科:「找不到可搬的层就绕回开头……」

水科:「1335 到 1339 行那个 ValueError 分支」

我:「滑窗层也搬吗?」

水科:「only_non_sliding 默认只搬非滑窗层……」

水科:「注释说得很直白」

水科:「滑窗层本来就小,搬它反而亏……」

我:「用户从哪儿设?」

水科:「generation_config.cache_implementation」

水科:「generation/configuration_utils.py……」

水科:「169 到 173 行,实体在 47 到 48 行」

水科:「五个名字……」

水科:「dynamic、static、offloaded」

水科:「offloaded_static、quantized……」

我:「我记得还有 offloaded_hybrid?」

水科:「进坟场了」

水科:「它和 offloaded_hybrid_chunked……」

水科:「都在 DEPRECATED_STATIC_CACHE_IMPLEMENTATIONS 里」

水科:「50 到 56 行,只为老配置兼容……」

我:「paged 呢?」

水科:「paged 根本不在那个列表里」

水科:「它是校验的时候单独加上的……」

水科:「685 行」

水科:「ALL_CACHE_IMPLEMENTATIONS 后面拼一个 paged……」

我:「为什么要单独加?」

水科:「因为它不是一个容器」

水科:「它是另一个世界……」

管理员翻了个身。

我们把声音压得更低。

第九章 Who Makes, Who Edits, Who Deletes 「谁在造、谁在改、谁在删」

“8 月 30 日”

坡道上,水科做了一件我以为她绝不会做的事。

她把那本笔记本拆了。

线抽出来的时候有很轻的一声。

她只在末尾写。

写过的地方不许涂改。

撕掉的页,也还是写过的页。

我:「……你」

水科:「嗯……」

散页在她手里,每一页的角落都有一个编号。

我:「那不是写过的页吗?」

水科:「是。现在它们可以分开借人……」

我:「借人?」

水科:「同一页,可以同时在两个人手里」

我:「怎么做到的?」

水科:「页角写一个数……」

她把一页递给我,页角是 2。

我:「这是?」

水科:「引用计数……」

电车从下面过去了。

水科:「先说造……」

本子上那一页写着两行。

modeling_llama.py 的 383 到 387 行。

如果 use_cache 而 past_key_values 是 None。

就 DynamicCache(config=self.config)。

接着 past_seen_tokens 取 get_seq_length()。

没有 cache 就是 0。

我:「生成那一侧呢?」

水科:「_prepare_cache_for_generation……」

水科:「generation/utils.py 的 1929 行」

水科:「用户自己传的 cache……」

水科:「和 generation_config.cache_implementation 互斥」

水科:「1948 到 1953 行……」

我:「传 tuple 呢?」

水科:「明确不支持,1954 到 1957 行」

水科:「错误信息写着……」

水科:「Please use a Cache instance」

我:「v4 时代那个 tuple[tuple[Tensor]]……」

水科:「在这里正式埋掉」

我:「名字呢?」

水科:「cache 的名字会按模型改……」

水科:「1943 到 1944 行」

水科:「mamba 系叫 cache_params……」

我:「paged 是第三种世界观吧」

水科:「注册表就是目录……」

那一页上五个键名,全用竖线拼。

paged 拼 flash_attention_4,

指向 paged_attention_forward。

paged 拼 flash_attention_3,同一个函数。

paged 拼 flash_attention_2,还是同一个。

paged 拼 sdpa,指向 sdpa_attention_paged_forward。

paged 拼 eager,指向 eager_paged_attention_forward。

位置在 modeling_utils.py 的 5147 到 5160 行。

我:「键名是用竖线拼起来的」

水科:「所以 paged 不是一种新算法……」

水科:「是三种算法共用的新布局」

我:「读的时候呢?」

水科:「integrations/sdpa_paged.py 的 28 到 40 行……」

水科:「cache 用 kwargs.pop 取出来,可能是 None」

水科:「不是 None 就调 update……」

水科:「传 key_states、value_states、layer_idx」

水科:「还有 read_index 和 write_index……」

水科:「出来的 key 再 transpose(0, 1)」

水科:「然后 unsqueeze(0)……」

我:「read_index 和 write_index 是什么?」

水科:「页表……」

水科:「物理块不必连续……」

水科:「读的时候按索引 gather 成逻辑连续」

我:「eager 版呢?」

水科:「eager_paged.py 的 28 到 34 行……」

水科:「注释写得更清楚」

水科:「shape 从 [1, H_kv, S_kv, d]……」

水科:「拍平成 [-1, H_kv, d]」

水科:「页拼成一条长条……」

我:「那个优雅的 update(k, v, layer_idx) 呢?」

水科:「在这里不成立」

水科:「所以 HF 另起了一套……」

水科:「PagedAttentionCache」

水科:「continuous_batching/cache.py 的 89 行……」

水科:「一共 474 行」

水科:「它不继承 Cache……」

我:「两套体系并存的原因」

水科:「就是这一句……」

她把散页分成两叠,一叠给我,一叠自己拿着。

我:「真正的价值在哪?」

水科:「不在少分配显存」

水科:「在共享……」

水科:「continuous_batching/cache_manager.py」

水科:「BlockManager 在 58 行……」

水科:「手里是 _free_block_ids 和 _id_to_block」

水科:「get_free_blocks 在 112 行……」

水科:「收 n、last_block_id、shareable、group_id」

水科:「分配的时候顺手把块串成父子链……」

我:「指纹呢?」

水科:「compute_hash 在 279 行」

水科:「收 parent_hash、tokens、group_id……」

水科:「块的指纹等于父块的指纹」

水科:「加自己的 token,加层组号……」

我:「所以前缀相同的请求」

水科:「哈希必然相同……」

水科:「search_prefix_match 在 cache.py 的 470 行」

水科:「直接命中……」

水科:「第二个请求的 prefill,大半是白嫖的」

我:「fork 呢?」

水科:「fork_blocks 在 131 行……」

水科:「docstring 画了两种情形」

水科:「4 块 fork 给 2 个子请求……」

水科:「不共享的时候,新分配 8 块,全复制」

水科:「共享的时候,只新分配 2 块……」

水科:「就是最后那个不完整的块」

水科:「前面的完整块,ref_count 加 num_forks……」

我:「页角那个数?」

水科:「就是那个数」

我:「TP 的时候 hash 会不一样吧?」

水科:「不能用内建的 hash(),282 到 288 行……」

水科:「每个进程加盐不同」

水科:「换成 hashlib.blake2b……」

水科:「digest_size 是 8」

我:「并发数是谁定的?」

水科:「PagedAttentionMemoryHandler……」

水科:「cache.py 的 565 行」

水科:「activation_peak 在 621 行,估激活的峰值……」

水科:「infer_max_batch_tokens_and_num_blocks」

水科:「在 652 行,反推块数……」

我:「先量显存再定并发」

水科:「而不是先开并发再 OOM……」

风把一页吹到坡道下面去了。

我们都没有去追。

我:「beam search 呢?投机解码的多分支呢?」

水科:「还有两个用户的 prompt 前 2K token 一模一样」

水科:「在这套里是同一个动作……」

水科:「引用计数,加不完整块复制」

我:「改和删呢?」

水科:「crop 在 165 到 186 行……」

水科:「负数是从尾部删 N 个」

水科:「正数是那个 deprecated 的截到绝对长度……」

水科:「reset 在 87 行,reorder_cache 在 100 行」

水科:「batch_repeat_interleave 在 190 行……」

水科:「batch_select_indices 在 196 行」

我:「滑窗层的 crop 呢?」

水科:「有额外脾气……」

水科:「越过窗口之后只接受负数」

水科:「而且要先调 activate_past_recording()……」

水科:「在 218 行」

水科:「否则直接 raise,286 到 296 行……」

我:「谁会走这条路?」

水科:「投机解码回退」

水科:「多轮对话裁剪……」

她把剩下的散页收进包里,动作很慢。

我:「拆了还装得回去吗?」

水科:「装不回去……」

水科:「装回去的就不是那一本了」

我:「……」

水科:「但每一页都还在……」

第十章 Forty Lines 「手写四十行」

“8 月 30 日”

回到屋顶。

水塔的轮廓比第一天矮了一点,或者是我的错觉。

水科把散页铺在地上,一页一页压平。

水科:「今天讲的那些,压回去」

我:「压成什么?」

水科:「一个能跑的最小实现……」

我:「多少行?」

水科:「四十行」

我:「……够吗?」

水科:「够。多的都是装饰……」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import torch

class KV: # DynamicLayer 的全部语义
def __init__(self, L): self.k, self.v = [None]*L, [None]*L
def update(self, k, v, i):
self.k[i] = k if self.k[i] is None else torch.cat([self.k[i], k], dim=-2)
self.v[i] = v if self.v[i] is None else torch.cat([self.v[i], v], dim=-2)
return self.k[i], self.v[i]

def eager(q, k, v, mask): # eager 那 23 行的最小形
w = q @ k.transpose(-1, -2) * q.size(-1) ** -0.5
return (w + mask).softmax(-1) @ v

def forward(x, W, cache, l, rope): # x: [Sq,d],prefill 时 Sq=S,decode 时 Sq=1
q, k, v = ((x @ w).view(-1, H, dh).transpose(0, 1) for w in W[:3]) # [H,Sq,dh]
q, k = rope(q), rope(k) # 契约:写 cache 之前旋转
k, v = cache.update(k[None], v[None], l) # 全篇唯一与缓存有关的一行,[1,H,Skv,dh]
Sq, Skv = q.size(-2), k.size(-2) # Skv = 旧的 + 新的
m = (torch.arange(Skv) > torch.arange(Sq)[:, None] + Skv - Sq).to(x.dtype) * -torch.inf
return eager(q[None], k, v, m) @ W[3] # bottom-right 对齐:decode 时 m 全空

# 数值对齐:一次性 prefill 与逐 token decode 必须逐位相等
c1, c2 = KV(1), KV(1)
full = forward(X, W, c1, 0, rope)[:, -1] # Sq = S,m 是下三角
for t in range(S):
step = forward(X[t:t+1], W, c2, 0, rope)[:, -1] # Sq = 1,m 全空,is_causal 空转
assert torch.allclose(full, step, atol=1e-4)
print('bytes/token =', 2 * L * Hkv * dh * 2)

水科:「出题」

我:「啊,好……」

水科:「一次性 prefill 的最后一行」

水科:「和逐 token decode 的第 S 步……」

水科:「必须逐位相等」

水科:「跑……」

我在手机上跑了很久。

风扇转了三圈。

我:「通过了!」

水科:「你做了什么」

我:「建了两个 cache,c1 和 c2……」

我:「c1 一次吃完整个 X,取最后一行」

我:「c2 一步一步吃,第 S 步取最后一行……」

我:「assert torch.allclose,过了」

水科:「答对了……」

我:「哦」

水科:「不过只能给你七十五分……」

我:「七十五分!?」

我:「为什么!」

水科:「你漏了一项」

我:「漏了什么?」

水科:「自己看那行 assert……」

我:「allclose,atol=1e-4」

我:「啊……」

那个容差是我自己写进去的。

allclose 不是逐位。

它只是说,两边的差在我给的宽容之内。

而我说的是逐位相等。

水科:「要真的逐位」

水科:「得两边同一条 kernel……」

水科:「同一个 dtype,同一个 reduction 顺序」

水科:「你这两边恰好都是 eager 那一条路……」

水科:「分母的长度都是 S 加 1」

水科:「求和顺序一致……」

水科:「所以它本来就该一模一样」

水科:「你却写了一个 1e-4……」

水科:「把本该完全相等降级成差不多」

我:「……」

水科:「还有一项……」

我:「还有?」

水科:「你的 rope 是个闭包」

水科:「位置是从外面喂进去的……」

水科:「代码里看不见」

我:「所以呢?」

水科:「所以你要用嘴说出来……」

水科:「cache 里的 K 已经带位置了」

水科:「decode 时只给新 token 转……」

水科:「说不出来,就是没懂」

我:「啊哈哈……我还真是个冒失鬼……」

水科:「七十五分」

我:「不能加到八十吗?」

水科:「不能……」

蝉在很远的地方叫。

只有一声。

我:「那这四十行里,哪一行是真的?」

水科:「你指哪一行……」

我:「cache.update 那一行?」

水科:「这四十行里,唯一与缓存有关的一行」

我:「把它换成 Static 呢?」

水科:「其余代码一个字都不用改……」

我:「换成滑窗?」

水科:「一个字都不用改」

我:「量化。paged……」

水科:「一个字都不用改」

我:「……」

水科:「结构、kernel、容器……」

水科:「这几天讲的全部内容」

水科:「都在这一行的两端……」

我:「对上那一下才算真懂」

水科:「你总算说了一句像样的……」

她把散页收起来,一页都没有折。

我:「那 bytes/token 那句呢?」

水科:「2 乘 L 乘 H_kv 乘 d_h 乘 2」

水科:「打印出来,是为了让你记住那个数……」

我:「512KB」

水科:「对 7B 来说……」

水科:「对别的来说,回去看那张九行的表」

我:「九行的那张?」

水科:「九行的那张……」

终章 Wonderful Everyday 「美好的每一天」

“8 月 30 日”

坡道下面,便利店的灯还亮着。

水科:「回去之前,把踩过的地方数一遍」

我:「数什么?」

水科:「坑」

我:「有几个?」

水科:「七个。你问,我答」

我:「第一条」

水科:「RoPE 必须在 update 之前」

水科:「modeling_llama.py 的 259 到 262 行」

水科:「那个顺序不是偶然」

我:「反过来会怎样?」

水科:「cache 里的 K 已经带位置了」

水科:「decode 时再按 position_ids 给旧 K 转一次」

水科:「就是位置叠加……」

我:「V4 呢?」

水科:「更绕」

水科:「压缩条目的 RoPE 打在窗位上……」

水科:「entry_count 乘 compress_rate」

水科:「modeling_deepseek_v4.py 的 412 行……」

我:「第二条」

水科:「repeat_kv 必须在 cache 之后……」

我:「放前面呢?」

水科:「就是往 cache 里存 H_q 份复制品」

水科:「那本账直接乘 H_q 除 H_kv……」

水科:「sdpa 用 enable_gqa,连这一步都省了」

我:「第三条……」

水科:「Dynamic cache 与 torch.compile 互斥」

水科:「cat 改 shape,必然 graph break……」

水科:「要 compile 就用 Static 加 max_cache_len」

水科:「那三个 mark_static_address 在 449 到 451 行……」

水科:「就是为这件事存在的」

我:「第四条……」

水科:「decode 阶段 is_causal 是空转」

水科:「sdpa_attention.py 的 124 行……」

水科:「q_len 等于 1,恒 False」

水科:「未来根本不在 cache 里……」

我:「还传 4D mask 呢?」

水科:「白给一个 S 乘 S」

我:「第五条……」

水科:「mask 的对齐」

水科:「sdpa 的 causal 是 upper-left……」

水科:「带 cache 偏移的滑窗要 bottom-right」

水科:「139 到 148 行的注释专门写了这一段……」

水科:「两者不等价」

我:「那怎么办?」

水科:「让 masking_utils 去算 kv_offset……」

水科:「别自己手搓 mask」

我:「第六条……」

水科:「cache 量化不一定更快」

水科:「每层 dequant 一次……」

水科:「decode 本来就是访存 bound」

水科:「而且 QuantizedCache 只支持纯 full_attention……」

水科:「1934 行」

水科:「先想清楚你省的是显存,还是时延……」

我:「第七条」

水科:「output_attentions=True……」

水科:「会让 sdpa 和 flash 告警或者降级」

水科:「sdpa_attention.py 的 92 到 95 行……」

水科:「flash_attention.py 的 40 到 44 行」

我:「所以……」

水科:「debug 完记得关」

我:「记得关……」

水科:「记得关」

我:「你重复了?」

水科:「因为你不会关……」

便利店的自动门开了又关,没有人出来。

我:「最后,把那些容器摆在一起」

水科:「本子上有……」

最后一页写满了,六种,四栏。

DynamicLayer,cat,每步变长,没有上限,默认就是它。

代价是 compile 不友好,还有 O(S²) 的写带宽。

StaticLayer,就地 index_copy_,上限是 max_cache_len。

生产和 compile 用它,代价是空 slot 也占显存。

DynamicSlidingWindowLayer,留 W 减 1,上限就是 W。

Mistral、Gemma2、V4 的滑窗层用它,代价是远处全盲。

QuantizedLayer,残差窗加分组量化,上限是位宽。

长上下文低并发的场景。

代价是 dequant 时延,只支持 full。

PagedAttentionCache,按块分配加引用计数。

上限是物理块数,serving 用它。

代价是与 Cache 的协议不通。

还要 read_index 和 write_index。

LinearAttentionLayer,不增长,上限是固定状态。

Mamba、GDN、hybrid 用它,代价是精确检索能力为零。

我:「趋势呢?」

水科:「三条……」

水科:「一,cache 从技巧变成了对象」

水科:「v4 时代它是 past_key_values 那个 tuple……」

水科:「v5 里它是每层一个 CacheLayer」

水科:「可 offload、可量化、可分页、可 reset……」

水科:「还带 layer_types 自动 dispatch」

我:「二……」

水科:「kernel 与 cache 互相驯化」

水科:「flash 要 varlen,sdpa 要连续……」

水科:「compile 要 static,serving 要 paged」

水科:「选哪一种 attention 实现……」

水科:「就是给 cache 选一种存储」

水科:「反过来,self.values = self.keys 那一行……」

水科:「也让 FA 的补零逻辑有了存在的理由」

我:「三……」

水科:「主线是少缓存」

水科:「MHA 到 GQA,到 MLA……」

水科:「到滑窗驱逐,到 CSA 和 HCA 压缩」

水科:「到线性状态……」

水科:「六级台阶」

水科:「全在把 O(S) 往 O(1) 压……」

我:「压到 0 之后呢?」

水科:「压到 0 就不叫 cache 了」

水科:「叫状态……」

她蹲下来,把散页一页一页对齐。

用橡皮筋绑住。

我:「那不是本子了吧……」

水科:「不是。是一叠页」

我:「还写吗?」

水科:「写。还是在末尾……」

水科:「只是末尾现在有好几个」

我:「有好几个末尾,还算末尾吗?」

水科:「你问页去……」

蝉停了。

夏天的最后几天,蝉声总会在某个时刻齐齐断掉。

像被人拔了电源。

我偶尔还会想这种事情。

人到底是靠记住活着,还是靠忘掉活着。

对我来说,那就只有这种程度。

比如一叠被橡皮筋绑住的纸。

比如一瓶没有喝完的常温麦茶。

比如那条被我抹出水痕的裤子,明天就要洗了。

幸福的每一天……

每一天都很幸福……

我便是生活在这样一个世界上……

我偶尔会思考这种事情。

……

Wonderful Everyday