【轻量化复刻大模型 / KIMI K3】KDA(Kimi Delta Attention):并行 – 罗西的思考

【轻量化复刻大模型 / KIMI K3】KDA(Kimi Delta Attention):并行 – 罗西的思考


0x00 概要

本文档是复刻 KIMI K3 KDA(Kimi Delta Attention)的第二部分,因为文章实在太长了,所以分为两期发出。

本篇主要分析并行相关的部分。

Chunkwise 并行并不是没有代价。RNN 的状态递推通常对输入顺序敏感:交换两个 Token 的位置,会改变它们经过的状态,也会改变后续计算。而 Attention 的一次内容聚合并不天然表达顺序关系。固定一个 Query,如果只把可见范围里的 Key-Value 对重新排列,单次聚合结果本身可能不发生变化。

KDA 能够同时获得:

  • 相对上下文长度为 O(1) 的状态存储和 Decode 计算;
  • Chunkwise 并行能力;

根本原因是 KDA 把对旧状态的递归更新写成线性或仿射变换:\(S_t=S_{t-1}T_t+H_t\)。这使得多个 Token 的状态转移可以组合成一个 Chunk 级变换。这里体现出的是表达能力和计算效率之间的取舍。

注:

  • 项目 和系列博客是我的一个尝试,也是我学习的过程。不确定是否满足读者的需求,我自己会反思改进,也希望大家提出宝贵意见。
  • 涉及 KIMI K3,DeepSeek V4,PI0.5,PI0.7 等模型。后续可能会跟踪新模型。
  • 全 CPU 复刻,不涉及 GPU,也不涉及大规模训练(这是另外一个庞杂深邃的领域)和硬件。
  • 模型的组件尽量是原始大模型的缩小版,如果组件没有开源版本,则用近似组件替代。
  • 会解释每个组件在解决一个什么问题、这个问题难在哪、为什么只能这么解、解法的原理是什么,以及组件之间如何咬合成一个整体。重点不是记名词,而是理解一条主线。
  • 每个主要组件都有可直接运行的 Gradio demo——无需 GPU。另有若干可视化脚本(plot_*.py),可生成静态图。
  • 所有文件行号基于本地源码快照;源码后续变化时,行号可能移动,但函数名和调用关系仍可作为定位入口。
  • 为了保证每个文章可以尽量独立成文,所以有些内容会在不同文章中都存在,可能在系列层面上存在冗余,还请大家理解。

0x04 子知识点深度分析

4.7 ShortConv、SiLU 与 L2 归一化(无位置编码的局部混合)

线性注意力擅长把很长的历史压缩到固定大小的状态中,但这种压缩也意味着细节可能丢失。尤其是相邻 Token 之间的短距离关系,不一定能够仅靠全局状态被充分保留下来。因此,DeltaNet、GDN、KDA 等架构通常会在投影得到的 q、k、v 上增加一维因果短卷积(Causal Conv1d)。处理流程可以理解为:

  1. 先从当前 Hidden State 投影得到原始的 q/k/v;
  2. 用因果短卷积融合当前 Token 和附近若干历史 Token 的局部信息;
  3. 把卷积后的 q/k/v 送入线性注意力的状态更新和读取过程;
  4. 由固定大小的递归状态负责聚合更长距离的全局历史。

因此,这类架构实际上把两种互补机制组合在了一起:

  • 短卷积负责局部信息:强化相邻几个 Token 之间的细节和短程模式;
  • 线性注意力负责全局信息:把不断增长的历史压缩到固定大小的状态中。

可以把它概括为:卷积补充局部关系,线性注意力聚合全局历史。

4.7.1 原理

直觉

KDA 没有位置编码,模型怎么知道”相邻 token”关系?ShortConv 是一个宽度 4 的因果深度卷积,让每个 channel 在进入递推前先”看”最近 3 个 token——局部位置信息由此注入。

想象每个通道有一个3 格的小记忆条(kernel 4 = 当前 + 前 3 格)。每来一个新 token,它把”当前值 + 前 3 个值”按一组小权重混合,作为新的输出。这个小记忆条只记得非常近的过去(4 个 token),所以它擅长捕捉局部模式(如字符级/词级的位置关系),但不负责长程 —— 长程由后面的递推(KDA)负责。

因为没有位置编码(NoPE),这个小记忆条成了模型感知”顺序/位移”的主要局部通道。

准专家视角

短卷积可以直接理解为:对 q、k、v 的每个通道分别使用一个固定长度的滑动窗口,并对窗口内的值做加权求和。即,对每个 channel 独立做因果 1D 卷积,kernel=4,无跨 channel 混合。输出 q/k/v 在进入递推前被局部平滑。它是“因果”的,意味着当前位置的输出只能使用当前 Token 和过去的 Token,不能访问未来 Token。

这是混合位置编码思路(卷积为局部位置敏感、递推为全局压缩)的关键一环。它只提供”移位”(shift)信息,配合 delta rule 的位置感知衰减共同替代 RoPE。

输入输出

  • 输入:x ∈ R^{B×T×d}(d=隐维/通道数)
  • 卷积核:W ∈ R^{d×1×k}(k=kernel_size=4,depthwise 即逐通道)
  • 输出:y ∈ R^{B×T×d}

假设卷积核长度为 4,那么当前输出会使用:

  • 前面第 3 个 Token;
  • 前面第 2 个 Token;
  • 前面第 1 个 Token;
  • 当前 Token。

每个相对位置有一个训练得到的卷积权重。以某一个通道为例,卷积和激活可以写成:

卷积输出 = SiLU(
    前第3个Token的qkv * conv_weight[:, 0] + 前第2个Token的qkv * conv_weight[:, 1] + 
    前第1个Token的qkv * conv_weight[:, 2] + 当前Token的qkv * conv_weight[:, 3]
)

这里的重点有两个:

  1. 窗口长度是固定的,所以局部计算成本不会随着总上下文长度增长;
  2. 卷积权重是在训练中学习得到的,但在具体计算时是固定权重,不会像注意力那样根据当前 Query 和局部 Key 的内容动态计算一组匹配分数。

公式(因果互相关)

y[t,c] = sum_{j=0}^{k-1} W[c,0,j] * x[t - (k-1-j), c]    # 只看当前与过去
  • 因果性:左侧补 k-1 个零/状态,保证 y

与标准做法的公式级对比

标准线性混合 ShortConv
混合范围 时间+通道全混合 时间局部 + 通道独立
参数 O(d·d) O(d·k)
位置 无 局部位移
专家视角

"无位置编码 + 局部卷积 + 递推衰减"是一套完整的位置信息方案:局部用卷积、全局用衰减序。

K3 的混合可以理解为:KDA 保留顺序、近因和 last-write-wins;NoPE MLA 提供顺序中立的集合聚合和全局内容交互。二者互补:任务需要"先后顺序"时,让 KDA 提供偏置(KDA 的顺序关系来自状态演化。即使计算被拆成 Chunk 并行,Chunk 之间仍然要按照状态转移关系组合,因此顺序不会因为并行而消失);任务需要"多证据集合聚合"时,让 MLA 减少近因偏置。

边界是:NoPE MLA 本身无法单独回答"谁先发生"。顺序信息仍需来自因果 mask、ShortConv、上游 KDA 或其他表示。

NoPE

NoPE 不是解决全部顺序问题:NoPE 只意味着 MLA 不额外加入显式位置旋转。顺序仍可能来自 causal mask、ShortConv、上游 KDA、数据本身的序列结构。所以不能把 NoPE 等同于"模型完全不理解顺序"。

另外,NoPE 不只是"因为 KDA 已经提供位置,所以省掉 RoPE"。它还避免了两套位置系统互相竞争:

KDA RoPE
数据依赖 位置依赖
相对过程 坐标 / 旋转
可收缩、可遗忘 通常保范数
内容影响位置传递 位置独立于内容

如果二者同时强行存在,模型可能同时面对:KDA 产生的内容相关相对距离;RoPE 产生的固定旋转距离。这两种位置归纳偏置可能方向不一致。所以 NoPE 的含义是避免重复或冲突的位置归纳偏置。

短卷积和滑窗注意力的区别

短卷积和滑窗注意力都会聚合附近 Token,看起来都像是在处理局部上下文,但它们的权重来源不同。

  • 短卷积使用预先学习好的固定权重。相同的相对位置通常使用同一组卷积参数,计算过程就是固定窗口内的逐通道加权求和。
  • 滑窗注意力则会根据当前内容动态计算权重。当前 Query 会分别和窗口内的 Key 做匹配,再根据匹配结果决定每个局部 Value 应该取多少。

因此,两者的差别可以这样概括:

机制 局部范围 权重如何得到 主要特点
Causal Conv1d 固定长度窗口 训练得到的固定卷积权重 计算简单、开销稳定、逐通道聚合
滑窗注意力 固定长度窗口 根据 Query-Key 内容动态计算 内容自适应,但需要额外匹配计算

短卷积并不取代线性注意力,而是为线性注意力提供一个便宜的局部信息通道。线性注意力的固定状态负责更远的历史,短卷积则弥补状态压缩对邻近细节建模不足的问题。

4.7.2 对比基线

维度 RoPE ShortConv
信息类型 全局相对位置 局部邻域混合
上下文扩展 需 YaRN 等重缩放 无需任何处理

4.7.3 计算复杂度

  • ShortConv:O(T·d·k)(k=4),几乎可忽略。

4.7.4 参数敏感性

  • conv_kernel(默认 4):调大→局部感受野↑但"位置模糊";调小→移位信息不足。

4.7.5 工程实现

项目代码 short_conv.py 中 ShortConv 类。初始化置恒等(weight[:,0,-1]=1.0)——卷积起点是 no-op,不扰动精心初始化的衰减动力学。decode 时窗口状态由 KDACache 的 conv_* 保存。

class ShortConv(nn.Module):
    """因果 depthwise 短卷积,自带显式状态以支持逐 token 精确 decode。

    本类同时提供两种形态:
    - forward:整段 / chunk 处理(prefill 或 chunkwise),一次算一整段输出,
      并返回"尾部的 kernel_size-1 个输入"作为下一段的状态;
    - step:单 token 递推(自回归 decode),用上一轮状态 + 当前 token 算输出。

    权重存储约定:weight[c, 0, lag] 乘的是 x[t - lag, c](互相关习惯),
    即 weight[:, 0, -1] 对应当前 token x
    其余置 0,于是 out
    """

    def __init__(self, dim: int, kernel_size: int = 4, *, bias: bool = False):
        super().__init__()
        self.dim = dim
        self.kernel_size = kernel_size
        self.weight = nn.Parameter(torch.zeros(dim, 1, kernel_size))
        self.bias = nn.Parameter(torch.zeros(dim)) if bias else None
        self.reset_parameters()

    def reset_parameters(self) -> None:
        """恒等初始化:卷积初始为 no-op。
        做法是权重清零,再把最后一个 tap(对应当前 token 的系数)置 1。
        这样模型一开始 q/k/v 的局部混合是恒等,不会与 KDA 初始化的
        "近似恒等衰减"互相干扰。
        """
        with torch.no_grad():
            self.weight.zero_()
            self.weight[:, 0, -1] = 1.0
            if self.bias is not None:
                self.bias.zero_()

    def forward(self, x: Tensor, state: Tensor | None = None) -> tuple[Tensor, Tensor]:
        """x 是 (batch, seq, dim)。返回 (卷积后的序列, 尾部窗口状态)。"""
        b, _, d = x.shape
        pad = self.kernel_size - 1
        xt = x.transpose(1, 2)
        left = xt.new_zeros(b, d, pad) if state is None else state
        padded = torch.cat([left, xt], dim=-1) if pad else xt
        out = F.conv1d(padded, self.weight, self.bias, groups=self.dim)
        new_state = padded[..., -pad:] if pad else padded[..., :0]
        return out.transpose(1, 2), new_state

    def step(self, x: Tensor, state: Tensor) -> tuple[Tensor, Tensor]:
        """单 token 更新(自回归 decode)。x 是 (batch, dim)。"""
        window = torch.cat([state, x.unsqueeze(-1)], dim=-1)
        out = (window * self.weight.squeeze(1)).sum(-1)
        if self.bias is not None:
            out = out + self.bias
        return out, window[..., 1:]

