sglang的KV缓存

7 阅读3分钟

好的,我们来用一个具体的文本生成例子,结合KV缓存的工作原理,详细解释为什么只有前缀缓存(本质是KV缓存),而不能缓存Q(Query)。我们将一步步拆解Transformer生成文本的过程。

案例背景

假设我们要生成文本:"人工智能改变了世界"

  1. ​系统提示(前缀)​​:"人工智能"(已被很多用户共享使用)
  2. ​后续生成内容​​:"改变了世界"(需要动态生成)

我们将聚焦第一个待生成字"改"的生成过程。假设我们的向量维度是3(实际中可能是768或4096维)。

案例设定

  • ​当前状态​​:已处理 "人"、"工"、"智"、"能" 四个字

  • ​目标​​:生成第五个字 "改"

  • ​向量设定​​:

    # 历史tokenKV缓存 (每个K/V都是3维向量)
    K₁ = [0.8, 0.1, -0.2]  // "人"的Key
    V₁ = [-0.3, 0.5, 0.7]  // "人"的Value
    
    K₂ = [0.6, -0.3, 0.4]  // "工"的Key
    V₂ = [0.1, -0.2, 0.9] 
    
    K₃ = [-0.4, 0.7, 0.1] // "智"的Key
    V₃ = [0.6, 0.3, -0.5]
    
    K₄ = [0.2, -0.1, 0.5] // "能"的Key
    V₄ = [-0.2, 0.4, 0.1]
    
    # 当前计算的Q (第五个位置)
    Q₅ = [1.0, 0.3, -0.7]  // "改"的Query (实时计算得出)
    

分步计算图解

第一步:attention_scores = Q₅ @ stack(K_cache).T

# 将历史K堆叠成矩阵
K_cache = [[0.8, 0.1, -0.2],  # K₁
           [0.6, -0.3, 0.4],  # K₂
           [-0.4, 0.7, 0.1],  # K₃
           [0.2, -0.1, 0.5]]  # K₄

# Q₅与每个K的点积运算
score₁ = Q₅ · K₁ = (1.0 * 0.8) + (0.3 * 0.1) + (-0.7*-0.2) = 0.8 + 0.03 + 0.14 = 0.97
score₂ = Q₅ · K₂ = (1.0 * 0.6) + (0.3*-0.3) + (-0.7 * 0.4) = 0.6 - 0.09 - 0.28 = 0.23
score₃ = Q₅ · K₃ = (1.0*-0.4) + (0.3 * 0.7) + (-0.7 * 0.1) = -0.4 + 0.21 - 0.07 = -0.26
score₄ = Q₅ · K₄ = (1.0 * 0.2) + (0.3*-0.1) + (-0.7 * 0.5) = 0.2 - 0.03 - 0.35 = -0.18

# 得到注意力分数向量
attention_scores = [0.97, 0.23, -0.26, -0.18]

​物理意义​​:计算当前"改"字(Q₅)对每个历史字("人-工-智-能")的关注程度


第二步:attention_weights = softmax(attention_scores)

# 公式:softmax(x_i) = e^{x_i} / Σ(e^{x_j})

# 1. 指数化
exp_scores = [e^{0.97}, e^{0.23}, e^{-0.26}, e^{-0.18}] 
           = [2.64, 1.26, 0.77, 0.84]  # 实际值更精确

# 2. 计算总和
sum_exp = 2.64 + 1.26 + 0.77 + 0.84 = 5.51

# 3. 归一化
weight₁ = 2.64 / 5.51 ≈ 0.48
weight₂ = 1.26 / 5.51 ≈ 0.23
weight₃ = 0.77 / 5.51 ≈ 0.14
weight₄ = 0.84 / 5.51 ≈ 0.15

attention_weights = [0.48, 0.23, 0.14, 0.15]

​物理意义​​:将关注度转化为概率分布,表示"改"字应该分配多少注意力给每个历史字


第三步:context_vector = attention_weights @ stack(V_cache)

# 1. 堆叠V向量
V_cache = [[-0.3, 0.5, 0.7],  # V₁
           [0.1, -0.2, 0.9],  # V₂
           [0.6, 0.3, -0.5],  # V₃
           [-0.2, 0.4, 0.1]]  # V₄

# 2. 加权融合(向量级运算)
dim0 = (0.48*-0.3) + (0.23 * 0.1) + (0.14 * 0.6) + (0.15*-0.2)
     = (-0.144)    + (0.023)     + (0.084)     + (-0.03)    = -0.067

dim1 = (0.48 * 0.5) + (0.23*-0.2) + (0.14 * 0.3) + (0.15 * 0.4)
     = (0.24)     + (-0.046)     + (0.042)     + (0.06)     = 0.296

dim2 = (0.48 * 0.7) + (0.23 * 0.9) + (0.14*-0.5) + (0.15 * 0.1)
     = (0.336)    + (0.207)     + (-0.07)      + (0.015)     = 0.488

context_vector = [-0.067, 0.296, 0.488]

​物理意义​​:生成代表"当前需要什么信息"的融合向量,用于预测输出


为什么Q必须实时计算?——用这个例子验证

假设我们想偷懒,复用之前某个位置的Q:

  1. ​场景​​:尝试用位置2("工"字)的Q₂

    Q₂ = [0.5, -0.2, 0.3]  // 之前计算的"工"字的Q
    
  2. ​错误计算​​:

    wrong_scores = Q₂ @ K_cache.T 
                 = [Q₂·K₁, Q₂·K₂, Q₂·K₃, Q₂·K₄]
                 = [(0.5 * 0.8)+(-0.2 * 0.1)+(0.3*-0.2), ...] 
                 = [0.4 -0.02 -0.06] = 0.32  // 错误!
    
  3. ​灾难性结果​​:

    • 本应计算"改"与"人"的关系 → 实际变成"工"与"人"的关系
    • 最终context_vector会包含错误的语义信息
    • 导致输出不是"改"而可能是错误词如"造"

核心结论

步骤关键点不可替代性证明
​Q₅计算​定义"当前想知道什么"每个位置/请求需求不同
​Q·K​计算"当前关注历史"即使相同token,不同位置关注点不同
​权重·V​生成"回答所需的依据"权重分布决定信息提取角度

✨ ​​真实模型比喻​​:

将Transformer想象成研讨会:

  • K/V缓存 = 参会者的知识储备(可复用)

  • Q₅实时计算 = 主持人针对当前议题(Q₅)提问

  • 注意力权重 = 参会者根据问题举手发言

  • Context Vector = 根据发言形成的会议纪要

在您的SGLang指标中:

  1. cache_hit_rate=61.76% = 61%的问题直接有现成专家
  2. 剩余38%仍需要主持人(Q)实时提出问题

这就是为什么Q永远需要实时生成,而KV可以安全缓存的原因。