推理成本高、生成不可控、上下文一长就卡顿——这些问题在 LLM 应用落地时几乎人人都会遇到。工程上常见的对策是“剪枝”:把权重稀疏化、把候选 token 截断、把 KV Cache 压缩。但剪枝的粒度停留在数值层面,很少人追问:模型到底依据什么语义规律,决定哪些推理分支可以安全砍掉?
本文尝试把一个更偏理论的思考框架拆开来讲——语义热力学(Semantic Thermodynamics)与叙事引力(Narrative Gravity)。前者把 token 生成过程看成一种“语义系统的热力学演化”,后者把上下文看成约束生成轨迹的引力场。两者结合,能够为 LLM 推理剪枝提供一个比“只看概率大小”更完整的形式化视角。
文章会先讲清楚这两个概念是什么,再落到 LLM 推理链路的技术细节上,最后给出一套可参考的工程映射与伪代码示例。适合对 LLM 推理优化、模型裁剪、可控文本生成感兴趣的开发者阅读,不需要深入的物理学背景。
1. 背景:为什么 LLM 推理必须谈“剪枝”
1.1 语言模型生成的高成本
大语言模型采用自回归方式生成文本。每生成一个 token,都要把当前已生成的全部 token 重新经过一次前向计算,从词表中计算每个候选 token 的得分(logits),再通过 softmax 转成概率分布,最后按某种采样策略选出下一个 token。
这个过程有两个明显的成本来源:
- 词表很大,但每一步只需要输出一个 token。比如词表 32K、64K 甚至 128K,真正被选中的只有一个。其余候选的计算结果绝大多数被浪费了。
- 上下文越长,KV Cache 越大。注意力机制需要把历史 token 的 Key 和 Value 缓存起来,每一步都要和这些缓存做计算。上下文长度翻倍,KV Cache 也会随之增长。
所以在推理阶段,提高速度、降低内存占用,几乎都离不开“剪掉某些不必要的东西”。
1.2 剪枝的两层含义
在 LLM 领域,“剪枝”这个词其实有两层含义,经常被混用:
| 层次 | 对象 | 目标 | 典型手段 |
|---|---|---|---|
| 权重剪枝 | 模型参数 | 减小模型体积、加速推理 | 结构化稀疏、低秩分解、量化 |
| 推理剪枝 | 推理过程中的候选/缓存/计算路径 | 降低单次生成延迟与内存 | top-k、top-p、KV Cache 压缩、提前终止 |
本文讨论的重点是第二层:推理剪枝。它不改变模型权重,而是改变每次生成时的搜索空间与计算路径。
1.3 从“局部贪心”到“全局语义约束”
常见的推理剪枝策略,比如 top-k 采样,本质上是局部的:只看当前步的概率分布,把概率最低的 token 直接排除。
但语言生成并不是一个局部事件。当前 token 的选择,既受到前文约束,也影响着后续所有 token 的生成空间。一个在局部看起来概率不低的 token,可能把整段话引向语义崩坏;一个局部概率中等的 token,反而可能是让叙事继续推进的关键节点。
这说明:更合理的剪枝策略,应该把“当前步”放到“全局语义轨迹”中评估。这也是语义热力学与叙事引力这两个概念真正有价值的地方——它们想把剪枝从“数值截断”升级为“语义约束”。
2. 语义热力学与叙事引力:两个核心概念
2.1 先从热力学说起
热力学研究的是大量微观粒子组成的宏观系统。温度、内能、熵、自由能这些概念,描述的是系统整体状态。
这里最值得关注的不是物理定律本身,而是它的建模方式:系统有大量微观状态,每个微观状态都有对应的能量;宏观上我们不逐一追踪每个粒子,而是用统计规律刻画系统的概率分布。低频高能状态出现的概率低,高频低能状态出现的概率高。
这一套“微观状态 + 能量 + 概率分布”的语言,和 LLM 生成文本的过程非常相似:
- 词表中的每个 token 是一个“微观状态”。
- logits 可以看成某种“能量”的负值。得分越高,说明在当前上下文中该 token 被选中的倾向越强。
- softmax 的结果就是一个概率分布,它定义了当前宏观状态(上下文)下,各个微观状态出现的概率。
2.2 语义热力学:把文本生成当作热力学演化
在语义热力学的视角下,一段文本的生成过程,可以理解为一个语义系统从高熵状态向低熵状态演化的过程。
生成开始时,模型只看到很少的上下文,候选 token 范围宽,分布比较平坦,熵较高。随着上下文逐渐累积,语义方向逐渐明确,概率分布会越来越尖,熵逐渐下降。最终生成的整段文本,可以看成系统达到某个相对稳定的“语义平衡态”。
这可以类比热力学中的自由能:
F = U - TS
其中 U 是“语义内能”,可以理解成文本与上下文的语义契合程度;S 是熵,刻画候选分布的离散程度;T 是“语义温度”,控制系统对多样性或混乱状态的容忍度。自由能越低,说明当前生成的文本在“符合语境”和“信息量/多样性”之间达到了较好的平衡。
这个公式并不算严格的物理推导,而是一种概念映射。它的价值在于提醒我们:生成过程同时受到两股力量影响——一股力量把文本拉向高概率、低风险的“稳定语义状态”,另一股力量让模型保持探索性,不因为局部高概率就锁死整段文本。
2.3 叙事引力:上下文如何约束生成轨迹
引力在物理中的核心是:质量弯曲时空,物体沿测地线运动。叙事引力借用了这个意象:
已经生成的文本,构成一个“语义质量场”。它不直接决定下一个 token 一定要是什么,而是改变了语义空间的结构——那些与既有叙事方向一致的 token,处在更“低洼”的位置;与当前叙事方向偏离较大的 token,处在更陡峭、更不稳定的位置。
注意力机制就是这种引力的一种工程化体现。某一时刻,模型会把注意力集中在少量关键 token 上,相当于那些 token 的质量最大,对下一步生成方向有更强的牵引作用。KV Cache 里存的上文,不只是字符串,更是一个携带语义质量分布的“引力场”。
用叙事引力的视角看,LLM 生成时的每一步,其实是候选 token 在当前语义引力场中做“定向坍缩”的过程。质量越大的前文概念,越容易吸引后续 token 向它靠拢。
2.4 为什么要形式化这两个概念
如果只是停留在隐喻层面,“语义热力学”和“叙事引力”就是两个漂亮的比喻,对工程没有直接帮助。但一旦把它们形式化,就可以得到可以用代码表达、用指标验证的剪枝规则:
- 可以定义语义熵,来衡量当前生成状态的不确定性。
- 可以定义语义势函数,来衡量候选 token 与当前叙事的契合程度。
- 可以定义自由能判据,来决定哪些候选 token 值得保留、哪些应当剪掉。
这就是标题中 “Formalizing” 的含义:把比喻变成模型,把模型变成算法。
3. 环境与背景技术:LLM 推理链路
在把概念落到算法之前,先快速过一遍 LLM 推理的关键环节,以及剪枝策略常见的落点。
3.1 自回归生成流程
一次标准生成过程可以简化为:
- 输入 prompt,得到最后一层隐藏向量。
- 通过 LM Head 将隐藏向量映射到词表维度,得到 logits。
- 对 logits 做 softmax,得到概率分布。
- 按策略(贪心、随机采样、beam search)选择下一个 token。
- 把新 token 拼接到输入,更新 KV Cache,进入下一步。
剪枝策略可以作用于第 2、3、4、5 步:
- 在 logits 层面进行稀疏化。
- 在概率分布层面截断低质量候选。
- 在搜索路径上做剪枝。
- 在 KV Cache 层面压缩历史信息。
3.2 logits、温度、top-k、top-p
logits 是模型输出的原始得分,范围不固定。温度参数 T 会先缩放 logits,再进入 softmax:
import torch import torch.nn.functional as F def apply_temperature(logits, temperature=1.0): if temperature <= 0: # 温度过低时直接走贪心 return logits.argmax(dim=-1, keepdim=True) return logits / temperature logits = torch.tensor([[2.0, 1.0, 0.1, -1.0]]) logits = apply_temperature(logits, temperature=0.8) probs = F.softmax(logits, dim=-1) print(probs)温度越低,分布越尖锐;温度越高,分布越平坦。top-k 只保留概率最高的 k 个候选,top-p(nucleus sampling)保留累积概率超过 p 的最小候选集合。这些都是推理剪枝的经典手段,但它们都只利用了概率分布本身的信息,没有显式建模“语义引力”。
3.3 注意力与 KV Cache
在 Transformer 解码阶段,每个 token 都会计算 Query,和历史 token 的 Key 做点积,得到注意力权重,再对 Value 加权求和。历史 token 的 Key 和 Value 会被缓存下来,避免每一步重复计算。
注意力权重本身就是一个“质量分布”:
# 概念示例:注意力权重分布 attention_weights = torch.softmax(q @ k.transpose(-1, -2) / (head_dim ** 0.5), dim=-1)注意力越集中的 token,对当前生成的影响越大。这一点恰好对应叙事引力场中的“质量集中”。如果某些历史 token 在很长一段时间内注意力权重都接近 0,说明它们对后续生成的贡献很低,可以考虑在 KV Cache 中压缩或剪枝。
3.4 剪枝策略的落点总结
| 落点 | 对应热力学/引力概念 | 工程手段 |
|---|---|---|
| logits 层 | 语义内能/势能 | logits 稀疏化、维度裁剪 |
| softmax 层 | 温度/熵 | 温度调节、分布平坦度约束 |
| 采样层 | 自由能判据 | top-k、top-p、语义约束采样 |
| 注意力层 | 引力场质量分布 | 稀疏注意力、KV Cache 压缩 |
| 上下文窗口 | 语义场边界 | 长文本截断、摘要压缩 |
4. 核心机制:将语义剪枝形式化
下面给出一种概念性形式化方案。注意,这不是某个开源库的官方定义,而是一种“从隐喻走向算法”的建模思路。实际使用时需要结合具体模型和任务做调节。
4.1 语义微状态与概率测度
设当前上下文的语义状态为 C,词表为 V。每个候选 token w ∈ V 都是一个语义微状态。模型的 logits 可以看作该微状态在当前语义场中的“负势能”:
U(w | C) = -logits(w | C)
采用 softmax 后,候选 token 的生成概率为:
P(w | C) = exp(-U(w | C)) / Σ_{v∈V} exp(-U(v | C))
从这个角度理解,logits 越大,势能越小,token 越容易被选中。
4.2 语义熵与上下文信息量
当前候选分布的熵定义为:
H(C) = -Σ_w P(w | C) log P(w | C)
熵高说明模型对下一步该生成什么没有明确把握,候选分布平坦;熵低说明语义方向已经比较收敛。
语义熵可以作为剪枝的一个重要参考指标:
- 熵很低时,说明当前语义方向非常明确,剪枝可以激进一些,保留少量候选即可。
- 熵很高时,说明上下文信息不足或语义存在明显分叉,此时盲目截断低概率 token 可能丢掉未来关键的叙事分支,剪枝要更保守。
实际工程中,你可以把每一步的熵记录到日志里,观察模式:
def compute_entropy(probs: torch.Tensor) -> torch.Tensor: # probs: shape [batch, vocab] log_probs = torch.log(probs + 1e-12) return -(probs * log_probs).sum(dim=-1)4.3 叙事引力势函数
叙事引力的核心假设是:前文中的关键语义单元会对候选 token 产生“吸引”。我们可以定义一个势函数来衡量候选 token 与当前上下文语义场的引力作用。
设前文共有 N 个 token,每个 token t_i 的语义强度为 m_i(可以取注意力权重的聚合值、TF-IDF 权重或嵌入向量的范数),候选 token w 与 t_i 的语义相似度为 sim(w, t_i)。那么候选 token 的叙事引力势可以定义为:
G(w | C) = Σ_{i=1}^{N} m_i · sim(w, t_i)
G(w) 越大,说明候选 token 越容易被前文语义场“捕获”,越符合当前叙事方向。计算相似度时,可以用嵌入向量的余弦相似度,也可以用注意力得分的聚合值实例化。
在剪枝场景中,我们不再只按原始概率排序,而是把叙事引力势作为“语义偏置”,叠加到候选得分上:
score(w) = log P(w | C) + λ · G(w | C)
其中 λ 是引力强度系数,控制前文语义对当前生成的约束程度。λ 越大,生成越倾向于紧跟已有叙事;λ 越小,模型越自由。
4.4 自由能剪枝判据
结合前面的“语义内能”和“语义熵”,可以构造一个自由能目标:
F(w) = -score(w) - T · H(C)
在剪枝时,我们希望保留自由能较低的候选 token 集合,同时控制候选集合的规模。一个简单的算法思路是:
- 计算所有候选 token 的原始 logits。
- 计算语义熵 H(C)。
- 计算候选 token 的叙事引力势 G(w)。
- 得到修正后的 score(w)。
- 按 score 排序,累积保留概率,剪掉尾部低分 token。
这样得到的效果是:剪枝不再只看当前步的概率大小,还会考虑候选 token 是否与叙事方向一致、当前不确定性是否足够高。如果当前熵很高,系统会自动“宽容”一些候选 token 进入保留集合;如果熵很低,剪枝就可以更果断。
5. 工程映射:可落地的剪枝策略
概念需要代码来验证。下面给出一个概念性实现,演示如何把上述形式化思路映射到一次采样过程中。
5.1 概念性框架:NarrativeGravityPruner
""" 概念性示例:基于叙事引力与语义熵的推理剪枝 仅用于说明思路,需要根据实际模型结构做适配 """ from dataclasses import dataclass from typing import List, Optional import torch import torch.nn.functional as F @dataclass class ContextState: """当前上下文状态,记录语义质量与注意力分布""" token_ids: List[int] attention_weights: torch.Tensor # shape [num_tokens, num_tokens] hidden_states: torch.Tensor # shape [num_tokens, hidden_dim] def compute_narrative_gravity( candidate_emb: torch.Tensor, context: ContextState, temperature: float = 1.0, ) -> torch.Tensor: """ 计算候选 token 与前文语义场之间的引力势。 这里用候选 token 嵌入与前文 token 嵌入的注意力相似度加权求和。 """ # candidate_emb: [num_candidates, hidden_dim] # context.hidden_states: [num_tokens, hidden_dim] # 使用缩放点积注意力作为相似度,也可以用余弦相似度 scores = candidate_emb @ context.hidden_states.T # [num_candidates, num_tokens] scores = scores / (candidate_emb.shape[-1] ** 0.5) # 用注意力权重作为语义质量 mass = context.attention_weights[-1] # 最后一步的注意力权重,近似衡量前文质量 mass = F.softmax(mass, dim=-1) # 加权得到叙事引力势 gravity = (scores * mass.unsqueeze(0)).sum(dim=-1) # [num_candidates] return gravity / temperature def narrative_gravity_prune( logits: torch.Tensor, candidate_emb: torch.Tensor, context: ContextState, top_p: float = 0.9, lambda_gravity: float = 0.1, temperature: float = 1.0, ): """ 基于叙事引力修正后的采样剪枝。 流程: 1. 计算原始分布。 2. 计算叙事引力势。 3. 将引力势叠加到 logits 上。 4. 按 top-p 累积概率截断。 """ # 原始 logits 加温度 scaled_logits = logits / temperature probs = F.softmax(scaled_logits, dim=-1) # 计算叙事引力势 gravity = compute_narrative_gravity(candidate_emb, context, temperature) # 修正 logits:保留原始语义内能,同时叠加前文引力偏置 adjusted_logits = scaled_logits + lambda_gravity * gravity # 基于累积概率的 top-p 剪枝 sorted_logits, sorted_indices = torch.sort(adjusted_logits, descending=True) sorted_probs = F.softmax(sorted_logits, dim=-1) cumsum_probs = torch.cumsum(sorted_probs, dim=-1) # 找到超过 top_p 的截断位置 cut_pos = torch.searchsorted(cumsum_probs, top_p) + 1 # 构造掩码,只保留前 cut_pos 个候选 mask = torch.zeros_like(adjusted_logits) mask.scatter_(-1, sorted_indices[:, :cut_pos], 1.0) # 未被剪掉的候选重新归一化 masked_logits = adjusted_logits.masked_fill(mask == 0, float("-inf")) final_probs = F.softmax(masked_logits, dim=-1) return final_probs, gravity这段代码是一个“思路示例”,不是可以直接放进任何项目就跑的现成库。你需要根据具体模型获得:
- 候选 token 对应的嵌入向量。
- 历史 token 的注意力权重。
- 合适的相似度函数。
但核心逻辑已经清楚:先算原始分布,再计算叙事引力势,修正后做 top-p 剪枝。这样剪枝结果不仅受当前概率影响,还受前文语义场约束。
5.2 用语义熵指导温度调节
另一个工程化思路是动态温度。传统做法是温度固定,但语义熵高时固定温度容易让生成发散,语义熵低时固定温度又可能让文本过于保守。
可以在推理时维护一个动态温度:
def adaptive_temperature(entropy: float, base_temp: float, target_entropy: float): """ 熵高于目标时,适当降低温度,避免过度随机; 熵低于目标时,适当提高温度,提升多样性。 """ diff = entropy - target_entropy # 简单的线性调节,实际项目中可以换成更平滑的策略 return base_temp + 0.1 * diff这里有一个值得注意的细节:温度作用于 logits,但熵描述的是“当前分布的不确定性”。当模型本身对下一步没有把握,也就是熵高时,更高温度会让更多无关 token 进入候选范围。调节方向需要根据场景判断——如果目标是稳定长文本生成,熵高时应该更保守,而不是更随机。
5.3 KV Cache 的“引力场剪枝”
KV Cache 压缩是长文本推理的核心需求。传统做法基于 token 的注意力分数直接剪掉低分 token。结合叙事引力的思路,可以多做一个语义层判断:
- 如果某个历史 token 的注意力权重低,并且与当前上下文嵌入的相似度低,说明它已经处在语义引力场边缘,可以优先压缩。
- 如果某个 token 注意力权重低,但它承载着关键的叙事主题(比如一段话的核心概念),即使当前注意力不高,也应该保留。
这种“语义重要性”可以通过计算 token 嵌入在句向量方向上的投影得到。更简单的做法是给每个 token 维护一个长期重要性得分,每步更新:
importance = 0.9 * old_importance + 0.1 * attention_weight当 importance 持续低于阈值时,再考虑从 KV Cache 中淘汰。
5.4 结合传统策略的推荐层级
落到实际项目时,建议按不同层级组合使用:
| 层级 | 目标 | 推荐策略 |
|---|---|---|
| 候选 token 层 | 控制采样空间 | 动态 top-p + 叙事引力校正 |
| 分布层 | 控制整体随机性 | 基于熵的适应温度 |
| 上下文层 | 控制计算量 | 注意力权重 + 语义重要性双指标压缩 KV Cache |
| 生成流程层 | 控制长度 | 当熵低且置信稳定时,提前终止生成 |
6. 常见问题与排查思路
这里整理几个理解语义热力学与叙事引力时容易踩的误区。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 剪掉低概率 token 后生成质量反而下降 | 只按概率剪枝,丢弃了延续叙事的关键分支 | 把语义相似度或叙事引力势加入排序依据 |
| 固定低温生成文本过于保守 | 温度过低导致分布过度尖锐,多样性下降 | 结合语义熵做动态温度调节 |
| 长文本生成到中途开始偏题 | 前文对后续方向约束不足,叙事引力较弱 | 加大 lambda_gravity,或提高上下文关键 token 的语义质量 |
| KV Cache 压缩后生成不连贯 | 只按注意力权重剪枝,把关键主题 token 删掉了 | 增加长期重要性得分,别一次剪太多 |
| 高熵状态下 top-p 截断仍然无效 | top-p 集合过大,采样随机性仍然很高 | 先降低温度,再使用更小的 top-p,或引入最小候选数量限制 |
几个排查时可以重点确认的问题:
- 语义熵的计算是否使用了正确的分布?如果 logits 还没有做温度缩放,计算出的熵会失真。
- 叙事引力势的相似度函数是否稳定?不同嵌入空间里余弦相似度的分布范围差异很大,最好做归一化。
- lambda_gravity 是否设置过大?过大时生成会被前文“锁死”,反复写同一个主题;过小时又退化为普通 top-p 采样。
- KV Cache 剪枝是否逐层独立?不同层的注意力分布差异很大,最好按层设置不同阈值。
7. 最佳实践与工程建议
7.1 先定义场景目标,再选剪枝策略
剪枝不是越狠越好。不同场景的目标差异很大:
- 代码生成:需要高确定性,适合低温度 + 小 top-p + 强叙事引力约束。
- 剧情写作:需要一定多样性,适合中等温度 + 动态熵控制 + 弱引力约束。
- 问答系统:需要忠实于上下文,适合强引力约束 + 严格 KV Cache 保留策略。
- 头脑风暴:需要探索性,适合高温度 + 高熵容忍 + 弱剪枝。
没有统一的“最佳剪枝配置”,只有“最适合当前场景的配置”,这一点在语义剪枝中体现得尤其明显。
7.2 用指标验证,不要凭感觉调参
引入语义热力学与叙事引力后,需要有三类指标支撑:
| 指标 | 说明 |
|---|---|
| 生成质量指标 | 困惑度、BLEU、ROUGE,或针对任务的自动化评估 |
| 多样性指标 | 文本间的重叠度、n-gram 多样性、语义距离方差 |
| 效率指标 | 延迟、KV Cache 内存、每 token 平均剪枝比例 |
特别建议把每一步的语义熵、引力势最大值、剪枝比例记录下来。训练一个线上调试面板,观察不同 prompt 下模型是快速收敛还是持续发散。这类日志信息比只看最终生成文本更容易定位问题。
7.3 把叙事引力做成轻量级 rerank 层
最稳妥的落地方案,不是在现有采样逻辑上做大规模改造,而是增加一个轻量级 rerank 层:
- 按原始概率采样生成一批候选 token。
- 对候选 token 计算叙事引力势。
- 在候选集合内做重新排序。
- 最终从排序后的集合中选择。
这样既不会干扰模型原本的 logits 计算,也能随时开关,线上回滚非常方便。它本质上是在“模型概率”和“语义约束”之间加了一个可调权重的中间层。
7.4 注意安全边界与可控性
任何涉及生成内容可控性的调整,都需要考虑安全和合规边界:
- 不要为了“高确定性”而把生成内容锁死,导致用户输入中的攻击性内容直接传递。
- 不要为了“叙事引力”而无限强化前文中的偏见内容。
- 剪枝策略一旦上线,需要有安全兜底:如果检测到生成内容存在风险,立即切换回默认采样策略。
这些建议不是形式化理论的附属品,而是任何推理优化方案上线前都需要过的安检。
8. 总结与学习路线
本文围绕“语义热力学”和“叙事引力”这两个概念,做了一次完整的拆解。
重点内容包括:
- 推理剪枝不只是权重稀疏化,还包括候选 token、KV Cache、采样路径上的剪枝。
- 语义热力学用熵、温度、自由能来描述 token 分布的状态变化。
- 叙事引力用“语义质量场”来解释前文对后续生成的约束。
- 两者结合,可以把剪枝从“只看当前概率”升级为“同时看语义方向与不确定性”。
- 给出了可参考的概念性伪代码,包括叙事引力修正、动态温度、KV Cache 双指标压缩。
如果你想把这套思路真正落地,下一步建议按这个顺序学习:
- 先掌握标准推理代码,理解 logits、温度、top-k、top-p 在代码中的位置。
- 在现有采样代码上增加语义熵日志,先只记录,不改行为。
- 用真实业务数据观察不同场景下的熵分布,找到高熵和低熵的分水岭。
- 再尝试把叙事引力势作为一种 rerank 信号加入候选筛选。
- 最后设置可回滚的线上实验,用延迟和生成质量指标验证收益。
如果你现在正好在优化 LLM 推理性能,可以先把项目中每一步的语义熵打印出来观察一下。很多问题在熵分布图上会变得非常直观:该收敛的地方没收敛,该发散的地方被压得太死,一眼就能看出来。
希望这篇文章能帮你打开一个新的视角:剪枝不只是工程上的省内存手段,它也可以是一项有理论层次的建模工作。