与开源参考核对:SGLang 用 mamba 的 causal_conv1d_fn/causal_conv1d_update;语义一致,初始化策略不同(本项目恒等初始化,kimi-k3-pytorch 用 normal_(0, 0.02) 随机初始化)。

4.7.6 讨论

为什么需要卷积?为什么 q、k、v 都需要卷积?

最简单的 Linear Attention 往往把每个 token 独立映射为 q,k,v,然后直接写入状态。这会有两个问题:相邻 token 之间缺少局部交互;单个 token 的 q,k,v 对微小噪声过于敏感。短卷积的作用是先做局部混合:

当前 token + 邻近 token → 更平滑、更有上下文的 q/k/v → 再写入长期状态

它补的是 Linear Attention 的局部感受野,不是把全部历史重新做一遍 Full Attention。

  • Q 需要卷积:Query 决定"现在要从状态中读什么"。如果只看当前 token,读取方向容易抖动。Q 卷积让查询带上局部上下文。
  • K 需要卷积:Key 决定"这条信息写入状态的哪个方向"。K 过于尖锐或噪声过大,会让相邻 token 写入完全不同的方向,造成状态碎片化。K 卷积让写入地址更稳定。
  • V 需要卷积:Value 是被写入的内容。如果 V 完全 token 独立,局部语义容易被拆散。V 卷积让写入内容带有邻域信息,类似先把一句短语整理成一个局部片段再存档。

所以 q/k/v 的卷积不是重复操作:Q 平滑读取;K 平滑地址;V 平滑内容。

为什么还需要 SiLU

卷积本身主要是线性混合。如果后面没有非线性,多个局部模式只能做线性叠加,表达能力有限。

SiLU(x) = x · σ(x)可以理解为一个平滑的"软开关":x 很小时抑制;x 较大且有用时放行;与硬 ReLU 相比,梯度更平滑。它让 q/k/v 在写入状态前能够进行输入依赖的门控和特征重组。

代价:卷积和 SiLU 会重新引入额外算子,说明 Linear Attention 并不是"完全没有非线性和局部计算"。它省掉的是长历史的全量交互,不是所有计算。

Q、K 为什么加 L2 Norm,V 为什么不需要?

Q/K 的点积决定相似度和路由。若范数变化过大,模型可能用"放大向量"代替真正的方向匹配,造成 logits、衰减和索引不稳定。L2 Norm 让 Q/K 更接近"方向相似度":

q_hat = q / ||q||_2,   k_hat = k / ||k||_2

V 不负责决定权重,它是被读取的内容。V 的幅度本身可能携带信息,因此不应强行归一化。

Decode 时如何保存卷积状态

在普通 Decode 中,每次通常只处理一个新 Token。为了计算这个 Token 的短卷积输出,需要知道它前面的 conv_kernel-1 个卷积输入。

因此,每个请求只需要保存最近的:

conv_kernel - 1

个“卷积前的 qkv”。

这里特别需要区分两种数据:

  • 缓存的是卷积之前的原始 qkv;
  • 卷积之后的输出不会再次写入 conv_state。

原因是下一步卷积需要把新的原始 qkv 与历史窗口中的原始 qkv 重新按照卷积核计算,而不是把已经卷积过的结果再次作为输入叠加。

整个缓存池的形状可以写成:

[num_cache_slots, 3 × num_heads × head_dim, conv_kernel - 1]

其中:

  • num_cache_slots 是缓存池能够容纳的物理槽位数量;
  • 3 对应 q、k、v 三类投影;
  • num_heads × head_dim 是每一类向量的总通道数;
  • conv_kernel - 1 是每个请求需要保留的历史窗口长度。

每个请求不会直接拥有一个独立的动态数组,而是通过缓存索引找到自己对应的物理槽位。新 Token 到来时,系统使用该请求的槽位读取最近的卷积输入,计算当前卷积输出,再把新的卷积前 qkv 放入窗口,供后续 Decode 使用。

因此,短卷积状态和递归状态 S 的职责不同:

  • conv_state 只保存最近几个 Token 的局部原始输入;
  • recurrent_state 把更长历史压缩到固定大小的全局记忆矩阵中。

这也解释了为什么推测解码时,conv_state 比递归状态 S 更容易回滚:卷积状态保存的是彼此独立的局部向量窗口,而 S 已经把多个 Token 混合进同一个矩阵。

4.7.7 反方辩手

  • 局部卷积提供的"位置"极其有限,长程位置信息完全靠衰减隐式编码。
  • 若失败,错在假设衰减累积足以编码任意位置关系。
  • 胜出:语义稠密、位置精度要求不高的任务。
  • 稳健替代:MLA 层加 RoPE 的混合变体(论文也评估过 Kimi Linear-RoPE 基线)。
  • 受伤方:精确坐标/时间戳引用任务。

4.8 单步递推

我们首先看看代码中的两个单步递推实现。

4.8.1 KDA 版本

KDA 的门控在推理/串行递推与训练/chunk 并行两种形态下采用两种实现,分别对应不同论文阶段:

  • v1(Kimi Linear 原版):门控用无界 softplus:

    g = -exp(A_log) * softplus(gate_logit + dt_bias)
    

    g 无下界(可以非常负),alpha = exp(g) 可以任意接近 0。这是 Kimi Linear 技术报告中的原始形式,也是 SGLang 的默认路径。

  • v2(K3 有界版):门控用有界 sigmoid:

    g = g_min * sigmoid(exp(A_log) * (gate_logit + dt_bias))
    

    g 严格在 (g_min, 0) 之间,因此 alpha 在 (exp(g_min), 1) 范围内。这是 K3 技术报告中的最终形态,也是本项目 kimi_k3 模块实际使用的版本。

两者都已在项目中实现,由 kimi_linear_step(..., version="v1"/"v2") 切换。kimi_linear_step 默认(也是 SGLang 默认)是 KDA v1;v2 是 K3 最终采用的"安全下界"版本。

4.8.2 kimi_linear_step vs KimiDeltaAttention

我们来解释下 kimi_linear_step 和 KimiDeltaAttention 有何差异和联系。

# ============================================================
# 6. Kimi Linear (Moonshot AI, 2025)
#    Core idea: KDA v1 with softplus gating
#    S_t = Diag(exp(g)) * S_{t-1} + beta*k*(v - S^T*k)^T
#    g = -exp(A_log) * softplus(z) — per-head-per-channel, unbounded below
#
#    【SGLang 对齐说明】
#    SGLang 递推核 fused_sigmoid_gating_recurrent.py
#    fused_sigmoid_gating_delta_rule_update (L274-288) 与 fused_recurrent.py
#    IS_KDA=True 分支 (L94-107) 用完全相同的时序:
#        a) b_h *= exp(g_k)              # 先衰减(per-K 通道门控)
#        b) b_v -= sum(b_h * b_k)        # 用衰减后状态算误差
#        c) b_v *= b_beta
#        d) b_h += b_v * b_k
#        e) b_o = sum(b_h * q_scaled)    # q 已乘 scale
#    门控激活形式(SGLang kda.py L1001-1006):
#        v1: g = -exp(A_log) * softplus(raw + dt_bias)          (默认)
#        v2: g = lower_bound * sigmoid(exp(A_log) * (raw + dt_bias)) (安全下界)
#    本函数因此有两处修改:
#       [1] 门控加 dt_bias 项(原版漏掉该偏置,SGLang 两版都会加)。
#       [2] 读出缩放 scale = Dk**-0.5(原版漏乘,数值验证显示输出差 1/scale ≈ 2.83 倍)。
#       [3] 衰减时序先于误差检索(与 gated_delta_net_step 同理)。
# ============================================================
def kimi_linear_step(S: torch.Tensor, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor,
                     gate_logit: torch.Tensor, beta_logit: torch.Tensor,
                     A_log: torch.Tensor, g_min: float = -5.0,
                     dt_bias: Optional[torch.Tensor] = None,
                     version: str = "v1") -> tuple[torch.Tensor, torch.Tensor]:
    """Single step of Kimi Linear / KDA.

    gate_logit (B, H, Dk) — raw gate input
    A_log (H,) — learnable log-scale per head
    dt_bias (H, Dk) or (Dk,) — per-key bias added to gate input (SGLang 有)
    g_min (float) — v2 门控的下界(衰减因子上界 alpha_max = exp(g_min))
    version — "v1":softplus 门控,g 无下界(无界衰减,SGLang 默认)
              "v2":sigmoid 门控,g ∈ (g_min, 0),保证 chunkwise 数值安全(K3 实际使用)

    KDA v1: g = -exp(A_log) * softplus(gate_logit + dt_bias)   (unbounded below)
    KDA v2: g = g_min * sigmoid(exp(A_log) * (gate_logit + dt_bias))  (bounded)
    """
    q = l2_normalize(q)
    k = l2_normalize(k)

    # 【对齐 SGLang】支持 dt_bias(kda.py L1003 / fused_recurrent.py L479, L486),
    # 缺失时退化为 0 偏置,不影响原调用方(测试未传 dt_bias 也通过)。
    z = gate_logit
    if dt_bias is not None:
        z = z + dt_bias

    if version == "v1":
        # v1: softplus 门控,无下界。softplus 无上界 → g 可非常负 → alpha = exp(g) 可任意接近 0
        g = -torch.exp(A_log).view(1, -1, 1) * F.softplus(z)
    elif version == "v2":
        # v2: sigmoid 门控,有下界。sigmoid ∈ (0,1) → g ∈ (g_min, 0),
        # 等价 alpha ∈ (exp(g_min), 1),给衰减因子一个硬上界,防 chunkwise 累积溢出。
        g = g_min * torch.sigmoid(torch.exp(A_log).view(1, -1, 1) * z)
    else:
        raise ValueError(f"unknown KDA version: {version!r} (expected 'v1' or 'v2')")

    alpha = torch.exp(g)  # decay factor in (0, 1)
    beta = torch.sigmoid(beta_logit).unsqueeze(-1)

    # 【对齐 SGLang】读出缩放(fused_recurrent.py L88),标准 softmax attention 习惯
    scale = q.shape[-1] ** -0.5
    q = q * scale

    # 【对齐 SGLang】先衰减,再用衰减后状态算误差(fused_recurrent.py L95-99)
    S = alpha.unsqueeze(-1) * S
    retrieved = torch.einsum('bhd,bhdv->bhv', k, S)
    error = v - retrieved
    S_new = S + beta.unsqueeze(-1) * torch.einsum('bhd,bhv->bhdv', k, error)
    out = torch.einsum('bhd,bhdv->bhv', q, S_new)
    return out, S_new, retrieved, error

