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 | # models/llama/modeling_llama.py:252-277(两处长参数列表被合并成行) |
我:「看着挺普通」
水科:「普通得刚好……」
水科:「第一种身份,写入」
水科:「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 | # cache_utils.py:244-258(DynamicSlidingWindowLayer.update 节选) |
我:「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 | # cache_utils.py:424-451 节选:一次开满,并把地址钉住 |
我:「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 | import torch |
水科:「出题」
我:「啊,好……」
水科:「一次性 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