二者是不同抽象层次的关系:kimi_linear_step 是最小的递推算子函数,KimiDeltaAttention 是完整的神经网络层,内部调用更底层的 kda_step/kda_chunkwise。

联系(数学核心一致)

二者的递推核心都是 Eq.1 的 delta rule + channel-wise decay:先衰减、再检索、再修正、最后读出。状态更新的数学形式完全相同。

【轻量化复刻大模型 / KIMI K3】KDA(Kimi Delta Attention):并行 – 罗西的思考

对拍验证:将 kimi_linear_step(version="v2") 与 kda_ref.py 中的 kda_step 在相同输入下对比——状态矩阵 S 的 max diff 为 1.11e-16(机器精度),说明二者的状态更新完全等价。

差异

维度 kimi_linear_step KimiDeltaAttention
抽象层次 单步算子函数(教学/基准) 完整神经网络层(生产级)
输入 q, k, v, gate_logit, beta_logit, A_log(已投影好的) 原始 x (B,T,d)
q/k 归一化 函数内做 L2 normalize 在 _project 中做
q scale 乘以 Dk^-0.5(对齐 SGLang fused_recurrent) 不额外乘 scale(尺度由 L2 norm + 输出门吸收)
输出差异 输出比 kda_step 小 Dk^-0.5 倍 输出尺度由 HeadRMSNorm + g_proj + o_proj 控制
v1/v2 版本 一个函数同时支持 v1 和 v2 只实现 v2(K3 形式)
ShortConv 无 有(q/k/v 各一条)
SiLU 激活 无 有(投影后)
beta 来源 传入 logit,函数内 sigmoid beta_proj 从 x 投影出
g 来源 传入 gate_logit + A_log + dt_bias log_decay 方法(低秩投影 + a_log)
输出门 无 HeadRMSNorm + g_proj + o_proj(全秩)
解码缓存 无(纯函数) KDACache(state + conv 窗口)
chunkwise 无(只有 step) 有(kda_chunkwise,块内并行)

一句话总结:kimi_linear_step 是 KDA 的"数学内核"教学演示,KimiDeltaAttention 是 KDA 的"工程完整版"——把投影、卷积、归一化、输出门、缓存管理全部包装好的实际使用层。数学核心相同,外层包裹了完整网络层所需的一切基础设施。KimiDeltaAttention 是完整的神经网络层,内部调用更底层的 kda_step/kda_chunkwise。

4.8.3 kda_step(decode 形态)

kda_step 是 v2 的"单步 decode 形态",即只做一次递推(不是整段序列)。

v2 和 kda_step 的区别只是工程封装:

  1. kimi_linear_step 输入 gate_logit,自己算 g;kda_step 输入预计算好的 g(上层 log_decay 算)
  2. kimi_linear_step 对 q 乘了 1/√Dk 的 scale(对齐 SGLang fused_recurrent);kda_step 不加(尺度由 HeadRMSNorm + 输出门吸收)
  3. 状态更新的数学本质完全一样
原理

decode 时每步只有 1 个 token,直接走 Eq.1 的单步更新(无 chunk 结构开销)。且因为首个 token 无先前状态,走 chunkwise(初始状态 0)保证与 prefill 完全一致。

计算复杂度为:单 tokenO(d_k·d_v)(一次 matmul 更新 + 一次读出)。

对比基线
维度 chunkwise kda_step
适用 prefill/训练 decode
并行 块内并行 单步
开销 三角求解 最小
工程实现

项目代码(kda_ref.py 中 kda_step):

def kda_step(q, k, v, g, beta, state):
    """单 token 更新,decode 阶段(一次只看一个 token)专用。

    q, k, g 形状 (B, H, Dk),v 形状 (B, H, Dv),beta 形状 (B, H),
    state 形状 (B, H, Dk, Dv)。逻辑与 kda_recurrent 的单步完全一致,
    只是把时间维去掉、直接在 batch 上张量化地执行。
    """
    state = g.exp().unsqueeze(-1) * state              # 逐 channel 衰减旧状态
    sk = torch.einsum("bhd,bhdv->bhv", k, state)       # S^T k:检索记忆
    delta = beta.unsqueeze(-1) * (v - sk)              # 写入方向 beta (v - S^T k)
    state = state + k.unsqueeze(-1) * delta.unsqueeze(-2)  # 外积更新 S
    out = torch.einsum("bhd,bhdv->bhv", q, state)      # 读出 o = S^T q
    return out, state

调用处在 kda.py 的 forward 中(t==1 and state_in is not None 才走单步,保证首个 decode token 也能走 chunkwise 初始化)。

if t == 1 and state_in is not None:
    # decode 形态:单 token 单步更新。注意此分支只在"有先前状态"时启用,
    # 保证 decode 的第一 token 也能走 chunkwise(初始状态为 0)。
    out, state = kda_step(
        qh[:, :, 0].to(work_dtype),
        kh[:, :, 0].to(work_dtype),
        vh[:, :, 0].to(work_dtype),
        gh[:, :, 0].to(work_dtype),
        bh[:, :, 0].to(work_dtype),
        state_in.to(work_dtype),
    )
    out = out.unsqueeze(2)                     # (B,H,1,Dv)
else:
    # prefill / 无状态 decode:chunkwise 并行形式
    fn = self._kernel()
    out, state = fn(qh, kh, vh, gh, bh, chunk_size=chunk,
                    initial_state=state_in, reset_mask=reset_mask, dtype=work_dtype)

与开源参考核对:SGLang fused_sigmoid_gating_delta_rule_update 即此单步 kernel;本项目 CPU 版等价。

4.9 chunkwise 并行(Eq.3-4,DPLR)

chunkwise 揭示了递推模型的普遍范式:能分块 = 能并行。只要转移矩阵是"对角+低秩"(DPLR 类),就存在 UT 变换把块内串行展开为矩阵运算。Mamba-2 的 SSD、GLA 都属此族。这是"RNN 复兴"的工程基石。

4.9.1 原理

直觉:递推天然串行(每步依赖上一步),但训练要并行。chunkwise 把序列切成块:块内用矩阵运算并行算出所有贡献,块间才串行传递状态。

专家视角(块长 C,进入块的状态 S_0):

O = (Γ ⊙ Q) S_0 + A Ṽ (Eq.4a 输出)
(I + M) Ṽ = Diag(β)(V − K̄ S_0) (Eq.4b UT 变换)

其中 Γ 是块内累积衰减,Q̄=ΓQ、K̄=ΓK、K̂=K/Γ,A = tril[(Q̄)(K̂)ᵀ] 是块内"注意力"矩阵,M = tril(Diag(β)K̄K̂ᵀ, −1)。(I+M) 是单位下三角 → 可精确三角求解(UT 变换)。

这是 DPLR 特例的分解:对角部分(Γ)逐元素累积,低秩部分(K̄K̂ᵀ 的 rank-1 累积)用稠密 matmul。

关键:因为衰减有下界,所有 tile(含对角)都可用 Tensor-Core 稠密乘。

4.9.2 chunkwise 形式的推导

单 Token 状态更新

设状态采用项目代码和技术报告的布局:

\[S_t\in\mathbb R^{d_k\times d_v}
\]

其中 k_t\in\mathbb R^{d_k}、v_t\in\mathbb R^{d_v} 是列向量,\alpha_t 和
\beta_t 是当前 Token 的门控量。论文 Eq. 1 的状态更新为:

\[S_t
=\left(I-\beta_t k_t k_t^\top\right)
\operatorname{Diag}(\alpha_t)S_{t-1}
+\beta_t k_t v_t^\top
\]

令:

\[M_t=
\left(I-\beta_t k_t k_t^\top\right)
\operatorname{Diag}(\alpha_t)
\]

则:

\[S_t=M_tS_{t-1}+\beta_t k_t v_t^\top
\]

这里:

  • M_t 是当前 Token 对旧状态施加的衰减与 delta-rule 修正;
  • \beta_t k_t v_t^\top 是当前 Token 的写入项;
  • M_t 和写入项由当前 Token 的 k_t、v_t、\alpha_t、\beta_t 决定;
  • 它们不需要等待 S_{t-1} 计算完成后才准备。

因此,虽然 KDA 的门控和投影可以包含非线性,但在当前 Token 的系数确定后,从 S_{t-1} 到 S_t 的状态更新是一个仿射变换。

Chunk 级状态转移

设第 i+1 个 Chunk 包含一段连续 Token。将这一段中的单 Token 仿射更新按顺序组合后,整个 Chunk 仍然可以表示为一个仿射变换:

\[\boxed{S_{[i+1]}=M_{[i+1]}S_{[i]}+\widetilde{S}_{[i+1]}}
\]

这条公式是 Chunkwise 实现的核心,也是论文中采用的状态传递形式。其中:

  • S_{[i]}:第 i 个 Chunk 的输入状态;
  • S_{[i+1]}:第 i+1 个 Chunk 的输出状态;
  • M_{[i+1]}:第 i+1 个 Chunk 的累积状态转移矩阵;
  • \widetilde{S}_{[i+1]}:第 i+1 个 Chunk 在输入状态取零时,
    由该 Chunk 自己生成的状态;
  • M_{[i+1]}S_{[i]}:输入状态经过该 Chunk 后对输出产生的影响;
  • \widetilde{S}_{[i+1]}:该 Chunk 内部 Token 独立产生的增量。

这里的下标 [i+1] 表示 Chunk 的编号,不是单 Token 的时间下标。

为避免与单 Token 公式混淆,本文统一使用:

S_t = M_t S_{t-1} + beta_t k_t v_t^T

表示单 Token 更新,使用:

S_[i+1] = M_[i+1] S_[i] + Stilde_[i+1]

表示 Chunk 之间的状态传递。论文公式的严格数学写法为:

\[S_{[i+1]} = M_{[i+1]} S_{[i]} + \widetilde{S}_{[i+1]}
\]

M 与 \widetilde S 的含义

考虑一个 Chunk 内部依次包含多个 Token。每个 Token 都有自己的局部仿射更新:

\[S_t=M_tS_{t-1}+\beta_t k_t v_t^\top
\]

将这些更新按 Token 顺序复合,可以得到:

\[S_{\mathrm{out}}
=M_{\mathrm{chunk}}S_{\mathrm{in}}
+\widetilde{S}_{\mathrm{chunk}}
\]

累积转移矩阵 M

M_{\mathrm{chunk}} 描述 Chunk 对输入状态的整体作用。它可以理解为:

  1. 将 Chunk 内所有 Token 的状态转移矩阵按顺序复合;
  2. 令输入状态为单位基底;
  3. 观察该 Chunk 对输入状态的最终变换。

直观地说,M 保留了“输入状态如何穿过整个 Chunk”的信息。

独立状态 \widetilde S

\widetilde{S}_{\mathrm{chunk}} 表示输入状态为零时,该 Chunk 通过自身 Token 生成的状态:

\[\widetilde{S}_{\mathrm{chunk}}
=
S_{\mathrm{out}}\big|_{S_{\mathrm{in}}=0}
\]

它不依赖前一个 Chunk 的输入状态,因此可以在 Chunk 内独立计算。于是,一个 Chunk 不必直接传递整段 Token 的中间递归状态,只需传递固定大小的(M, \widetilde S) 表示。

Chunk 间状态传递

假设序列被切成多个 Chunk:

Chunk 0, Chunk 1, Chunk 2, ...

第 i+1 个 Chunk 的状态更新为:

\[S_{[i+1]}
=M_{[i+1]}S_{[i]}
+\widetilde{S}_{[i+1]}
\]

因此,执行流程是:

  1. 每个 Chunk 独立计算自己的 M_{[i+1]};
  2. 每个 Chunk 独立计算自己的 \widetilde{S}_{[i+1]};
  3. 按 Chunk 顺序取得 S_{[i]};
  4. 使用论文公式计算 S_{[i+1]};
  5. 将 S_{[i+1]} 传给下一个 Chunk。

Chunk 之间仍然存在状态依赖,但依赖的数据不再是整段 Token 的中间结果,而是固定大小的 Chunk 状态。

如果要把前 n 个 Chunk 合并成一个更大的仿射变换,也可以递归组合:

\[\begin{aligned}
S_{[i+1]} &= M_{[i+1]}S_{[i]}+\widetilde S_{[i+1]},\\
S_{[i+2]} &= M_{[i+2]}S_{[i+1]}+\widetilde S_{[i+2]}.
\end{aligned}
\]

代入得到:

\[S_{[i+2]}
=
M_{[i+2]}M_{[i+1]}S_{[i]}
+
M_{[i+2]}\widetilde S_{[i+1]}
+
\widetilde S_{[i+2]}
\]

因此,两个连续 Chunk 仍然可以合并成:

\[S_{[i+2]}
=M_{[i:i+2]}S_{[i]}
+\widetilde S_{[i:i+2]}
\]

其中:

\[M_{[i:i+2]}=M_{[i+2]}M_{[i+1]}
\]

\[\widetilde S_{[i:i+2]}
=M_{[i+2]}\widetilde S_{[i+1]}
+\widetilde S_{[i+2]}
\]

这说明 Chunk 的状态转移具有可组合性。

4.9.3 对比基线

维度 纯递推 chunkwise
并行度 O(1)(串行) 块内全并行
复杂度 O(T·d_k·d_v) 相同(常数更低)
数值 逐步递推误差累积 三角求解在代数上等价,浮点误差仍取决于缩放与 dtype

4.9.4 计算复杂度

chunkwise 是训练/prefill 的并行桥梁;decode 切到 kda_step。

  • 块内 token 并行阶段主要包含 A/M 的 C×C 矩阵构造、三角求解和输出矩阵乘;仅计算 A/M 时约为 O(C²·d_k) per block,完整代价还依赖 d_v。因此不能只用一个 d_k 项概括全部 chunkwise 计算。
  • 块间串行递推:O(n_chunks · d_k·d_v)。
  • 代码通过单位下三角求解避免显式求逆;实际复杂度还取决于 C、d_k、d_v
    和所使用的 kernel,不能仅凭本节简化式断言相对所有 DPLR 实现都“显著减计算”。

4.9.5 参数敏感性

  • chunk_size(默认 64):越大块内并行越充分但内存越大;不能超过 safe_chunk_size。
  • tile_size(默认 16):数值安全基本块。

4.9.5 工程实现

伪代码(简化)
for each chunk:
    log_gamma = cumsum(g) # 块内累积衰减
    gamma, gamma_inv = exp(log_gamma), exp(-log_gamma)
    qbar = q*gamma; khat = k*gamma_inv; kbar = k*gamma
    A = tril(qbar @ khatᵀ) # 块内 attention
    M = tril(beta * (kbar @ khatᵀ), -1)
    rhs = cat([beta*v, beta*kbar], -1)
    [U,W] = solve_triangular(M, rhs) # UT 变换(精确)
    Ṽ = U - W @ S0 # 与入块状态解耦
    O = qbar @ S0 + A @ Ṽ # 读出
    S_C = gamma_last*S0 + k_tail @ Ṽ # 出块状态

真实代码

项目真实代码(kda_ref.py 中 kda_chunkwise),核心流程:

  • 累积衰减与倒数
  • qbar/khat/kbar 三把重缩放 key/query
  • M 与 A 矩阵(严格下三角 / 因果下三角)
  • UT 变换(torch.linalg.solve_triangular,unitriangular=True)
  • 块间串行递推
def kda_chunkwise(
    q: Tensor,
    k: Tensor,
    v: Tensor,
    g: Tensor,
    beta: Tensor,
    *,
    chunk_size: int = 16,
    initial_state: Tensor | None = None,
    reset_mask: Tensor | None = None,
    dtype: torch.dtype | None = None,
    return_chunk_states: bool = False,
) -> tuple[Tensor, Tensor] | tuple[Tensor, Tensor, list[Tensor]]:
    """Eqs. 3-4 的 chunkwise 并行形式。

    与 :func:`kda_recurrent` 数学等价;区别只在调度:块内 token 之间并行(一次矩阵乘
    解决),块与块之间串行传递状态。``chunk_size`` 不得超过工作 dtype 下的
    :func:`safe_chunk_size`(理由见模块 docstring 的"数值范围"段)。

    ``return_chunk_states`` 额外返回进入每个 chunk 时的状态快照——KDA 上下文并行
    (KCP)正是用它来校验自己的前缀扫描(prefix scan)是否正确。
    """
    # 工作精度:默认 fp64(输入本身是 fp64 时),否则一律 fp32 做中间计算。
    dtype = dtype or (torch.float64 if q.dtype == torch.float64 else torch.float32)
    qf, kf, vf, gf, bf = _as_float(q, k, v, g, beta, dtype=dtype)
    b, h, t, dk = qf.shape
    dv = vf.shape[-1]
    device = q.device

    state = (
        torch.zeros(b, h, dk, dv, dtype=dtype, device=device)
        if initial_state is None
        else initial_state.to(dtype).clone()
    )
    # 向上取整到整块数:“-(-t // c)” 等价于 ceil(t / c)。
    n_chunks = -(-t // chunk_size)
    pad = n_chunks * chunk_size - t
    if pad:
        # padding 的参数特意取 beta=0(不写状态)、g=0(无衰减),后期不携带任何信息,
        # 因此对递推状态是严格的 no-op——剥离 padding 后结果与不 padding 完全一致。
        qf = F.pad(qf, (0, 0, 0, pad)) # (b,h,t+pad,dk):在时间维尾部补 0
        kf = F.pad(kf, (0, 0, 0, pad))
        vf = F.pad(vf, (0, 0, 0, pad))
        gf = F.pad(gf, (0, 0, 0, pad))
        bf = F.pad(bf, (0, pad)) # beta 是 (b,h,t),只补时间维
        if reset_mask is not None:
            reset_mask = F.pad(reset_mask, (0, pad), value=False) # 补的 padding 不是新文档起点

    c = chunk_size
    # 重排成 (batch, head, chunk 数, chunk 内位置, 特征),让"块内位置"变成一个可矩阵化的维。
    q_c = qf.view(b, h, n_chunks, c, dk)
    k_c = kf.view(b, h, n_chunks, c, dk)
    v_c = vf.view(b, h, n_chunks, c, dv)
    g_c = gf.view(b, h, n_chunks, c, dk)
    b_c = bf.view(b, h, n_chunks, c)

    # ---- token 并行阶段 ----------------------------------------------------
    # 所有"不依赖传入状态 S_0"的量都能对全部 chunk 一次性算完——这正是 fused kernel
    # 把这段空排成与递推无关的独立调度阶段的原因(论文 §5.1.1)。
    # (下面 gamma/khat/kbar 等符号全部对应模块 docstring 的推导记号。)

    # 每个 chunk 开始起的累积 log-decay(含当前位置):log gamma^{1->r}。
    log_gamma = g_c.cumsum(dim=3)
    gamma = log_gamma.exp() # gamma = 保留因子连乘,<= 1(因 g<=0)
    gamma_inv = (-log_gamma).exp() # 1/gamma >= 1,上界 exp(-c * g_min)

    # 三把重缩放 key/query,分别为 Eq. 4 的 Gamma*Q、K/Gamma、K*Gamma。
    q_bar = q_c * gamma # Q * Gamma
    k_hat = k_c * gamma_inv # K / Gamma
    k_bar = k_c * gamma # K * Gamma

    # chunk 内的文档分段:不同文档的位置绝不能互相读;只有第一个文档段能读携带状态。
    # seg 取值 = "相对 chunk 起点的文档起始计数"(_segment_ids 做的事 + 减去块首偏置)。
    seg = _segment_ids(reset_mask, b, n_chunks * c, device).view(b, n_chunks, c)
    seg = seg - seg[:, :, :1] # 把每个 chunk 的第一个位置计成 0
    if reset_mask is not None:
        # 若 chunk 首 token 自己就是新文档起点,seg 会整体 -1 成负值;这里 +1 把它
        # 抬回 0、该 chunk 从"新文档"开始(不允许读携带状态,由 carry_ok 处理)。
        seg = seg + reset_mask.view(b, n_chunks, c)[:, :, :1].long()
    same_seg = (seg[:, :, :, None] == seg[:, :, None, :]).unsqueeze(1) # (b,1,nc,c,c) 块内同文档对
    carry_ok = (seg == 0).to(dtype)[:, None, :, :, None] # (b,1,nc,c,1) 只有文档段 0 能接续旧状态
    last_seg = seg[:, :, -1:] # (b,nc,1) 每个 chunk 的末位置文档号
    in_last = (seg == last_seg).to(dtype)[:, None, :, :, None] # (b,1,nc,c,1) 标记"属于末段"的位置
    keep = (last_seg == 0).to(dtype)[:, None, :, :, None] # (b,1,nc,1,1) 末位置仍在文档 0 才传递状态

    # 因果 / 严格下三角掩码,用于构造 A 与 M。
    causal = torch.ones(c, c, dtype=torch.bool, device=device).tril() # 下三角(含对角线)
    strict = causal & ~torch.eye(c, dtype=torch.bool, device=device) # 严格下三角(不含对角线)

    # M = tril(Diag(b) Kbar Khat^T, -1),用 strict & same_seg 同时遮挡"对角线"与"跨文档":
    # (k_bar @ k_hat^T) 是 (b,h,nc,c,c) 的块内点积矩阵,先乘 beta 再按掩码过滤。
    m = (b_c.unsqueeze(-1) * (k_bar @ k_hat.transpose(-1, -2))) * (strict & same_seg)
    # A = Tril[(Q*Gamma)(K/Gamma)^T],因果 & 同文档即可(允许对角线,对应 o_i 能读到自身贡献)。
    a = (q_bar @ k_hat.transpose(-1, -2)) * (causal & same_seg)

    # UT 变换,两套右端项合并成一次三角求解:
    # U = (I+M)^-1 Diag(beta) V , W = (I+M)^-1 Diag(beta) Kbar
    # 于是 Vt = U - W S_0,其中 S_0 由逐 chunk 的递推在串行阶段提供。
    # I + M 是单位下三角,所以 solve_triangular 的求解是精确且 O(c^2) 廉价的。
    # 注意第二列右端项乘了 carry_ok:只有文档段 0 的位置才携带 Kbar 去算 W(供接续旧状态)。
    rhs = torch.cat(
        [b_c.unsqueeze(-1) * v_c, b_c.unsqueeze(-1) * (carry_ok * k_bar)], dim=-1
    )
    uw = torch.linalg.solve_triangular(m, rhs, upper=False, unitriangular=True)
    u_mat, w_mat = uw[..., :dv], uw[..., dv:] # 拆回 U 与 W,供下面的逐 chunk 递推用

    # 块末状态更新要用的三个量:
    log_tail = log_gamma[:, :, :, -1:] - log_gamma # log gamma^{r+1->C} <= 0(从 r 到块末的衰减)
    k_tail = (k_c * log_tail.exp() * in_last).transpose(-1, -2) # (b,h,nc,dk,c) 加权后的 K^T
    gamma_last = gamma[:, :, :, -1].unsqueeze(-1) # (b,h,nc,dk,1) 整块累积衰减 gamma^{1->C}

    # ---- head 并行递推:唯一的串行部分 ---------------------------------------
    outputs = []
    chunk_states: list[Tensor] = []
    for i in range(n_chunks):
        if return_chunk_states:
            chunk_states.append(state.clone()) # 记录进入第 i 块前的状态快照
        v_tilde = u_mat[:, :, i] - w_mat[:, :, i] @ state # Vt = U - W S_0
        outputs.append(
            carry_ok[:, :, i] * (q_bar[:, :, i] @ state) + a[:, :, i] @ v_tilde
        ) # O = (Gamma*Q) S_0 + A Vt:携带项只对文档段 0 生效,其余位置只有块内注意力
        # 块末状态:S_C = Diag(gamma^{1->C}) S_0 + sum_r (gamma^{r+1->C} k_r) Vt_r^T
        # keep 只在该 chunk 末位置仍属于文档 0 时才传递状态,避免跨文档泄漏。
        state = keep[:, :, i] * (gamma_last[:, :, i] * state) + k_tail[:, :, i] @ v_tilde

    out = torch.cat(outputs, dim=2) # 沿 chunk 维拼回 (b, h, n_chunks*c, dv)
    if pad:
        out = out[:, :, :t] # 剥掉 padding 恢复原始序列长度
    if return_chunk_states:
        return out, state, chunk_states
    return out, state

与开源参考核对:与 kimi3-master 的 kda_ref.py 的 chunkwise 语义一致;与 SGLang chunk_kda(FLA kernel)逻辑逐位对齐(CPU 版用 PyTorch 实现同一数学)。

图例

下面是 kda_chunkwise(kda_ref.py:225)的 ASCII 流程图与数据流图。

流程图(控制流)

kda_chunkwise_图例

数据流图(张量形状演变)

kda_chunkwise_数据流

关键点:

  • 阶段 1(全并行):所有不依赖携带状态 S 的量(A、M、U、W、掩码、衰减)一次性算完。
  • 阶段 2(串行):仅 n_chunks 步递推,每步 Vt = U − W·S、O = ΓQ·S + A·Vt、更新 S。
  • carry_ok/keep/same_seg 三个掩码保证跨文档不泄漏(reset_mask 语义)。
  • padding 取 β=0, g=0,对递推严格 no-op。

4.9.6 反方辩手

  • chunkwise 的 1/γ(K/Γ 重缩放)在无界衰减下溢出——这就是必须改有界衰减的原因。
  • 若失败,错在假设三角求解与块间递推的数值稳定性。
  • 胜出:硬件充裕、需训练并行。
  • 稳健替代:纯递推(慢但简单)。
  • 受伤方:精度敏感的 tiny 模型。

4.9.7 讨论

Q1:为什么 chunk 还要细分成 16 token tile

一个大 chunk 内 token 越多,相对衰减跨度越大。直接对整个 chunk 展开,数值范围和中间矩阵都可能过大。细分为 16 token tile 后:每个 tile 内的衰减范围较小;tile 之间可以用规则矩阵乘法;更容易装入 Tensor Core 的 tile 形状;中间结果更容易保持在可控范围。

Q2:为什么 Tril 要保留对角线

chunkwise 计算通常会构造一个 token‑pair 关系矩阵:第 t 个 token 读取哪些更早 token 的写入。如果只保留下三角但去掉对角线,表示的是:当前 token 只能看到过去写入的内容。但 KDA 的实际定义是:当前 token 先把自己的内容写进去,然后读取"包含自己写入之后"的状态。所以 token 自己对自己的作用必须保留,对应矩阵对角线必须是 1 或保留该项。

通俗说:不保留对角线,当前员工看不到自己刚提交的修改;保留对角线,当前员工的修改会立刻进入本次结果。

Q3:为什么非对角 tile 容易 GEMM,而对角 tile 较难

非对角 tile 表示:一整块 token 对另一整块 token 的影响。它们之间没有"当前 token 不能看未来 token"的复杂边界,可以直接组成稠密矩阵乘。

对角 tile 则包含同一小块内部的因果关系:token 0 不能看 token 1;token 1 可以看 token 0;token 1 还要看到自己的写入;每个位置的衰减长度不同。因此对角 tile 必须显式计算 position‑pair,难以完全当作普通稠密 GEMM。

Q4:每个 token 都是一个仿射变换

把 KDA 的单步更新写成:S_t = M_t S_{t-1} + b_t,其中 M_t 是旧状态经过多少衰减和擦除,b_t 是当前 token 写入的新内容。两个 token 连起来:

S_2 = M_2(M_1 S_0 + b_1) + b_2
S_2 = (M_2 M_1) S_0 + (M_2 b_1 + b_2)

所以两步可以合并成一个新的仿射变换:(M_2, b_2) ∘ (M_1, b_1) = (M_2 M_1, M_2 b_1 + b_2)。

Q5:稳定性主要在 M_t,表达力主要在 b_t

S_t = M_t S_{t-1} + b_t

  • M_t 决定旧状态如何传播、衰减和变形,关系到爆炸、消失和数值稳定;
  • b_t 决定新信息写入哪些方向、写入多少,关系到能表达多少内容。

因此可以粗略说:M_t 管"旧记忆会不会坏掉",b_t 管"新知识能不能写进去"。

Q6:交叉项是表达力和并行难度的根源

两段合并时:b_C = M_2 b_1 + b_2。前一段写入的 b_1 必须接受后一段 M_2 的继续作用。这让 KDA 比纯加法更能表达"后续 token 修改前面 token 的影响",但也使段间合并必须保持顺序,不能直接无序求和。

Q7:单位元的意义

仿射变换的单位元是:(I, 0)。它表示状态保持不变。因此空 token 序列可以视为一个合法的空 chunk:不改变状态;仍可参与 chunk 合并;边界不需要额外 if 分支;分段算法更规整。

Q8:结合律为什么重要

如果一段 token 的净效果可以浓缩成一个 (M_C, b_C),就可以:先算 chunk A;先算 chunk B;再把 A、B 的结果合并。而且 ((A∘B)∘C) = (A∘(B∘C)),也就是说,加括号的方式不影响最终结果。这就是结合律。

它允许:把长序列切成多个 chunk;chunk 内部并行计算;用树形归约或分层归约合并;用 Tensor Core 处理块矩阵;形成 FlashKDA、KCP 一类的并行策略。

Q9:为什么不满足交换律

交换两个 token:(M_2, b_2)∘(M_1, b_1) ≠ (M_1, b_1)∘(M_2, b_2)。原因就在交叉项:M_2 b_1 + b_2。前一个 token 写入的 b_1,必须先被后一个 token 的 M_2 衰减或擦除,再和 b_2 相加。如果交换顺序,交叉项就变成 M_1 b_2 + b_1,一般不会相等。

Q10:非交换性为什么正好编码时序

语言中的顺序本来就有意义。KDA 的"后写入继续作用于先写入"的机制,使得新信息可以覆盖旧信息;越近的信息通常影响更大;中间发生过什么会影响最终状态;顺序信息保留在状态转移乘积中。这就是 KDA 的近因偏置和时序表达能力。

Q11:为什么 (I+M) 容易求解? → I+M 是单位下三角矩阵,因此可以用forward substitution 直接求解,不需要显式矩阵求逆。这里的“精确”是代数结构上的精确解法;实际浮点误差仍受 khat 的放大、dtype 和 chunk/tile 大小影响,不能称为“无条件数值稳定”。

Q12:padding 是否影响结果? → 不会:padding 位置 β=0、g=0,对状态是 no-op。

Q13:跨文档(reset_mask)怎么处理? → _segment_ids 用 cumsum 标记文档边界,跨文档位置强制不交互。

4.10 KDA 上下文并行(KCP)

本节是对前面一节的扩展。

4.10.1 原理

直觉:

多卡并行时,序列切成 N 段分给 N 卡。对 softmax attention,跨 rank 的 KV 交换量通常随需要访问的上下文长度增长;

KDA 状态不能简单相加,因为递推的转移矩阵不可交换。

解法:每段分解出"自己的贡献"(Stilde)与"传递矩阵"(M),再按 S = M·S + Stilde 前缀扫描重组。

专家视角:

S_{[i+1]} = M_{[i+1]} S_{[i]} + Stilde_{[i+1]}

这里是论文 Eq.17 在“本段处理完全部本地 Token”时的简化写法。论文的更一般形式是在第 i+1 个 rank 内处理到本地位置 t:

\[S^{t}_{[i+1]}
=\widetilde S^{t}_{[i+1]}
+M^{t\leftarrow1}_{[i+1]}S^{T_i}_{[i]}
\]

当 t=T_{i+1} 时,M^{T_{i+1}\leftarrow1}_{[i+1]} 和 \widetilde S^{T_{i+1}}_{[i+1]} 就是该 rank 对外交换的两个局部量,从而得到上面的段末状态公式。

  • Stilde_{[i]}:段 [i] 从 S=0 起算的自生成状态(可并行算)。
  • M_{[i]}:段 [i] 的累积转移矩阵(v=0、S0=I 下跑完该段)。
  • 通信张量只包含 (M,\widetilde S);在状态维度、dtype 和并行度固定时,张量形状与序列长度无关。

报告 Eq.17 的准确含义是:在 d_k、d_v、batch/head 数以及 world size 固定时,每个 rank 只需交换固定形状的 M 和 \widetilde S;通信张量大小不随本地序列长度增长,但 all-gather 的总流量仍会随 rank 数和实现细节变化。这是 KDA KCP 与需要交换增长中的 KV 块的 softmax CP 的关键区别。

KIMI-K3-Eq17

4.10.2 解读

KDA 的状态更新看起来是逐 Token 的递归过程,但它可以被整理成适合Chunkwise 并行的仿射状态转移。

4.10.2.1 Chunkwise 实现的三个阶段

Chunkwise 可以被分成三个阶段。

阶段 主要工作 Chunk 之间的依赖
1. 块内预计算 准备衰减系数,将块内残差依赖整理成下三角系统,并计算不依赖输入状态的辅助矩阵 不同 Chunk 可以并行
2. 块间状态传递 使用论文公式 S_[i+1] = M_[i+1] S_[i] + Stilde_[i+1] 更新块末状态,并保存输出阶段需要的数据 同一请求内按 Chunk 顺序递推
3. 块内输出 使用已准备好的块首状态和实际残差,计算每个 Token 的输出 不同 Chunk 可以并行

阶段 2 保留了 Chunk 之间的顺序依赖,但其并行度仍然来自:

  • 请求数量;
  • Head 数量;
  • Value 轴方向上的分块。

阶段 1 和阶段 3 则可以利用序列维度上的并行性。

4.10.2.2 两层并行
宏观并行:Chunk 之间

每个 Chunk 可以先独立算出:

  • 对输入状态的累积转移矩阵 M_{[i]};
  • 输入状态为零时的独立状态 \widetilde S_{[i]}。

之后再按照:

\[S_{[i+1]} = M_{[i+1]}S_{[i]}+\widetilde S_{[i+1]}
\]

顺序传递 Chunk 状态。

因此,Chunk 的独立预计算可以并行,只有固定大小的状态传递保留顺序。

微观并行:Chunk 内部

Chunk 内部的残差更新本来是递归的。通过代数整理,可以将其改写成下三角系统和矩阵运算。

文中使用的辅助矩阵形式为:

\[U=(I+A)^{-1}BV
\]

\[W=(I+A)^{-1}BK_{\mathrm{in}}
\]

\[E=U-WS_0^\top
\]

这些式子用于把大量块内递推转成矩阵乘法,使硬件更容易并行执行。其中,S_0 表示当前 Chunk 的输入状态;它由 Chunk 间的论文公式提供,而不是在Chunk 内重新计算得到。需要注意,U/W/E 的具体记号来自 chunkwise 推导,而 Eq.17 的 KCP 只负责段级 (M,\widetilde S) 重组,二者不是同一层次的公式。

4.10.2.3 为什么这种形式适合上下文并行

对于第 i+1 个 Chunk,只要知道:

(M_[i+1], Stilde_[i+1])

以及前一个 Chunk 的输出状态 S_[i],就可以计算:

\[S_{[i+1]} = M_{[i+1]}S_{[i]}+\widetilde S_{[i+1]}
\]

通信只需交换固定大小的状态表示,而不需要交换该 Chunk 内每个 Token 的全部中间递归结果。

因此,当状态维度和并行度固定时,单个 rank 的 KCP 交换张量形状与序列长度无关。序列变长时,主要增加本地计算量;如果改变 rank 数,all-gather 的总通信量当然会变化。这正是论文 Eq.17 所体现的固定状态通信性质,也是递推模型上下文并行与 softmax attention 上下文并行的重要区别。

4.10.2.4 与注意力类型的对比

Softmax attention 通常需要处理跨 Token 的归一化和全局依赖,序列分片后需要交换更多与序列长度相关的统计量或中间结果。

KDA 的递归状态则可以在 Chunk 级别表示为:

\[S_{[i+1]} = M_{[i+1]}S_{[i]}+\widetilde S_{[i+1]}
\]

每个 Chunk 只需提供一个固定形状的累积转移矩阵和一个固定形状的独立状态。这使得:

  • Chunk 内 Token 可以通过矩阵运算并行处理;
  • Chunk 之间只需传递固定大小的状态;
  • 上下文并行可以将顺序依赖压缩到 Chunk 级别;
  • 状态 checkpoint 可以自然放在 Chunk 边界。

关键不在于整个模型是否线性,而在于:

  1. 递归状态如何进入下一步更新;
  2. 更新系数是否可以在不依赖递归状态的情况下提前准备;
  3. 状态更新是否能被写成可组合的线性或仿射形式。

对 KDA 而言,单 Token 级别的仿射形式是:

\[S_t=M_tS_{t-1}+\beta_t k_t v_t^\top
\]

Chunk 级别、也是论文采用的形式是:

\[\boxed{
S_{[i+1]} = M_{[i+1]}S_{[i]}+\widetilde S_{[i+1]}
}
\]

前者解释局部递归如何组合,后者解释 Chunk 如何进行状态传递与上下文并行。

4.10.3 对比基线

维度 softmax CP KDA KCP
通信张量大小 随需交换的 KV 长度增长 O(d_k^2 + d_k d_v),对固定并行度与 dtype 不随序列长度增长
跨段重组 交换/聚合增长中的 KV 块 M·S + \widetilde S 前缀扫描

4.10.3 反方辩手

  • a) KCP 的前缀扫描是串行的(段间依赖),极端多卡下通信重叠收益递减。
  • b) 若失败,错在假设固定状态值得交换成本。
  • c) 胜出:超长上下文 + 多卡。
  • d) 稳健替代:直接全量(序列不长时)。

4.10.4 工程实现

kda_chunkwise(..., return_chunk_states=True) 专门提供段入状态快照——是 KCP 的验证接口。参考实现 kimi3-master/parallel/kcp.py 实现报告 Eq.17 的前缀扫描;本项目parallel/kcp.py 是 CPU/单进程的等价模拟,不是已经接入 torch.distributed 的生产多卡实现。真正的跨 rank 交换需要把本地 (M,\widetilde S) 接入 all-gather,再按文档边界处理前缀扫描。

核心代码:

def local_transition_and_state(k, v, g, beta, *, chunk_size=16, dtype=None):
    # Stilde:段从 S=0 开始,自身激活产生的状态
    _, s_tilde = kda_chunkwise(k, k, v, g, beta, chunk_size=chunk_size, dtype=dtype)
    # M:homogeneous 响应。v=0 后写入项消失,只剩过渡项,给定 S0=I,
    # 最终 state 就等于段内累积过渡矩阵
    eye = torch.eye(dk, dtype=dtype, device=k.device).expand(b, h, dk, dk).contiguous()
    zero_v = torch.zeros_like(k)
    _, m_cum = kda_chunkwise(k, k, zero_v, g, beta, chunk_size=chunk_size,
                            initial_state=eye, dtype=dtype)
    return m_cum, s_tilde

def prefix_scan_states(transitions, zero_states):
    # transitions: (n,B,H,Dk,Dk) 每段过渡矩阵 M;zero_states: (n,B,H,Dk,Dv) 每段 Stilde
    n = transitions.shape[0]
    incoming = torch.zeros_like(zero_states)
    state = torch.zeros_like(zero_states[0])
    for i in range(n):
        incoming[i] = state
        state = transitions[i] @ state + zero_states[i]
    return incoming

def kcp_forward(q, k, v, g, beta, *, chunk_size=16, n_segments=4, dtype=None):
    # 阶段 1:每段局部 (M, Stilde)(分布式下在各自 rank 本地并行执行)
    m_all, s_all = [], []
    for sl in segs:
        m_, st_ = local_transition_and_state(k[:,:,sl], v[:,:,sl], g[:,:,sl], beta[:,:,sl], ...)
        m_all.append(m_); s_all.append(st_)
    Ms = torch.stack(m_all, dim=0)
    Ss = torch.stack(s_all, dim=0)
    incoming = prefix_scan_states(Ms, Ss) # 前缀扫描重组出各段入状态
    # 阶段 2:逐段用 incoming 重新计算真实输出
    outs = []; state = incoming[0]
    for i in range(n_segments):
        out, state = kda_chunkwise(q[:,:,segs[i]], ..., initial_state=incoming[i], ...)
        outs.append(out)
    return torch.cat(outs, dim=2), state

通信张量:每个 rank 每个 KDA 层交换两个定长张量 (M,\widetilde S);其元素数约为 d_k^2+d_kd_v,字节数还要乘以 batch、head 和 dtype 的 item size。它对序列长度不变,但 all-gather 的总字节数会受 world size 和通信算法影响。

4.10.5 实测数值

直接相加 vs KCP 前缀扫描(64 token, 4 段):

  • 直接相加误差:9.782e-01(几乎完全错误)——证明"跨段不可加"。
  • KCP 前缀扫描误差: 1.110e-16(机器精度)——证明 M/Stilde 分解 + 前缀扫描是精确的。
  • state rms:0.11986。

通信量对比:原文中的具体字节数必须绑定配置后再引用。按本项目communication_volume(batch, heads, dk, dv, world, itemsize) 的定义,单层基础张量大小为batch * heads * (dk^2 + dk*dv) * itemsize;函数返回值还乘以 world,表示集群总量。
例如 batch=heads=1、dk=dv=16 时,float32 为 2048 bytes,float64 为4096 bytes,不能脱离 dtype 统一写成“恒定 2048 bytes”。softmax 的 1MB/1GB数字也应标明 KV 维度、dtype、rank 数和统计口径,否则只能作为某组实验配置下的示例。

4.10.6 讨论

KCP 是"状态压缩 → 通信恒定"的典范:当每个 worker 只携带固定状态,分布式成本与上下文无关,这是线性注意力在系统层面的杀手锏——也是 K3 报告 §5.1 花大篇幅讲系统协同设计的理由。

Q1:为什么状态不能像 vanilla 线性注意力那样相加? → 更新里有 (I−βkk^T) 作用于 S,各段转移算子不可交换,S_seg1 + S_seg2 无意义(实测:直接相加误差 0.978)。


4.11 解码缓存 KDACache 与 clone() 分支

4.11.1 原理

直觉:KV cache 可逐 token 截断回滚;KDA 状态是整体矩阵,回滚只能靠快照。

专家视角:KDACache = {state, conv_q, conv_k, conv_v},clone() 深拷贝。clone() 是 beam search / 投机解码"分支状态"的接口——候选续写互不污染,失败候选丢弃副本。这是 SGLang ReplaySSM 在工程上解决的同款问题。

4.11.2 对比基线

维度 KV cache KDACache
回滚 截断即可 快照/重放
尺寸 随 T 增长 固定

4.11.3 反方辩手

  • a) 每分支都 clone 整个状态矩阵,beam 宽度大时显存爆炸。
  • b) 若失败,错在假设分支数小。

4.11.4 工程实现

项目代码(kda.py 中 KDACache):

class KDACache:
    """每层的解码状态:递推矩阵 + 三条 ShortConv 的滑动窗口。

    这是 KDA 的"KV cache 等价物",但与 softmax attention 的 cache 有本质区别:
    - 尺寸固定:state 是 (batch, num_heads, head_dim_k, head_dim_v),
      不随已生成 token 数增长;只有 conv_* 的窗口随 kernel_size 固定。
    - 是整体状态而非逐 token 条目,因此"回滚到第 k 步"无法靠简单截断实现,
      只能靠保存/恢复快照(clone())。

    支持 clone():beam search 和投机解码需要在不同候选续写之间分支状态而不互相污染。
    """

    __slots__ = ("state", "conv_q", "conv_k", "conv_v")

    def __init__(self, state, conv_q, conv_k, conv_v):
        self.state = state          # (B, H, Dk, Dv) 递推状态矩阵 S
        self.conv_q = conv_q        # (B, k_dim, kernel-1) q 卷积窗口
        self.conv_k = conv_k        # (B, k_dim, kernel-1) k 卷积窗口
        self.conv_v = conv_v        # (B, v_dim, kernel-1) v 卷积窗口

    def clone(self):
        return KDACache(
            state=self.state.clone(),
            conv_q=self.conv_q.clone(),
            conv_k=self.conv_k.clone(),
            conv_v=self.conv_v.clone(),
        )

init_cache 方法:为 decode 初始化空状态,之后每步由 forward(cache=...) 原地更新。

模型级使用示例:init_caches 逐 block 调 block.attn.init_cache(...) → 返回 KDACache 或 MLACache 的混合列表;generate 逐 token 复用同一份 caches。

cache 列表长度 = n_layers(block 数),但 contents 是 KDA/MLA 两类交替。每层 attention 只消费 caches[i],list 下标 i 恰好对应该 block。

4.11.5 问题 & 讨论

Q1:KDACache 中,state 是针对每个 head 一个吗?

精确说是每 batch × 每 head 一个递推矩阵:

state       (B, H, Dk, Dv)   # B=批次, H=head 数, Dk/Dv=head 维 key/value

即每个 head 持有自己独立的 Dk×Dv 矩阵 S。逐 head 独立——S_h ∈ R^{Dk×Dv},delta-rule 递推 S ← γS + beta·k·vᵀ 在 H 维各自进行,互不混合。

conv_q/conv_k/conv_v 却不是逐 head 的:形状 (B, k_dim, kernel-1)、(B, v_dim, kernel-1),其中 k_dim = H·Dk、v_dim = H·Dv——卷积窗口按扁平宽存,head 维被折叠进第二维,因为 ShortConv 先于 head 切分。

所以结构是:head 各自的空间给 state(矩阵),共享的空间给卷积窗口(扁平宽)。这与 KDA 的语义一致——递推状态是每头的关联记忆,卷积只做 token 级局部混合、作用在整个宽向量上、head 无关闭。

Q2:K3 是每个 block 一个 Cache?

是的——但 cache 的类型随 layer 交替。因为每个 block 只有一个注意力子层 self.attn,它要么是 KimiDeltaAttention 要么是 GatedMLA。所以:

block attn 类型 cache 类型
偶数层 KDA KDACache
奇数层 GatedMLA MLACache

与开源参考核对:kimi3-master 用 serve/decode.py::SpeculativeKDAState + ReplaySSM 处理回滚(不存每步快照,而是重放轨迹);本项目 clone() 是简单但正确的 CPU 等价。

Q3:Block KDA 的 State 描述是否合理?

合理,但应理解为实现语义变化:

  • 纯 KDA: implicit + long-term,状态隐式包含历史;
  • block KDA: 把一段历史的状态效果显式封装为 block‑level transition;
  • 配合 Full/Sparse: 提供 explicit/addressable + short‑term 访问。

它不是把 KDA 变成了普通 KV Cache,而是把部分隐式状态管理显式化,以支持缓存、分块和并行。


0x05 可视化与实验

5.1 可视化脚本

脚本 生成图 对应小节
experiments/models/llm/kimi_k3/modules/kda/plot_kda_principle.py KDA 原理图(状态矩阵 + 三件事 + delta 修正 + 有界门控) 原理(Eq.1 + Eq.5)
experiments/models/llm/kimi_k3/modules/kda/plot_kcp.py KCP 前缀扫描验证 + 通信量对比 KCP
experiments/models/llm/kimi_k3/modules/kda/plot_decay_stability.py 数值稳定性(1/gamma 上界、safe_chunk_size、失败 case) 有界衰减 + 数值安全

5.2 图说明

plot_kda_principle.png 四格:

  • 左上:状态矩阵 S(记忆)热图。展示固定大小 Dk × Dv 矩阵,T 个 token 只产生一个矩阵。
  • 右上:单步三件事拆解(衰减→检索→只写误差)。
  • 左下:delta rule 的"纠错"本质(同一 key 下 v 逐渐收敛到最新值)。
  • 右下:有界 log-decay(g ∈ (g_min, 0))曲线。

plot_kda_principle

plot_kcp.png 两格:

  • 左图:直接相加 vs KCP 前缀扫描的误差(log 轴)。
  • 右图:KDA 固定通信量 vs softmax 线性增长(log-log)。

plot_kcp

plot_decay_stability.png 四格:

  • 左上:1/gamma 上界 vs tile 大小(有界版是数据无关直线)。
  • 右上:实测 tile=16——无界 z+6/z+9 爆成 inf。
  • 左下:safe_chunk_size vs g_min。
  • 右下:失败 case——chunk=64 时连有界版也溢出。

plot_decay_stability

5.3 关键实测数值回填

KCP 实验:

  • 直接相加误差:9.782e-01
  • KCP 前缀扫描误差:1.110e-16
  • 通信量:KDA 恒定 2048 字节;softmax 从 1k 到 1M 序列线性到 1GB

数值稳定性实验:

  • max_reciprocal_decay(-5, 16) = 5.5406e+34 = exp(80)
  • safe_chunk_size(-5, bf16) = 16 = 论文 tile
  • safe_chunk_size(-5, fp16) = 1
  • safe_chunk_size(-8, bf16) = 10
  • 无界 softplus z+6 tile=16 → inf/NaN
  • chunk=64 连有界版也溢出(印证安全边界)

5.4 测试验证

测试 路径 结果
chunkwise vs recurrent 对拍 tests/models/llm/kimi_k3/modules/test_kda.py chunk=1/2/4/8/16/32/64 全部一致
kda_step 与序列一致 同上 一致
module forward 形状与 cache 同上 通过
decoding 与 prefill 一致 同上 max diff < 1e-6
decay 下界有界 同上 通过
初始化 target alpha_init 同上 通过
tile 倒数 fits BF16 同上 通过
unbounded decay overflow 同上 验证溢出
KCP 等于全递推 tests/models/llm/kimi_k3/parallel/test_kcp.py 一致
KCP 通信量与长度无关 同上 通过
ShortConv 因果性/恒等初始化/分块一致/step 匹配 tests/models/llm/kimi_k3/modules/test_short_conv.py 7 项全部通过

合计 37 项测试全部通过。



TransFormer-封面


0xFF 参考

AI Infra进阶:3万字深入理解线性注意力机制



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *

தீவிர சூறாவளி புயல் 'முரசு' சென்னையில் நாளை பள்ளிகளுக்கு விடுமுறை 'முரசு' புயல் குறித்த தகவல்கள் சென்னையில் நாளை பள்ளிகளுக்கு விடுமுறையா? வானிலை சென்னையில் நாளை விடுமுறையா? 'முரசு' புயல் வானிலை சென்னையில் புயல் 'முரசு' புயல் கண்காணிப்பு சென்னையின் வானிலை தெலுங்கானா சென்னை வானிலை புயல் கண்காணிப்பு செங்கல்பட்டில் இன்று பள்ளிகளுக்கு விடுமுறை சென்னையில் பள்ளிகளுக்கு விடுமுறை செங்கல்பட்டில் பள்ளிகளுக்கு விடுமுறை விஜயவாடா வானிலை வானிலை அறிக்கை 'முரசு' புயல் கண்காணிப்பு இன்றைய வானிலை அறிக்கை சென்னையில் நாளை பள்ளிகளுக்கு விடுமுறை சென்னை வானிலை சென்னையில் இன்று பள்ளிகளுக்கு விடுமுறையா? இன்றைய சென்னை வானிலை விமான சேவைகள் தமிழ் பக்தாகோர் (Pakhtakor) vs அல்-நாசர் (Al-Nassr) பக்தாகோர் எஃப்சி vs அல்-நாசர் எஃப்சி தரவரிசை ஏஎஃப்சி (AFC) சாம்பியன்ஸ் லீக் பக்தாகோர் எஃப்சி vs அல்-நாசர் எஃப்சி அணிகளின் விவரம் ஜோவா ஃபெலிக்ஸ் (João Félix) அல்-நாசர் எஃப்சி பக்தாகோர் எஃப்சி நியூ இங்கிலாந்து (New England) vs இன்டர் மியாமி (Inter Miami) நியூ இங்கிலாந்து ரெவல்யூஷன் vs இன்டர் மியாமி தரவரிசை எம்எல்எஸ் (MLS) இன்டர் மியாமி லியோனல் மெஸ்ஸி இன்டர் மியாமி சிஎஃப் இன்டர் மியாமி vs நியூ இங்கிலாந்து நியூ இங்கிலாந்து ரெவல்யூஷன் vs இன்டர் மியாமி அணிகளின் விவரம் மெஸ்ஸியின் மொத்த கோல்கள் நியூ இங்கிலாந்து ரெவல்யூஷன் vs இன்டர் மியாமி போட்டி நிகழ்வுகள் இன்டர் மியாமி போட்டிகள் நியூ இங்கிலாந்து ரெவல்யூஷன் நியூ இங்கிலாந்து ரெவல்யூஷன் vs இன்டர் மியாமி ஓமன் vs நெதர்லாந்து ஓமன் தேசிய கிரிக்கெட் அணி vs நெதர்லாந்து தேசிய கிரிக்கெட் அணி தரவரிசை ஓமன் vs நெதர்லாந்து ஓமன் தேசிய கிரிக்கெட் அணி vs நெதர்லாந்து தேசிய கிரிக்கெட் அணி போட்டி ஸ்கோர்கார்டு ஐசிசி (ICC) சிடபிள்யூசி (CWC) லீக் 2 ஓமன் தேசிய கிரிக்கெட் அணி vs நெதர்லாந்து தேசிய கிரிக்கெட் அணி தரவரிசை ஓமன் தேசிய கிரிக்கெட் அணி vs நெதர்லாந்து தேசிய கிரிக்கெட் அணி போட்டி ஸ்கோர்கார்டு நவம்பர் 2 - தங்கம் மற்றும் வெள்ளி விலை நிலவரம் வெள்ளி மற்றும் தங்கத்தின் விலைகள் தங்கம் குறித்த செய்திகள் இன்றைய தங்க விலை பெங்களூருவில் இன்றைய தங்க விலை ஹைதராபாத்தில் இன்றைய தங்க விலை கொல்கத்தாவில் இன்றைய தங்க விலை சென்னையில் இன்றைய தங்க விலை இன்றைய தங்கம் மற்றும் வெள்ளி விலை பெங்களூருவில் இன்றைய தங்க விலை சென்னையில் இன்றைய தங்க விலை இன்றைய தங்க விலை தனிஷ்க் (Tanishq) தங்க விலை வெள்ளி - எம்சிஎக்ஸ் (MCX) நேரலை விலை பெங்களூருவில் இன்றைய வெள்ளி விலை கோயம்புத்தூரில் இன்றைய தங்க விலை இன்றைய தங்கம் விலை தங்கத்தின் விலைகள் சென்னையில் இன்றைய தங்க விலை இன்றைய தங்க விலை இன்றைய வெள்ளி விலை MCX தங்கம் MCX தங்க விலை விலைமதிப்பற்ற உலோகம் மும்பையில் இன்றைய தங்க விலை ஹைதராபாத்தில் இன்றைய தங்க விலை டெல்லியில் இன்றைய தங்க விலை ஹைதராபாத்தில் இன்றைய தங்க விலை MCX தங்க விலை சென்னையில் தங்க விலை பெங்களூருவில் இன்றைய தங்க விலை MCX வெள்ளி விலை MCX மலபார் தங்க விலை இன்றைய MCX தங்க விலை இன்றைய 22 காரட் தங்க விலை இன்றைய 22K தங்க விலை நேரலை தங்க விலை இன்றைய 18 காரட் தங்க விலை கிமி அன்டோனெல்லி கிமி F1 தரவரிசை F1 F1-ன் மிக இளம் சாம்பியன் கிமி அன்டோனெல்லி ஃபார்முலா 1 F1 ஓட்டுநர் தரவரிசை F1 பந்தயம் F1 நேரலை F1 மெக்சிகோ கிராண்ட் பிரிக்ஸ் F1 2026 மேக்ஸ் வெர்ஸ்டாப்பன் F1 முடிவுகள் மெக்சிகோ கிராண்ட் பிரிக்ஸ் F1 மெக்சிகோ GP ரிலையன்ஸ் பங்கு விலை ரிலையன்ஸ் பங்கு ரிலையன்ஸ் RIL பங்கு விலை ரிலையன்ஸ் இண்டஸ்ட்ரீஸ் பங்கு விலை RIL பங்கு RIL பங்கு விலை உயர்வு குறித்த பகுப்பாய்வு ரிலையன்ஸ் இண்டஸ்ட்ரீஸ் பங்கு அனுஜ் தாக்கூர் நடிகர் அனுஜ் தாக்கூர் வலிப்பு (seizure) தூக்கம் விமானம் விபத்து புளோரிடா விமான விபத்து லியர்ஜெட் (Learjet) லியர்ஜெட் 60 வீடியோக்கள் புளோரிடா ஆஸ்டின் விபத்து இன்றைய செய்திகள் ரிமி டோமி லிபின் மேத்யூ - ரிமி டோமி ரிமி டோமி திருமணம் திருமணம் லிபின் மேத்யூ ரிமி டோமி டேலைட் சேவிங்ஸ் (Daylight Savings) 2026 டேலைட் சேவிங்ஸ் அமெரிக்காவில் டேலைட் சேவிங்ஸ் 2026 அமெரிக்காவில் டேலைட் சேவிங்ஸ் 2026 டேலைட் சேவிங்ஸ் அமெரிக்காவில் டேலைட் சேவிங்ஸ் 2026 ஷாருக்கான் பிறந்தநாள் ஷாருக்கான் ஷாருக்கான் பிறந்தநாள் கிங் (King) SRK பிறந்த தேதி சித்தார்த் ஆனந்த் SRK வயது ஷாருக் இந்தித் திரைப்படம் கேரள லாட்டரி முடிவுகள் கேரள லாட்டரி முடிவு இன்றைய கேரள லாட்டரி முடிவு இன்றைய பாக்யதாரா லாட்டரி முடிவு இன்றைய லாட்டரி முடிவு இன்றைய கேரள லாட்டரி முடிவு (நேரலை) இன்றைய கேரள லாட்டரி முடிவு இன்றைய கேரள லாட்டரி முடிவு (மதியம் 3 மணி) லாட்டரி இன்றைய முடிவு கேரள லாட்டரி முடிவு - இன்று பிற்பகல் 3 மணி (நேரலை) கேரள லாட்டரி முடிவு - நேற்று サイクロン「ムラス(Murasu)」 チェンナイの学校、明日は休校か サイクロン「ムラス」最新情報 チェンナイの学校は明日休みか? 天気 チェンナイは明日休みか? サイクロン「ムラス」の気象情報 チェンナイのサイクロン サイクロン「ムラス」追跡 チェンナイの天気 テランガーナ州 チェンナイの天気 サイクロン追跡 チェンガルパットゥの学校は今日休校か チェンナイの学校休校情報 チェンガルパットゥの学校休校情報 ビジャヤワーダの天気 天気予報 サイクロン「ムラス」追跡 今日の天気予報 チェンナイの学校、明日は休校 チェンナイの天気 チェンナイの学校は今日休みか? 今日のチェンナイの天気 フライト情報 タミル語 パフタコール対アル・ナスル パフタコールFC対アル・ナスル:順位表 AFCチャンピオンズリーグ パフタコールFC対アル・ナスル:出場メンバー ジョアン・フェリックス アル・ナスルFC パフタコールFC ニューイングランド対インテル・マイアミ ニューイングランド・レボリューション対インテル・マイアミ:順位表 MLS(メジャーリーグサッカー) インテル・マイアミ リオネル・メッシ インテル・マイアミCF インテル・マイアミ対ニューイングランド ニューイングランド・レボリューション対インテル・マイアミ:出場メンバー メッシの通算ゴール数 ニューイングランド・レボリューション対インテル・マイアミ:試合経過 インテル・マイアミの試合 ニューイングランド・レボリューション ニューイングランド・レボリューション対インテル・マイアミ オマーン対オランダ オマーン代表クリケットチーム対オランダ代表クリケットチーム:順位表 オマーン対オランダ オマーン代表クリケットチーム対オランダ代表クリケットチーム:試合スコアカード ICCクリケット・ワールドカップ・リーグ2 オマーン代表クリケットチーム対オランダ代表クリケットチーム:順位 オマーン代表クリケットチーム対オランダ代表クリケットチーム:試合スコアカード 11月2日の金・銀価格 銀・金価格 金に関するニュース 今日の金価格 バンガロールの今日の金価格 ハイデラバードの今日の金価格 コルカタの今日の金価格 チェンナイの今日の金価格 今日の金・銀価格 バンガロールの今日の金価格 チェンナイの今日の金価格 今日の金価格 タニシュク(Tanishq)の金価格 銀のMCX(インド商品取引所)ライブ価格 バンガロールの今日の銀価格 コインバトールの今日の金価格 今日の金価格価格 金価格 チェンナイの今日の金価格 今日の金価格 今日の銀価格 MCX(インド商品取引所)金価格 MCX金価格 貴金属 ムンバイの今日の金価格 ハイデラバードの今日の金価格 デリーの今日の金価格 ハイデラバードの今日の金価格 MCX金価格 チェンナイの金価格 バンガロールの今日の金価格 MCX銀価格 MCX マラバール・ゴールド(Malabar Gold)の金価格 今日のMCX金価格 今日の22金(22カラット)価格 今日の22金価格 金価格(リアルタイム) 今日の18金(18カラット)価格 キミ・アントネッリ キミ F1順位表 F1 キミ・アントネッリ(最年少F1チャンピオン候補) フォーミュラ1 F1ドライバー順位表 F1レース F1ライブ F1メキシコグランプリ F1 2026年シーズン マックス・フェルスタッペン F1結果 メキシコグランプリ F1メキシコGP リライアンス(Reliance)の株価 リライアンス株 リライアンス RIL株価 リライアンス・インダストリーズの株価 RIL株 RIL株価急騰の分析 リライアンス・インダストリーズ株 アヌジ・タクール 俳優アヌジ・タクール 発作 睡眠 飛行機墜落事故 フロリダでの飛行機墜落事故 リアジェット リアジェット60 動画 フロリダ オースティンでの事故 今日のニュース リミ・トミー リビン・マシュー、リミ・トミー リミ・トミーの結婚 結婚 リビン・マシュー リミ・トミー 2026年のサマータイム(デイライト・セービング) サマータイム 2026年米国のサマータイム 2026年米国サマータイム サマータイム 2026年米国サマータイム シャー・ルク・カーンの誕生日 シャー・ルク・カーン シャー・ルク・カーンの誕生日 キング SRK(シャー・ルク・カーン)の誕生日 シッダールト・アーナンド SRKの年齢 シャー・ルク ヒンディー語映画 ケーララ州宝くじの結果 ケーララ州宝くじ結果 今日のケーララ州宝くじ結果 今日の「バギャターラ(Bhagyathara)」宝くじ結果 今日の宝くじ結果 今日のケーララ州宝くじ結果(ライブ) 今日のケーララ州宝くじ結果 今日のケーララ州宝くじ結果(午後3時) 宝くじ本日の結果 ケーララ宝くじ 本日の結果(午後3時ライブ) ケーララ宝くじ 昨日の結果 তীব্র ঘূর্ণিঝড় 'মুরাসু' চেন্নাইতে আগামীকাল স্কুল ছুটি ঘূর্ণিঝড় 'মুরাসু' সংক্রান্ত আপডেট চেন্নাইতে আগামীকাল স্কুল ছুটি কি না আবহাওয়া চেন্নাইতে কি আগামীকাল ছুটি? ঘূর্ণিঝড় 'মুরাসু' ও আবহাওয়া চেন্নাইতে ঘূর্ণিঝড় ঘূর্ণিঝড় 'মুরাসু' ট্র্যাকার চেন্নাইয়ের আবহাওয়া তেলেঙ্গানা চেন্নাইয়ের আবহাওয়া ঘূর্ণিঝড় ট্র্যাকার চেঙ্গলপাট্টুতে আজ স্কুল ছুটি চেন্নাইতে স্কুল ছুটি চেঙ্গলপাট্টুতে স্কুল ছুটি বিজয়ওয়াড়ার আবহাওয়া আবহাওয়া প্রতিবেদন ঘূর্ণিঝড় 'মুরাসু' ট্র্যাকার আজকের আবহাওয়া প্রতিবেদন চেন্নাইতে আগামীকাল স্কুল ছুটি চেন্নাইয়ের আবহাওয়া চেন্নাইতে আজ স্কুল ছুটি কি না আজকের চেন্নাইয়ের আবহাওয়া বিমান চলাচল তামিল পাখতাকোর বনাম আল-নাসর পাখতাকোর এফসি বনাম আল-নাসর পয়েন্ট টেবিল এএফসি চ্যাম্পিয়ন্স লিগ পাখতাকোর এফসি বনাম আল-নাসর লাইনআপ জোয়াও ফেলিক্স আল-নাসর এফসি পাখতাকোর এফসি নিউ ইংল্যান্ড বনাম ইন্টার মায়ামি নিউ ইংল্যান্ড রেভোলিউশন বনাম ইন্টার মায়ামি পয়েন্ট টেবিল এমএলএস (MLS) ইন্টার মায়ামি লিওনেল মেসি ইন্টার মায়ামি সিএফ ইন্টার মায়ামি বনাম নিউ ইংল্যান্ড নিউ ইংল্যান্ড রেভোলিউশন বনাম ইন্টার মায়ামি লাইনআপ মেসির মোট গোল নিউ ইংল্যান্ড রেভোলিউশন বনাম ইন্টার মায়ামি টাইমলাইন ইন্টার মায়ামির ম্যাচসমূহ নিউ ইংল্যান্ড রেভোলিউশন নিউ ইংল্যান্ড রেভোলিউশন বনাম ইন্টার মায়ামি ওমান বনাম নেদারল্যান্ডস ওমান জাতীয় ক্রিকেট দল বনাম নেদারল্যান্ডস জাতীয় ক্রিকেট দল পয়েন্ট টেবিল ওমান বনাম নেদারল্যান্ডস ওমান জাতীয় ক্রিকেট দল বনাম নেদারল্যান্ডস জাতীয় ক্রিকেট দল ম্যাচ স্কোরকার্ড আইসিসি সিডব্লিউসি (CWC) লিগ ২ ওমান জাতীয় ক্রিকেট দল বনাম নেদারল্যান্ডস ক্রিকেট দল র‍্যাঙ্কিং ওমান জাতীয় ক্রিকেট দল বনাম নেদারল্যান্ডস ক্রিকেট দল ম্যাচ স্কোরকার্ড ২রা নভেম্বর সোনা ও রুপার দর সোনা ও রুপার দাম সোনা বিষয়ক খবর আজকের সোনার দর ব্যাঙ্গালোরে আজকের সোনার দর হায়দ্রাবাদে আজকের সোনার দর কলকাতায় আজকের সোনার দর চেন্নাইতে আজকের সোনার দর আজ সোনা ও রুপার দর ব্যাঙ্গালোরে আজকের সোনার দর চেন্নাইতে আজকের সোনার দর আজকের সোনার দর