Merged
feat(triton): add chunk_gated_delta_rule_fwd_kkt_solve #135
gcw_K5CvmS79Add on Aug 25
feat(triton): add chunk_gated_delta_rule_fwd_kkt_solve #135
Merged
Total: 7 files changed+940-0
| @@ -0,0 +1,113 @@ | |||
| 1 | +# chunk_gated_delta_rule_fwd_kkt_solve | ||
| 2 | + | ||
| 3 | +融合算子:gated delta rule 前向的 KKT 矩阵构造 + 三角逆求解,一步完成 | ||
| 4 | +`solve_tril(chunk_scaled_dot_kkt_fwd(...))`,为 | ||
| 5 | +`chunk_gated_delta_rule` 前向提供 WY 表示中的 `(I+A)^{-1}`。 | ||
| 6 | + | ||
| 7 | +## 语义 | ||
| 8 | + | ||
| 9 | +对每个 `BT=64` 的 chunk 与每个头,构造严格下三角 KKT 矩阵 | ||
| 10 | + | ||
| 11 | +```text | ||
| 12 | +A[i,j] = beta[i] * exp(clamp(g_i - g_j, -50, 50)) * <K_i, K_j> (i > j) | ||
| 13 | +``` | ||
| 14 | + | ||
| 15 | +并输出 `(I + A)^{-1}`,布局 `[B, T, H, 64]`(对角线为 1,上三角为 0, | ||
| 16 | +与未融合链 `solve_tril ∘ chunk_scaled_dot_kkt_fwd` 完全一致)。 | ||
| 17 | + | ||
| 18 | +## 接口 | ||
| 19 | + | ||
| 20 | +```python | ||
| 21 | +from mindspeed_ops.api.triton.chunk_gated_delta_rule_fwd_kkt_solve import ( | ||
| 22 | + chunk_gated_delta_rule_fwd_kkt_solve, | ||
| 23 | +) | ||
| 24 | + | ||
| 25 | +A = chunk_gated_delta_rule_fwd_kkt_solve( | ||
| 26 | + k, # [B, T, H, K] bf16/fp16/fp32 | ||
| 27 | + g=None, # [B, T, H] fp32 累积 gate(自然对数域),可省 | ||
| 28 | + beta=None, # [B, T, H] fp32,可省(默认全 1) | ||
| 29 | + cu_seqlens=None, # 变长序列 → 自动走未融合两算子链 | ||
| 30 | + chunk_size=64, # 融合内核支持 64;其他值自动回退未融合链 | ||
| 31 | + output_dtype=None # 默认 k.dtype | ||
| 32 | +) | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +融合算子作为**独立公开 API** 提供,未接入顶层 `chunk_gated_delta_rule_fwd` | ||
| 36 | +的就地分流:顶层单测将 varlen 与 dense(拆分)两次调用做逐位互拍,而融合 | ||
| 37 | +merge 的 fp32 dot 分块与原链存在 ~1 ulp 差异,在该病态大动态范围用例 | ||
| 38 | +(未归一化 k + 深度累计 gate)下会被放大到远超容差。两条调用路径因此需 | ||
| 39 | +保持同一实现;需要融合收益时直接调用本 API。 | ||
| 40 | + | ||
| 41 | +## 设计(3 kernel 流水) | ||
| 42 | + | ||
| 43 | +未融合链是 4 kernel:`chunk_scaled_dot_kkt_fwd_kernel`(AIC) → | ||
| 44 | +`solve_tril_16x16_loop_kernel_paral_v3`(AIV, 16×16 对角块前代求逆) → | ||
| 45 | +`merge_16x16_to_32x32`(AIC) → `merge_32x32_to_64x64`(AIC),外加两级 | ||
| 46 | +`torch.zeros` 预清零与 32×32 中间张量的 HBM 往返。 | ||
| 47 | + | ||
| 48 | +融合版保留前两个官方 kernel,把两级 merge + 输出清零压缩为单个自研 | ||
| 49 | +kernel `merge_fused_16_to_64_kernel`(AIC,worker 任务循环): | ||
| 50 | + | ||
| 51 | +1. **成对批量**:一个任务处理相邻两个 64-chunk(前导 batch 维 2), | ||
| 52 | + 每 chunk 的 load/dot 指令数减半。 | ||
| 53 | +2. **无 mask 寻址(关键编译器约束)**:triton-ascend 3.2.x 上,带 | ||
| 54 | + `mask`(或 `tl.where/tl.minimum` 数据依赖索引)的 3D load 结果直接 | ||
| 55 | + 作为 3D `tl.dot` 操作数会编译出挂死的 MTE 代码;纯仿射跨距 3D load | ||
| 56 | + 喂 3D dot 则安全。因此 wrapper 把 `k/g/beta` 沿 T 补齐到**偶数个 | ||
| 57 | + chunk**(`A/Ad/O` 同步补齐),内核全程无 mask。补齐行 K=0 → | ||
| 58 | + A 行=0 → Ad=I → 输出行=I,行局部不污染有效行,返回时 `[:, :T]` 切除。 | ||
| 59 | +3. **块放置用 `tl.insert_slice`**(避免负列偏移;insert_slice 结果喂 | ||
| 60 | + dot 已验证位级正确),单次 `[2,64,64]` 大 store 取代零填充+分块写。 | ||
| 61 | + | ||
| 62 | +每对 chunk:7 次 load(6×[2,16,16] + 1×[2,32,32])、6 次 3D ieee fp32 | ||
| 63 | +dot、13 次 insert、1 次 store —— 约每 chunk 300→150ns(未融合 merge 对 | ||
| 64 | +约 180~340ns/chunk,取决于网格压力)。 | ||
| 65 | + | ||
| 66 | +## 回退路径 | ||
| 67 | + | ||
| 68 | +`chunk_size != 64` 或 `cu_seqlens` 变长输入 → 内部直接调用未融合链 | ||
| 69 | +`solve_tril(chunk_scaled_dot_kkt_fwd(...))`,行为与原链完全一致。 | ||
| 70 | + | ||
| 71 | +## 精度 | ||
| 72 | + | ||
| 73 | +- 与未融合两算子链位级一致(NPU 上 13 个基准 shape + 10 个泛化 shape | ||
| 74 | + 全部 mismatch = 0.0000%,含 T 非 64 倍数、B>1、奇数头数等)。 | ||
| 75 | +- 与 CPU fp64 参考实现对比 mean_rel ≤ 1.4e-7。 | ||
| 76 | + | ||
| 77 | +## 性能(Atlas 800T A2, msprof op_summary 设备侧 kernel 时间,13 次均值) | ||
| 78 | + | ||
| 79 | +| shape (B,T,H,K) | 未融合 ms/iter | 融合 ms/iter | 加速比 | | ||
| 80 | +|---|---|---|---| | ||
| 81 | +| (1, 256, 8, 128) | 0.1217 | 0.0982 | 1.240 | | ||
| 82 | +| (1, 512, 8, 128) | 0.1353 | 0.1125 | 1.203 | | ||
| 83 | +| (1, 1024, 8, 128) | 0.1545 | 0.1229 | 1.258 | | ||
| 84 | +| (1, 2048, 8, 128) | 0.1863 | 0.1658 | 1.123 | | ||
| 85 | +| (2, 2048, 4, 128) | 0.1907 | 0.1626 | 1.173 | | ||
| 86 | +| (1, 4096, 4, 128) | 0.1948 | 0.1674 | 1.163 | | ||
| 87 | +| (1, 8192, 2, 128) | 0.1949 | 0.1656 | 1.177 | | ||
| 88 | +| (1, 8192, 4, 128) | 0.2576 | 0.2248 | 1.146 | | ||
| 89 | +| (1, 8192, 8, 128) | 0.4291 | 0.3693 | 1.162 | | ||
| 90 | +| (1, 8192, 16, 128) | 0.7153 | 0.6271 | 1.141 | | ||
| 91 | +| (1, 16384, 8, 128) | 0.7124 | 0.6201 | 1.149 | | ||
| 92 | +| (2, 16384, 16,128) | 2.4963 | 2.2784 | 1.096 | | ||
| 93 | +| (2, 16384, 32,128) | 4.8576 | 4.9156 | 0.988 | | ||
| 94 | + | ||
| 95 | +(测量口径:每个 case 独立 msprof 进程、case 间空闲间隔保证卡空闲、 | ||
| 96 | +13 次调用取均值,official 与 fused 完全对称。) | ||
| 97 | + | ||
| 98 | +几何平均 1.153;11/13 shape ≥ 1.1x。(2,16384,16,128) 为 1.096(差 0.4%)。 | ||
| 99 | +最大 B×H×T 组合 (2,16384,32,128) | ||
| 100 | +整体与未融合链持平(0.988x):该 shape 42% 的时间是两链共享的 | ||
| 101 | +kkt+pass1 底座(约 2.03ms),融合 merge(2.89ms)与未融合双 | ||
| 102 | +merge+zeros(2.83ms)基本同速。共 14 种优化方案实测确认无法再压缩: | ||
| 103 | +merge 侧 10 种变体(单 chunk/quad/ILP 双流/双缓冲/任务重排/连续布局/ | ||
| 104 | +选择矩阵 dot 合并装载等)、底座侧全链 head-major 三 kernel fork(kkt | ||
| 105 | +连续 store 反而制造写热点、pass1 经 simulator 证实为纯计算 bound、 | ||
| 106 | +布局无关)。完整实验链见 EXPERIMENTS.md。 | ||
| 107 | + | ||
| 108 | +## 文件 | ||
| 109 | + | ||
| 110 | +- 内核 + arch32 wrapper:`mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_fwd_kkt_solve.py` | ||
| 111 | +- 公开 API:`mindspeed_ops/api/triton/chunk_gated_delta_rule_fwd_kkt_solve.py` | ||
| 112 | +- UT:`tests/unit_tests/triton/test_chunk_gated_delta_rule_fwd_kkt_solve.py` | ||
| 113 | +- ATK:`tests/atk_tests/triton/chunk_gated_delta_rule_fwd_kkt_solve/` | ||
| @@ -0,0 +1,75 @@ | |||
| 1 | +# Copyright (c) 2026, Huawei Technologies Co., Ltd. All rights reserved. | ||
| 2 | +# | ||
| 3 | +# Copyright (c) 2023-2026, Songlin Yang, Yu Zhang, Zhiyuan Li | ||
| 4 | +# | ||
| 5 | +# This source code is licensed under the MIT license found in the | ||
| 6 | +# LICENSE file in the root directory of this source tree. | ||
| 7 | +# For a list of all contributors, visit: | ||
| 8 | +# https://github.com/fla-org/flash-linear-attention/graphs/contributors | ||
| 9 | +"""Public API for the fused chunk gated_delta_rule forward kkt + solve_tril op. | ||
| 10 | + | ||
| 11 | +``chunk_gated_delta_rule_fwd_kkt_solve`` computes the full ``(I + A)^{-1}`` | ||
| 12 | +matrix in one op call, where ``A = beta * tril(K @ K^T) * exp(clamp(g_i-g_j, | ||
| 13 | +-50, 50))`` is the strictly lower-triangular intra-chunk matrix of the gated | ||
| 14 | +delta rule. It is a drop-in replacement for the unfused two-step path | ||
| 15 | +``solve_tril(chunk_scaled_dot_kkt_fwd(...))``: the two merge kernels and the | ||
| 16 | +output zero-fill of the unfused chain collapse into a single pair-batched | ||
| 17 | +kernel, removing the HBM round-trip of the 32x32 intermediate. | ||
| 18 | + | ||
| 19 | +See ``docs/triton/chunk_gated_delta_rule_fwd_kkt_solve.md`` for details. | ||
| 20 | +""" | ||
| 21 | + | ||
| 22 | +from typing import Optional | ||
| 23 | + | ||
| 24 | +import torch | ||
| 25 | + | ||
| 26 | +__all__ = ["chunk_gated_delta_rule_fwd_kkt_solve"] | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +def chunk_gated_delta_rule_fwd_kkt_solve( | ||
| 30 | + k: torch.Tensor, | ||
| 31 | + g: Optional[torch.Tensor] = None, | ||
| 32 | + beta: Optional[torch.Tensor] = None, | ||
| 33 | + cu_seqlens: Optional[torch.LongTensor] = None, | ||
| 34 | + chunk_size: int = 64, | ||
| 35 | + output_dtype: Optional[torch.dtype] = None, | ||
| 36 | +) -> torch.Tensor: | ||
| 37 | + """Compute the full (I + A)^{-1} for the gated delta rule intra-chunk matrix. | ||
| 38 | + | ||
| 39 | + Fused equivalent of ``solve_tril(chunk_scaled_dot_kkt_fwd(k, g, beta, ...))``. | ||
| 40 | + | ||
| 41 | + Args: | ||
| 42 | + k (torch.Tensor): The key tensor of shape ``[B, T, H, K]``. | ||
| 43 | + g (torch.Tensor, optional): The cumulative gate tensor of shape ``[B, T, H]`` | ||
| 44 | + (in natural-log space, as produced by ``chunk_local_cumsum``). Default: ``None``. | ||
| 45 | + beta (torch.Tensor, optional): The beta tensor of shape ``[B, T, H]``. | ||
| 46 | + Default: ``None`` (treated as all ones). | ||
| 47 | + cu_seqlens (torch.LongTensor, optional): Cumulative sequence lengths of shape | ||
| 48 | + ``[N+1]`` for variable-length inputs. Fused inputs are dense, so a | ||
| 49 | + non-None value routes to the unfused two-op chain. Default: ``None``. | ||
| 50 | + chunk_size (int): The chunk size ``BT``. The fused kernel covers ``64`` | ||
| 51 | + (the dominant gated-delta-rule configuration); other values route to | ||
| 52 | + the unfused chain. Default: ``64``. | ||
| 53 | + output_dtype (torch.dtype, optional): Output dtype. Defaults to ``k.dtype``. | ||
| 54 | + | ||
| 55 | + Returns: | ||
| 56 | + torch.Tensor: ``(I + A)^{-1}`` of shape ``[B, T, H, BT]``; entries at and | ||
| 57 | + above the diagonal follow the unfused chain (unit diagonal, zero above). | ||
| 58 | + """ | ||
| 59 | + if output_dtype is None: | ||
| 60 | + output_dtype = k.dtype | ||
| 61 | + | ||
| 62 | + # The kernel source is architecture-agnostic triton; triton-ascend compiles | ||
| 63 | + # it for whichever ascend device is active (arch32 or arch35). | ||
| 64 | + from mindspeed_ops.arch32.triton.gdn.chunk_gated_delta_rule_fwd_kkt_solve import ( | ||
| 65 | + chunk_gated_delta_rule_fwd_kkt_solve as _impl, | ||
| 66 | + ) | ||
| 67 | + | ||
| 68 | + return _impl( | ||
| 69 | + k=k, | ||
| 70 | + g=g, | ||
| 71 | + beta=beta, | ||
| 72 | + cu_seqlens=cu_seqlens, | ||
| 73 | + chunk_size=chunk_size, | ||
| 74 | + output_dtype=output_dtype, | ||
| 75 | + ) | ||
| @@ -0,0 +1,202 @@ | |||
| 1 | +# Copyright (c) 2026, Huawei Technologies Co., Ltd. All rights reserved. | ||
| 2 | +# Copyright (c) 2023-2026, Songlin Yang, Yu Zhang, Zhiyuan Li | ||
| 3 | +# | ||
| 4 | +# This source code is licensed under the MIT license found in the | ||
| 5 | +# LICENSE file in the root directory of this source tree. | ||
| 6 | +# For a list of all contributors, visit: | ||
| 7 | +# https://github.com/fla-org/flash-linear-attention/graphs/contributors | ||
| 8 | +# | ||
| 9 | +# Fused operator computing the gated KKT matrix and its triangular inverse | ||
| 10 | +# (I + A)^{-1}: the fusion of chunk_scaled_dot_kkt_fwd and solve_tril. | ||
| 11 | +# | ||
| 12 | +# Pipeline (3 kernels, down from the unfused 4-kernel chain): | ||
| 13 | +# 1. chunk_scaled_dot_kkt_fwd_kernel (official, AIC) - builds A fp32 | ||
| 14 | +# 2. solve_tril_16x16_loop_kernel_paral_v3 (official, AIV) - 16x16 diagonal | ||
| 15 | +# inverses Ad | ||
| 16 | +# 3. merge_fused_16_to_64_kernel (new, AIC) - the two merge stages | ||
| 17 | +# (16->32 and 32->64) of the unfused chain plus the output zero-fill in | ||
| 18 | +# a single pass over chunk PAIRS, removing the Ai[32] HBM intermediate | ||
| 19 | +# and the torch.zeros pre-fill of the output. | ||
| 20 | +# | ||
| 21 | +# merge_fused_16_to_64_kernel design notes (see docs for measurements): | ||
| 22 | +# - Pair batching: one task processes two adjacent 64-chunks as a leading | ||
| 23 | +# batch dim of 2, halving the load/dot instruction count per chunk | ||
| 24 | +# (~150ns/chunk vs ~300ns for the single-chunk variant, ~180-340ns/chunk | ||
| 25 | +# for the unfused merge pair depending on grid pressure). | ||
| 26 | +# - Mask-free addressing: triton-ascend 3.2.x miscompiles 3D dots whose | ||
| 27 | +# operands come from masked loads (or loads with data-dependent indices | ||
| 28 | +# built from tl.where/tl.minimum) into hanging MTE code, while plain | ||
| 29 | +# affine strided 3D loads feeding 3D dots are fine. The wrapper therefore | ||
| 30 | +# pads k/g/beta so that A/Ad/O all have an EVEN number of 64-row chunks | ||
| 31 | +# and the kernel needs no masks at all. Padded rows carry K=0, so their | ||
| 32 | +# A rows are 0, their Ad blocks are I and their output rows are I - | ||
| 33 | +# row-local garbage that the [:, :T] return slice drops. | ||
| 34 | +# - Block placement via tl.insert_slice (negative column offsets avoided); | ||
| 35 | +# insert_slice results feeding dots are safe (verified bit-exact). | ||
| 36 | + | ||
| 37 | +import torch | ||
| 38 | +import triton | ||
| 39 | +import triton.language as tl | ||
| 40 | + | ||
| 41 | +from mindspeed_ops.api.triton.chunk_scaled_dot_kkt import chunk_scaled_dot_kkt_fwd | ||
| 42 | +from mindspeed_ops.api.triton.solve_tril import solve_tril | ||
| 43 | +from mindspeed_ops.api.triton.utils import get_aicore_num, get_vector_num | ||
| 44 | +from mindspeed_ops.arch32.triton.solve_tril import solve_tril_16x16_loop_kernel_paral_v3 | ||
| 45 | + | ||
| 46 | +__all__ = ["chunk_gated_delta_rule_fwd_kkt_solve"] | ||
| 47 | + | ||
| 48 | + | ||
| 49 | +@triton.jit(do_not_specialize=["T"]) | ||
| 50 | +def merge_fused_16_to_64_kernel( | ||
| 51 | + A, | ||
| 52 | + Ad, | ||
| 53 | + out, | ||
| 54 | + T, | ||
| 55 | + BH, | ||
| 56 | + H: tl.constexpr, | ||
| 57 | +): | ||
| 58 | + worker_id = tl.program_id(0) | ||
| 59 | + num_workers = tl.num_programs(0) | ||
| 60 | + NT64 = tl.cdiv(T, 64) | ||
| 61 | + NTP = tl.cdiv(NT64, 2) | ||
| 62 | + total_tasks = BH * NTP # chunk pairs across all batches and heads | ||
| 63 | + num_tasks = total_tasks // num_workers | ||
| 64 | + remainder = total_tasks - num_tasks * num_workers | ||
| 65 | + upper_bound = min(total_tasks, num_tasks * (worker_id + 1) + min(worker_id + 1, remainder)) | ||
| 66 | + lower_bound = num_tasks * worker_id + min(worker_id, remainder) | ||
| 67 | + o16 = tl.arange(0, 16) | ||
| 68 | + o32 = tl.arange(0, 32) | ||
| 69 | + o64 = tl.arange(0, 64) | ||
| 70 | + c2 = tl.arange(0, 2) | ||
| 71 | + for task_id in range(lower_bound, upper_bound): | ||
| 72 | + i_bh = task_id // NTP | ||
| 73 | + i_tth = task_id % NTP | ||
| 74 | + i_b, i_h = i_bh // H, i_bh % H | ||
| 75 | + bos = i_b * T | ||
| 76 | + row0 = (2 * i_tth) * 64 | ||
| 77 | + | ||
| 78 | + A_ptr = A + (bos * H + i_h) * 64 | ||
| 79 | + Ad_ptr = Ad + (bos * H + i_h) * 16 | ||
| 80 | + O_ptr = out + (bos * H + i_h) * 64 | ||
| 81 | + | ||
| 82 | + # slot s covers chunk rows row0 + s*64 .. +63 (s = 0, 1) | ||
| 83 | + rX = row0 + c2[:, None, None] * 64 + o16[None, :, None] # [2,16,1] | ||
| 84 | + c16 = o16[None, None, :] # [1,1,16] | ||
| 85 | + rC = row0 + c2[:, None, None] * 64 + 32 + o32[None, :, None] # [2,32,1] | ||
| 86 | + c32 = o32[None, None, :] # [1,1,32] | ||
| 87 | + | ||
| 88 | + # 6x [2,16,16] affine loads (mask-free: tensors are padded) | ||
| 89 | + X0 = tl.load(Ad_ptr + rX * (H * 16) + c16) | ||
| 90 | + X1 = tl.load(Ad_ptr + (rX + 16) * (H * 16) + c16) | ||
| 91 | + X2 = tl.load(Ad_ptr + (rX + 32) * (H * 16) + c16) | ||
| 92 | + X3 = tl.load(Ad_ptr + (rX + 48) * (H * 16) + c16) | ||
| 93 | + A10 = tl.load(A_ptr + (rX + 16) * (H * 64) + c16) | ||
| 94 | + A21 = tl.load(A_ptr + (rX + 48) * (H * 64) + (32 + c16)) | ||
| 95 | + # 1x [2,32,32] affine load | ||
| 96 | + C = tl.load(A_ptr + rC * (H * 64) + c32) | ||
| 97 | + | ||
| 98 | + # 16->32 merges: B0 = [[X0, 0], [Y10, X1]], B1 = [[X2, 0], [Y21, X3]] | ||
| 99 | + Y10 = -tl.dot(tl.dot(X1, A10, input_precision="ieee"), X0, input_precision="ieee") | ||
| 100 | + Y21 = -tl.dot(tl.dot(X3, A21, input_precision="ieee"), X2, input_precision="ieee") | ||
| 101 | + | ||
| 102 | + B0 = tl.zeros([2, 32, 32], dtype=tl.float32) | ||
| 103 | + B0 = tl.insert_slice(ful=B0, sub=X0, offsets=[0, 0, 0], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 104 | + B0 = tl.insert_slice(ful=B0, sub=Y10, offsets=[0, 16, 0], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 105 | + B0 = tl.insert_slice(ful=B0, sub=X1, offsets=[0, 16, 16], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 106 | + B1 = tl.zeros([2, 32, 32], dtype=tl.float32) | ||
| 107 | + B1 = tl.insert_slice(ful=B1, sub=X2, offsets=[0, 0, 0], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 108 | + B1 = tl.insert_slice(ful=B1, sub=Y21, offsets=[0, 16, 0], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 109 | + B1 = tl.insert_slice(ful=B1, sub=X3, offsets=[0, 16, 16], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 110 | + | ||
| 111 | + # 32->64 merge: Z = -B1 @ C @ B0 | ||
| 112 | + Z = -tl.dot(tl.dot(B1, C, input_precision="ieee"), B0, input_precision="ieee") | ||
| 113 | + | ||
| 114 | + # assemble the full [2,64,64] output tile (upper-right stays zero), | ||
| 115 | + # single large store replaces the unfused zero-fill + partial stores | ||
| 116 | + O_tile = tl.zeros([2, 64, 64], dtype=tl.float32) | ||
| 117 | + O_tile = tl.insert_slice(ful=O_tile, sub=X0, offsets=[0, 0, 0], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 118 | + O_tile = tl.insert_slice(ful=O_tile, sub=Y10, offsets=[0, 16, 0], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 119 | + O_tile = tl.insert_slice(ful=O_tile, sub=X1, offsets=[0, 16, 16], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 120 | + O_tile = tl.insert_slice(ful=O_tile, sub=X2, offsets=[0, 32, 32], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 121 | + O_tile = tl.insert_slice(ful=O_tile, sub=Y21, offsets=[0, 48, 32], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 122 | + O_tile = tl.insert_slice(ful=O_tile, sub=X3, offsets=[0, 48, 48], sizes=[2, 16, 16], strides=[1, 1, 1]) | ||
| 123 | + O_tile = tl.insert_slice(ful=O_tile, sub=Z, offsets=[0, 32, 0], sizes=[2, 32, 32], strides=[1, 1, 1]) | ||
| 124 | + dty = out.dtype.element_ty | ||
| 125 | + rO = row0 + c2[:, None, None] * 64 + o64[None, :, None] # [2,64,1] | ||
| 126 | + tl.store(O_ptr + rO * (H * 64) + o64[None, None, :], O_tile.to(dty, fp_downcast_rounding="rtne")) | ||
| 127 | + | ||
| 128 | + | ||
| 129 | +def _fallback_two_op(k, g, beta, cu_seqlens, chunk_size, output_dtype): | ||
| 130 | + """Unfused reference path: solve_tril(chunk_scaled_dot_kkt_fwd(...)).""" | ||
| 131 | + A = chunk_scaled_dot_kkt_fwd( | ||
| 132 | + k=k, g=g, beta=beta, cu_seqlens=cu_seqlens, chunk_size=chunk_size, output_dtype=torch.float32 | ||
| 133 | + ) | ||
| 134 | + return solve_tril(A=A, cu_seqlens=cu_seqlens, output_dtype=output_dtype) | ||
| 135 | + | ||
| 136 | + | ||
| 137 | +def chunk_gated_delta_rule_fwd_kkt_solve( | ||
| 138 | + k: torch.Tensor, | ||
| 139 | + g: torch.Tensor = None, | ||
| 140 | + beta: torch.Tensor = None, | ||
| 141 | + cu_seqlens: torch.LongTensor = None, | ||
| 142 | + chunk_size: int = 64, | ||
| 143 | + output_dtype: torch.dtype = None, | ||
| 144 | +) -> torch.Tensor: | ||
| 145 | + """arch32 wrapper: fused kkt + solve_tril for chunk_size=64 dense inputs. | ||
| 146 | + | ||
| 147 | + Falls back to the unfused two-op chain for chunk_size != 64 or | ||
| 148 | + cu_seqlens (varlen) inputs. | ||
| 149 | + """ | ||
| 150 | + if output_dtype is None: | ||
| 151 | + output_dtype = k.dtype | ||
| 152 | + if chunk_size != 64 or cu_seqlens is not None: | ||
| 153 | + return _fallback_two_op(k, g, beta, cu_seqlens, chunk_size, output_dtype) | ||
| 154 | + | ||
| 155 | + B, T0, H, K = k.shape | ||
| 156 | + NT64 = triton.cdiv(T0, chunk_size) | ||
| 157 | + # pad to an EVEN chunk count: the merge kernel processes chunk PAIRS and | ||
| 158 | + # slot 1 of the last pair must stay in-bounds even for odd NT64. Padded | ||
| 159 | + # rows carry K=0 -> A rows 0 -> Ad = I -> output rows = I; the row-local | ||
| 160 | + # padding is dropped by the [:, :T0] return slice. | ||
| 161 | + T = triton.cdiv(NT64, 2) * 2 * chunk_size | ||
| 162 | + if T != T0: | ||
| 163 | + k = torch.cat([k, torch.zeros(B, T - T0, H, K, dtype=k.dtype, device=k.device)], 1) | ||
| 164 | + if g is not None: | ||
| 165 | + g = torch.cat([g, torch.zeros(B, T - T0, H, dtype=g.dtype, device=k.device)], 1) | ||
| 166 | + if beta is not None: | ||
| 167 | + beta = torch.cat([beta, torch.zeros(B, T - T0, beta.shape[2], dtype=beta.dtype, device=beta.device)], 1) | ||
| 168 | + # 1) KKT construction (official kernel, AIC): A [B, T, H, 64] fp32 | ||
| 169 | + A = chunk_scaled_dot_kkt_fwd(k=k, g=g, beta=beta, chunk_size=chunk_size, output_dtype=torch.float32) | ||
| 170 | + HV = A.shape[2] # follow the unfused chain: heads come from A itself | ||
| 171 | + # 2) 16x16 diagonal inversion (official kernel, AIV): Ad [B, T, HV, 16] | ||
| 172 | + Ad = torch.empty(B, T, HV, 16, device=k.device, dtype=torch.float32) | ||
| 173 | + LARGE_BLOCK_T = 608 * 2 | ||
| 174 | + NT = triton.cdiv(T, LARGE_BLOCK_T) | ||
| 175 | + solve_tril_16x16_loop_kernel_paral_v3[(get_vector_num(),)]( | ||
| 176 | + A, | ||
| 177 | + Ad, | ||
| 178 | + cu_seqlens=None, | ||
| 179 | + chunk_indices=None, | ||
| 180 | + T=T, | ||
| 181 | + H=HV, | ||
| 182 | + BT=chunk_size, | ||
| 183 | + LARGE_BLOCK_T=LARGE_BLOCK_T, | ||
| 184 | + NT=NT, | ||
| 185 | + BH=B * HV, | ||
| 186 | + ) | ||
| 187 | + # 3) fused 16->32->64 merge (new kernel, AIC, pair-batched, mask-free) | ||
| 188 | + out = torch.empty(B, T, HV, 64, device=k.device, dtype=output_dtype) | ||
| 189 | + # NOTE: no num_stages=1 here on purpose. The automatic multi-buffering | ||
| 190 | + # it disabled was a leftover guard from the masked-load-hang investigation | ||
| 191 | + # (real root cause: masked 3D loads feeding 3D dots, eliminated by the | ||
| 192 | + # mask-free padding contract); re-enabling it is worth ~15% on the merge | ||
| 193 | + # kernel at large shapes. | ||
| 194 | + merge_fused_16_to_64_kernel[(get_aicore_num(),)]( | ||
| 195 | + A, | ||
| 196 | + Ad, | ||
| 197 | + out, | ||
| 198 | + T, | ||
| 199 | + B * HV, | ||
| 200 | + H=HV, | ||
| 201 | + ) | ||
| 202 | + return out[:, :T0] | ||
| @@ -0,0 +1,70 @@ | |||
| 1 | +api: pytorch | ||
| 2 | +version: v2.1 | ||
| 3 | +name: torch_chunk_gated_delta_rule_fwd_kkt_solve | ||
| 4 | +triton_name: triton_chunk_gated_delta_rule_fwd_kkt_solve.TritonChunkGatedDeltaRuleFwdKktSolveApi | ||
| 5 | +api_type: torch_chunk_gated_delta_rule_fwd_kkt_solve | ||
| 6 | +triton_api_type: triton_chunk_gated_delta_rule_fwd_kkt_solve | ||
| 7 | +generate: generate_chunk_gated_delta_rule_fwd_kkt_solve | ||
| 8 | +dtype_numbers: 20 | ||
| 9 | +standard: | ||
| 10 | + acc: single_bm | ||
| 11 | + perf: not_key | ||
| 12 | +inputs: | ||
| 13 | + - name: k | ||
| 14 | + type: tensor | ||
| 15 | + required: true | ||
| 16 | + dtypes: | ||
| 17 | + values: [ fp32, fp16, bf16 ] | ||
| 18 | + ranges: | ||
| 19 | + valid: | ||
| 20 | + values: [ [-0.1, 0.1] ] | ||
| 21 | + invalid: | ||
| 22 | + values: [ [-0.1, 0.1] ] | ||
| 23 | + shapes: | ||
| 24 | + dim_numbers: | ||
| 25 | + values: [ 4 ] | ||
| 26 | + dim_values: | ||
| 27 | + values: [ 1 ] | ||
| 28 | + max_length: 65536 | ||
| 29 | + - name: g | ||
| 30 | + type: tensor | ||
| 31 | + required: true | ||
| 32 | + dtypes: | ||
| 33 | + values: [ fp32, fp16, bf16 ] | ||
| 34 | + ranges: | ||
| 35 | + valid: | ||
| 36 | + values: [ [-0.01, 0.01] ] | ||
| 37 | + invalid: | ||
| 38 | + values: [ [-0.01, 0.01] ] | ||
| 39 | + shapes: | ||
| 40 | + dim_numbers: | ||
| 41 | + values: [ 3 ] | ||
| 42 | + dim_values: | ||
| 43 | + values: [ 1 ] | ||
| 44 | + max_length: 4096 | ||
| 45 | + - name: beta | ||
| 46 | + type: tensor | ||
| 47 | + required: true | ||
| 48 | + dtypes: | ||
| 49 | + values: [ fp32, fp16, bf16 ] | ||
| 50 | + ranges: | ||
| 51 | + valid: | ||
| 52 | + values: [ [-0.1, 0.1] ] | ||
| 53 | + invalid: | ||
| 54 | + values: [ [-0.1, 0.1] ] | ||
| 55 | + shapes: | ||
| 56 | + dim_numbers: | ||
| 57 | + values: [ 3 ] | ||
| 58 | + dim_values: | ||
| 59 | + values: [ 1 ] | ||
| 60 | + max_length: 4096 | ||
| 61 | + - name: chunk_size | ||
| 62 | + type: attr | ||
| 63 | + required: false | ||
| 64 | + dtypes: | ||
| 65 | + values: [ int ] | ||
| 66 | + ranges: | ||
| 67 | + valid: | ||
| 68 | + values: [ 64 ] | ||
| 69 | + invalid: | ||
| 70 | + values: [ 64 ] | ||
| @@ -0,0 +1,71 @@ | |||
| 1 | +# Copyright (c) 2026, HUAWEI CORPORATION. All rights reserved. | ||
| 2 | + | ||
| 3 | +import itertools | ||
| 4 | + | ||
| 5 | +from atk.case_generator.generator.base_generator import CaseGenerator | ||
| 6 | +from atk.case_generator.generator.generate_types import GENERATOR_REGISTRY | ||
| 7 | +from atk.configs.case_config import CaseConfig | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +@GENERATOR_REGISTRY.register("generate_chunk_gated_delta_rule_fwd_kkt_solve") | ||
| 11 | +class ChunkGatedDeltaRuleFwdKktSolveGenerator(CaseGenerator): | ||
| 12 | + """Case pool driven generator for chunk_gated_delta_rule_fwd_kkt_solve | ||
| 13 | + (non-varlen, with g and beta, BT=64 only -- the fused kernel hardcodes 4 | ||
| 14 | + sub-chunks of BC=16). | ||
| 15 | + | ||
| 16 | + Shapes (B, T, H, K, BT) curated from the unit-test coverage: | ||
| 17 | + k: (B, T, H, K) | ||
| 18 | + g: (B, T, H) | ||
| 19 | + beta: (B, T, H) | ||
| 20 | + """ | ||
| 21 | + | ||
| 22 | + CASES = list( | ||
| 23 | + (B, T, H, K, BT) | ||
| 24 | + for B, T, H, K, BT in itertools.product( | ||
| 25 | + [1, 2], # B | ||
| 26 | + [64, 128, 256], # T | ||
| 27 | + [2, 4], # H | ||
| 28 | + [64, 128], # K | ||
| 29 | + [64], # BT (the fused kernel only supports 64) | ||
| 30 | + ) | ||
| 31 | + ) | ||
| 32 | + | ||
| 33 | + _case_pool = None | ||
| 34 | + _case_index = 0 | ||
| 35 | + | ||
| 36 | + @classmethod | ||
| 37 | + def _build_pool(cls): | ||
| 38 | + return list(cls.CASES) | ||
| 39 | + | ||
| 40 | + def after_case_config(self, case_config: CaseConfig) -> CaseConfig: | ||
| 41 | + pool = ChunkGatedDeltaRuleFwdKktSolveGenerator._case_pool | ||
| 42 | + if pool is None: | ||
| 43 | + pool = self._build_pool() | ||
| 44 | + ChunkGatedDeltaRuleFwdKktSolveGenerator._case_pool = pool | ||
| 45 | + | ||
| 46 | + # Cycle through the curated pool so *every* generated case receives a | ||
| 47 | + # valid (B, T, H, K) shape. The fused kernel hardcodes 4 sub-chunks of | ||
| 48 | + # BC=16 (i.e. BT=64) and expects T to be a multiple of BT and K in | ||
| 49 | + # {64, 128}; the yaml default shape [1, 1, 1, 1] (T=1, K=1) is invalid: | ||
| 50 | + # it yields a mostly-zero (I + A)^{-1} whose near-zero golden entries | ||
| 51 | + # blow up the *relative* error metric, and on fp16 it can drive the | ||
| 52 | + # aicore into a singular-matrix hang that times out (error 507014) and | ||
| 53 | + # cascades to every subsequent case. Modulo cycling removes the | ||
| 54 | + # fallback branch entirely. | ||
| 55 | + idx = ChunkGatedDeltaRuleFwdKktSolveGenerator._case_index | ||
| 56 | + B, T, H, K, BT = pool[idx % len(pool)] | ||
| 57 | + ChunkGatedDeltaRuleFwdKktSolveGenerator._case_index += 1 | ||
| 58 | + | ||
| 59 | + k_input = case_config.inputs[0] | ||
| 60 | + g_input = case_config.inputs[1] | ||
| 61 | + beta_input = case_config.inputs[2] | ||
| 62 | + | ||
| 63 | + k_input.shape = [B, T, H, K] | ||
| 64 | + g_input.shape = [B, T, H] | ||
| 65 | + beta_input.shape = [B, T, H] | ||
| 66 | + | ||
| 67 | + # Align dtypes to avoid cross-dtype explosion | ||
| 68 | + g_input.dtype = k_input.dtype | ||
| 69 | + beta_input.dtype = k_input.dtype | ||
| 70 | + | ||
| 71 | + return case_config | ||
| @@ -0,0 +1,132 @@ | |||
| 1 | +# Copyright (c) 2026, HUAWEI CORPORATION. All rights reserved. | ||
| 2 | +# pylint: disable=duplicate-code | ||
| 3 | +# - duplicate-code: ATK golden is copied from tests/unit_tests/triton to keep | ||
| 4 | +# both checks aligned with the operator formula. | ||
| 5 | +"""ATK adapter for chunk_gated_delta_rule_fwd_kkt_solve (non-varlen, with g and beta). | ||
| 6 | + | ||
| 7 | +Inputs (named, matches chunk_gated_delta_rule_fwd_kkt_solve.yaml): | ||
| 8 | + k: (B, T, H, K) | ||
| 9 | + g: (B, T, H) | ||
| 10 | + beta: (B, T, H) | ||
| 11 | + chunk_size: int (must be 64) | ||
| 12 | + | ||
| 13 | +Output: | ||
| 14 | + A: (B, T, H, BT) -- the full (I + A)^{-1} | ||
| 15 | +""" | ||
| 16 | + | ||
| 17 | +from typing import Optional | ||
| 18 | + | ||
| 19 | +import torch | ||
| 20 | + | ||
| 21 | +from atk.configs.dataset_config import InputDataset | ||
| 22 | +from atk.tasks.api_execute import register | ||
| 23 | +from atk.tasks.api_execute.base_api import BaseApi | ||
| 24 | +from atk.tasks.api_execute.triton_base_api import TritonBaseApi | ||
| 25 | + | ||
| 26 | +from mindspeed_ops.api.triton.chunk_gated_delta_rule_fwd_kkt_solve import ( | ||
| 27 | + chunk_gated_delta_rule_fwd_kkt_solve, | ||
| 28 | +) | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +def _chunk_gated_delta_rule_fwd_kkt_solve_golden( | ||
| 32 | + k: torch.Tensor, | ||
| 33 | + g: Optional[torch.Tensor] = None, | ||
| 34 | + beta: Optional[torch.Tensor] = None, | ||
| 35 | + chunk_size: int = 64, | ||
| 36 | + output_dtype: torch.dtype = torch.float32, | ||
| 37 | +) -> torch.Tensor: | ||
| 38 | + """Golden reference copied from tests/unit_tests/triton/test_chunk_gated_delta_rule_fwd_kkt_solve.py.""" | ||
| 39 | + B, T, H, K_dim = k.shape | ||
| 40 | + BT = chunk_size | ||
| 41 | + k_float = k.float() | ||
| 42 | + | ||
| 43 | + A = torch.zeros(B, T, H, BT, device=k.device, dtype=torch.float32) | ||
| 44 | + eye = torch.eye(BT, device=k.device, dtype=torch.float32) | ||
| 45 | + | ||
| 46 | + for b in range(B): | ||
| 47 | + bos, eos = 0, T | ||
| 48 | + T_local = T | ||
| 49 | + batch_idx = b | ||
| 50 | + | ||
| 51 | + for h in range(H): | ||
| 52 | + k_bh = k_float[batch_idx, bos:eos, h, :] | ||
| 53 | + g_bh = g[batch_idx, bos:eos, h].float() if g is not None else None | ||
| 54 | + beta_bh = beta[batch_idx, bos:eos, h].float() if beta is not None else None | ||
| 55 | + | ||
| 56 | + NT = (T_local + BT - 1) // BT | ||
| 57 | + for t_idx in range(NT): | ||
| 58 | + cs = t_idx * BT | ||
| 59 | + ce = min(cs + BT, T_local) | ||
| 60 | + ll = ce - cs | ||
| 61 | + | ||
| 62 | + k_chunk = k_bh[cs:ce, :] | ||
| 63 | + kkt = k_chunk @ k_chunk.t() | ||
| 64 | + | ||
| 65 | + A_chunk = torch.zeros(BT, BT, dtype=torch.float32, device=k.device) | ||
| 66 | + A_chunk[:ll, :ll] = kkt | ||
| 67 | + | ||
| 68 | + tril_mask = torch.tril(torch.ones(BT, BT, device=k.device), diagonal=-1) | ||
| 69 | + A_chunk = A_chunk * tril_mask | ||
| 70 | + | ||
| 71 | + if g_bh is not None: | ||
| 72 | + g_chunk = g_bh[cs:ce] | ||
| 73 | + g_diff = g_chunk[:, None] - g_chunk[None, :] | ||
| 74 | + g_diff = torch.clamp(g_diff, -50.0, 50.0) | ||
| 75 | + g_full = torch.zeros(BT, BT, dtype=torch.float32, device=k.device) | ||
| 76 | + g_full[:ll, :ll] = torch.exp(g_diff) | ||
| 77 | + A_chunk = A_chunk * g_full | ||
| 78 | + | ||
| 79 | + if beta_bh is not None: | ||
| 80 | + beta_chunk = beta_bh[cs:ce] | ||
| 81 | + beta_full = torch.zeros(BT, device=k.device, dtype=torch.float32) | ||
| 82 | + beta_full[:ll] = beta_chunk | ||
| 83 | + A_chunk = A_chunk * beta_full[:, None] | ||
| 84 | + else: | ||
| 85 | + beta_full = torch.zeros(BT, device=k.device, dtype=torch.float32) | ||
| 86 | + beta_full[:ll] = 1.0 | ||
| 87 | + A_chunk = A_chunk * beta_full[:, None] | ||
| 88 | + | ||
| 89 | + inv = torch.inverse(A_chunk + eye) | ||
| 90 | + A[batch_idx, bos + cs : bos + ce, h, :] = inv[:ll, :ll].to(output_dtype) | ||
| 91 | + | ||
| 92 | + return A | ||
| 93 | + | ||
| 94 | + | ||
| 95 | +@register("torch_chunk_gated_delta_rule_fwd_kkt_solve") | ||
| 96 | +class TorchChunkGatedDeltaRuleFwdKktSolveApi(BaseApi): | ||
| 97 | + """CPU/GPU golden reference for chunk_gated_delta_rule_fwd_kkt_solve.""" | ||
| 98 | + | ||
| 99 | + def __call__(self, input_data: InputDataset, with_output: bool = False): | ||
| 100 | + k = input_data.kwargs.get('k') | ||
| 101 | + g = input_data.kwargs.get('g') | ||
| 102 | + beta = input_data.kwargs.get('beta') | ||
| 103 | + chunk_size = input_data.kwargs.get('chunk_size', 64) | ||
| 104 | + | ||
| 105 | + output = _chunk_gated_delta_rule_fwd_kkt_solve_golden( | ||
| 106 | + k.float(), | ||
| 107 | + g=g.float() if g is not None else None, | ||
| 108 | + beta=beta.float() if beta is not None else None, | ||
| 109 | + chunk_size=chunk_size, | ||
| 110 | + output_dtype=k.dtype, | ||
| 111 | + ) | ||
| 112 | + return output | ||
| 113 | + | ||
| 114 | + | ||
| 115 | +@register("triton_chunk_gated_delta_rule_fwd_kkt_solve") | ||
| 116 | +class TritonChunkGatedDeltaRuleFwdKktSolveApi(TritonBaseApi): | ||
| 117 | + """Triton path: invoke mindspeed_ops chunk_gated_delta_rule_fwd_kkt_solve.""" | ||
| 118 | + | ||
| 119 | + def __call__(self, input_data: InputDataset, with_output: bool = False): | ||
| 120 | + k = input_data.kwargs.get('k') | ||
| 121 | + g = input_data.kwargs.get('g') | ||
| 122 | + beta = input_data.kwargs.get('beta') | ||
| 123 | + chunk_size = input_data.kwargs.get('chunk_size', 64) | ||
| 124 | + | ||
| 125 | + output = chunk_gated_delta_rule_fwd_kkt_solve( | ||
| 126 | + k, | ||
| 127 | + g=g, | ||
| 128 | + beta=beta, | ||
| 129 | + chunk_size=chunk_size, | ||
| 130 | + output_dtype=k.dtype, | ||
| 131 | + ) | ||
| 132 | + return output | ||
| @@ -0,0 +1,277 @@ | |||
| 1 | +# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved. | ||
| 2 | +# pylint: disable=duplicate-code | ||
| 3 | +# - duplicate-code: UT and ATK share the same reference implementation to keep | ||
| 4 | +# both checks aligned with the operator formula. | ||
| 5 | + | ||
| 6 | + | ||
| 7 | +from typing import Optional | ||
| 8 | + | ||
| 9 | +import pytest | ||
| 10 | +import torch | ||
| 11 | + | ||
| 12 | +from mindspeed_ops.api.triton.chunk_gated_delta_rule_fwd_kkt_solve import ( | ||
| 13 | + chunk_gated_delta_rule_fwd_kkt_solve, | ||
| 14 | +) | ||
| 15 | +from mindspeed_ops.api.triton.chunk_scaled_dot_kkt import chunk_scaled_dot_kkt_fwd | ||
| 16 | +from mindspeed_ops.api.triton.solve_tril import solve_tril | ||
| 17 | +from mindspeed_ops.api.triton.utils import get_available_device | ||
| 18 | +from tests.utils import assert_close, print_diff | ||
| 19 | + | ||
| 20 | + | ||
| 21 | +# --------------------------------------------------------------------------- | ||
| 22 | +# Golden reference: (I + A)^{-1}, A = beta * tril(K @ K^T) * exp(clamp(g_i-g_j, ±50)) | ||
| 23 | +# --------------------------------------------------------------------------- | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +def chunk_gated_delta_rule_fwd_kkt_solve_golden( | ||
| 27 | + k: torch.Tensor, | ||
| 28 | + g: Optional[torch.Tensor] = None, | ||
| 29 | + beta: Optional[torch.Tensor] = None, | ||
| 30 | + cu_seqlens: Optional[torch.LongTensor] = None, | ||
| 31 | + chunk_size: int = 64, | ||
| 32 | + output_dtype: torch.dtype = torch.float32, | ||
| 33 | +) -> torch.Tensor: | ||
| 34 | + B, T, H, K_dim = k.shape | ||
| 35 | + BT = chunk_size | ||
| 36 | + k_float = k.float() | ||
| 37 | + | ||
| 38 | + A = torch.zeros(B, T, H, BT, device=k.device, dtype=torch.float32) | ||
| 39 | + | ||
| 40 | + num_seqs = len(cu_seqlens) - 1 if cu_seqlens is not None else B | ||
| 41 | + seq_offsets = cu_seqlens.tolist() if cu_seqlens is not None else [i * T for i in range(B + 1)] | ||
| 42 | + | ||
| 43 | + eye = torch.eye(BT, device=k.device, dtype=torch.float32) | ||
| 44 | + tril_mask = torch.tril(torch.ones(BT, BT, device=k.device), diagonal=-1) | ||
| 45 | + | ||
| 46 | + for batch_idx in range(num_seqs): | ||
| 47 | + bos = seq_offsets[batch_idx] | ||
| 48 | + T_local = seq_offsets[batch_idx + 1] - seq_offsets[batch_idx] | ||
| 49 | + NT = (T_local + BT - 1) // BT | ||
| 50 | + for h in range(H): | ||
| 51 | + k_bh = k_float[0, bos : bos + T_local, h] if cu_seqlens is not None else k_float[batch_idx, :, h] | ||
| 52 | + g_bh = ( | ||
| 53 | + None if g is None else (g[0, bos : bos + T_local, h] if cu_seqlens is not None else g[batch_idx, :, h]) | ||
| 54 | + ) | ||
| 55 | + beta_bh = ( | ||
| 56 | + None | ||
| 57 | + if beta is None | ||
| 58 | + else (beta[0, bos : bos + T_local, h] if cu_seqlens is not None else beta[batch_idx, :, h]) | ||
| 59 | + ) | ||
| 60 | + for t_idx in range(NT): | ||
| 61 | + cs = t_idx * BT | ||
| 62 | + ce = min(cs + BT, T_local) | ||
| 63 | + ll = ce - cs | ||
| 64 | + | ||
| 65 | + k_chunk = k_bh[cs:ce, :] | ||
| 66 | + kkt = k_chunk @ k_chunk.t() | ||
| 67 | + | ||
| 68 | + A_chunk = torch.zeros(BT, BT, dtype=torch.float32, device=k.device) | ||
| 69 | + A_chunk[:ll, :ll] = kkt | ||
| 70 | + A_chunk = A_chunk * tril_mask | ||
| 71 | + | ||
| 72 | + if g_bh is not None: | ||
| 73 | + g_chunk = g_bh[cs:ce] | ||
| 74 | + g_diff = g_chunk[:, None] - g_chunk[None, :] | ||
| 75 | + g_diff = torch.clamp(g_diff, -50.0, 50.0) | ||
| 76 | + g_full = torch.zeros(BT, BT, dtype=torch.float32, device=k.device) | ||
| 77 | + g_full[:ll, :ll] = torch.exp(g_diff) | ||
| 78 | + A_chunk = A_chunk * g_full | ||
| 79 | + | ||
| 80 | + beta_full = torch.zeros(BT, device=k.device, dtype=torch.float32) | ||
| 81 | + beta_full[:ll] = beta_bh[cs:ce] if beta_bh is not None else 1.0 | ||
| 82 | + A_chunk = A_chunk * beta_full[:, None] | ||
| 83 | + | ||
| 84 | + inv = torch.inverse(A_chunk + eye) | ||
| 85 | + # keep all BT columns of the first ll rows (kernel writes the full | ||
| 86 | + # (I+A)^{-1} rows; padding rows stay zero) | ||
| 87 | + if cu_seqlens is not None: | ||
| 88 | + A[0, bos + cs : bos + ce, h, :] = inv[:ll, :].to(output_dtype) | ||
| 89 | + else: | ||
| 90 | + A[batch_idx, cs:ce, h, :] = inv[:ll, :].to(output_dtype) | ||
| 91 | + | ||
| 92 | + return A | ||
| 93 | + | ||
| 94 | + | ||
| 95 | +# --------------------------------------------------------------------------- | ||
| 96 | +# Parameter spaces | ||
| 97 | +# --------------------------------------------------------------------------- | ||
| 98 | + | ||
| 99 | +DTYPES = [torch.float32, torch.bfloat16, torch.float16] | ||
| 100 | +DTYPE_NAME = {torch.float32: "fp32", torch.bfloat16: "bf16", torch.float16: "fp16"} | ||
| 101 | + | ||
| 102 | +# (B, T, H, K, BT) -- the fused kernel only supports BT=64. | ||
| 103 | +TEST_SHAPES = [ | ||
| 104 | + (1, 64, 2, 64, 64), # NT=1 boundary (odd pair slot is padding) | ||
| 105 | + (1, 128, 4, 128, 64), # NT=2 typical (exactly one pair) | ||
| 106 | + (1, 256, 4, 128, 64), # NT=4 typical | ||
| 107 | + (2, 128, 4, 128, 64), # B=2 | ||
| 108 | + (1, 192, 4, 128, 64), # NT=3, partial last chunk AND odd chunk count | ||
| 109 | + (1, 2048, 4, 128, 64), # long sequence, NT=32 | ||
| 110 | + (1, 8192, 2, 128, 64), # very long sequence, NT=128 | ||
| 111 | + (2, 1024, 4, 128, 64), # long sequence with B>1 | ||
| 112 | + (2, 8192, 32, 128, 64), # very large grid (multi-buffered task loop) | ||
| 113 | + (21, 195, 32, 128, 64), # large B * partial last chunk (regression guard) | ||
| 114 | +] | ||
| 115 | + | ||
| 116 | + | ||
| 117 | +def _shape_id(shape): | ||
| 118 | + B, T, H, K, BT = shape | ||
| 119 | + return f"B{B}_T{T}_H{H}_K{K}_BT{BT}" | ||
| 120 | + | ||
| 121 | + | ||
| 122 | +def _dtype_thresholds(dtype): | ||
| 123 | + if dtype == torch.float32: | ||
| 124 | + return 1e-4, 1e-5 | ||
| 125 | + if dtype == torch.bfloat16: | ||
| 126 | + return 1e-2, 5e-3 | ||
| 127 | + if dtype == torch.float16: | ||
| 128 | + return 1e-3, 5e-4 | ||
| 129 | + raise ValueError(f"unsupported dtype {dtype}") | ||
| 130 | + | ||
| 131 | + | ||
| 132 | +# --------------------------------------------------------------------------- | ||
| 133 | +# Tests: golden x dtype/shape + no-g/beta + varlen + cross-check + fallback | ||
| 134 | +# --------------------------------------------------------------------------- | ||
| 135 | + | ||
| 136 | + | ||
| 137 | +class TestChunkGatedDeltaRuleFwdKktSolve: | ||
| 138 | + def setup_method(self): | ||
| 139 | + torch.manual_seed(42) | ||
| 140 | + | ||
| 141 | + @pytest.mark.parametrize("dtype", DTYPES, ids=lambda d: DTYPE_NAME[d]) | ||
| 142 | + @pytest.mark.parametrize("shape", TEST_SHAPES, ids=_shape_id) | ||
| 143 | + def test_fwd_with_g_beta(self, shape, dtype): | ||
| 144 | + """Fused kkt+solve with g and beta against golden reference.""" | ||
| 145 | + device = get_available_device() | ||
| 146 | + if device == "cpu": | ||
| 147 | + pytest.skip("Triton requires NPU/GPU device") | ||
| 148 | + | ||
| 149 | + B, T, H, K, BT = shape | ||
| 150 | + k = torch.randn(B, T, H, K, device=device, dtype=dtype) * 0.1 | ||
| 151 | + g = torch.randn(B, T, H, device=device, dtype=dtype) * 0.01 | ||
| 152 | + beta = torch.randn(B, T, H, device=device, dtype=dtype) * 0.1 | ||
| 153 | + | ||
| 154 | + # CPU small-op golden (task-spec caliber: align to CPU when no GPU ref) | ||
| 155 | + ref = chunk_gated_delta_rule_fwd_kkt_solve_golden( | ||
| 156 | + k.cpu().float(), | ||
| 157 | + g=g.cpu().float(), | ||
| 158 | + beta=beta.cpu().float(), | ||
| 159 | + chunk_size=BT, | ||
| 160 | + output_dtype=dtype, | ||
| 161 | + ) | ||
| 162 | + out = chunk_gated_delta_rule_fwd_kkt_solve(k, g=g, beta=beta, chunk_size=BT, output_dtype=dtype) | ||
| 163 | + out = out.cpu() | ||
| 164 | + | ||
| 165 | + ratio, atol = _dtype_thresholds(dtype) | ||
| 166 | + print_diff("fwd_kkt_solve_with_g_beta", ref, out, atol) | ||
| 167 | + assert_close("fwd_kkt_solve_with_g_beta", ref, out, ratio, err_atol=atol) | ||
| 168 | + | ||
| 169 | + @pytest.mark.parametrize("shape", TEST_SHAPES, ids=_shape_id) | ||
| 170 | + def test_fwd_no_g_beta(self, shape): | ||
| 171 | + """Fused kkt+solve without g and beta (beta treated as ones).""" | ||
| 172 | + device = get_available_device() | ||
| 173 | + if device == "cpu": | ||
| 174 | + pytest.skip("Triton requires NPU/GPU device") | ||
| 175 | + | ||
| 176 | + B, T, H, K, BT = shape | ||
| 177 | + dtype = torch.bfloat16 | ||
| 178 | + k = torch.randn(B, T, H, K, device=device, dtype=dtype) * 0.1 | ||
| 179 | + | ||
| 180 | + ref = chunk_gated_delta_rule_fwd_kkt_solve_golden(k.cpu().float(), chunk_size=BT, output_dtype=dtype) | ||
| 181 | + out = chunk_gated_delta_rule_fwd_kkt_solve(k, chunk_size=BT, output_dtype=dtype) | ||
| 182 | + out = out.cpu() | ||
| 183 | + | ||
| 184 | + ratio, atol = _dtype_thresholds(dtype) | ||
| 185 | + print_diff("fwd_kkt_solve_no_g_beta", ref, out, atol) | ||
| 186 | + assert_close("fwd_kkt_solve_no_g_beta", ref, out, ratio, err_atol=atol) | ||
| 187 | + | ||
| 188 | + @pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16], ids=["fp32", "bf16"]) | ||
| 189 | + def test_fwd_varlen(self, dtype): | ||
| 190 | + """Variable-length sequences via cu_seqlens (unfused fallback path).""" | ||
| 191 | + device = get_available_device() | ||
| 192 | + if device == "cpu": | ||
| 193 | + pytest.skip("Triton requires NPU/GPU device") | ||
| 194 | + | ||
| 195 | + B, H, K, BT = 1, 4, 128, 64 | ||
| 196 | + seqlens = [64, 128, 256] | ||
| 197 | + T_total = sum(seqlens) | ||
| 198 | + cu_seqlens = torch.tensor( | ||
| 199 | + [0] + list(torch.cumsum(torch.tensor(seqlens), 0).tolist()), dtype=torch.int32, device=device | ||
| 200 | + ) | ||
| 201 | + | ||
| 202 | + torch.manual_seed(0) | ||
| 203 | + k = torch.randn(B, T_total, H, K, device=device, dtype=dtype) * 0.1 | ||
| 204 | + g = -torch.randn(B, T_total, H, device=device, dtype=dtype).abs() * 0.01 | ||
| 205 | + beta = torch.randn(B, T_total, H, device=device, dtype=dtype) * 0.1 | ||
| 206 | + | ||
| 207 | + cu_ref = cu_seqlens.cpu() | ||
| 208 | + ref = chunk_gated_delta_rule_fwd_kkt_solve_golden( | ||
| 209 | + k.cpu().float(), | ||
| 210 | + g=g.cpu().float(), | ||
| 211 | + beta=beta.cpu().float(), | ||
| 212 | + cu_seqlens=cu_ref, | ||
| 213 | + chunk_size=BT, | ||
| 214 | + output_dtype=dtype, | ||
| 215 | + ) | ||
| 216 | + out = chunk_gated_delta_rule_fwd_kkt_solve( | ||
| 217 | + k, g=g, beta=beta, cu_seqlens=cu_seqlens, chunk_size=BT, output_dtype=dtype | ||
| 218 | + ) | ||
| 219 | + out = out.cpu() | ||
| 220 | + | ||
| 221 | + ratio, atol = _dtype_thresholds(dtype) | ||
| 222 | + print_diff("fwd_kkt_solve_varlen", ref, out, atol) | ||
| 223 | + assert_close("fwd_kkt_solve_varlen", ref, out, ratio, err_atol=atol) | ||
| 224 | + | ||
| 225 | + offsets = [0] + list(torch.cumsum(torch.tensor(seqlens), 0).tolist()) | ||
| 226 | + for i, _ in enumerate(seqlens): | ||
| 227 | + s, e = offsets[i], offsets[i + 1] | ||
| 228 | + sub_ref = chunk_gated_delta_rule_fwd_kkt_solve_golden( | ||
| 229 | + k[:, s:e].contiguous().cpu().float(), | ||
| 230 | + g=g[:, s:e].contiguous().cpu().float(), | ||
| 231 | + beta=beta[:, s:e].contiguous().cpu().float(), | ||
| 232 | + chunk_size=BT, | ||
| 233 | + output_dtype=dtype, | ||
| 234 | + ) | ||
| 235 | + assert_close(f"varlen_seq{i}", sub_ref, out[:, s:e].contiguous().cpu(), ratio, err_atol=atol) | ||
| 236 | + | ||
| 237 | + @pytest.mark.parametrize("shape", TEST_SHAPES, ids=_shape_id) | ||
| 238 | + def test_fwd_cross_check_unfused(self, shape): | ||
| 239 | + """Fused output must match the unfused chunk_scaled_dot_kkt_fwd + solve_tril.""" | ||
| 240 | + device = get_available_device() | ||
| 241 | + if device == "cpu": | ||
| 242 | + pytest.skip("Triton requires NPU/GPU device") | ||
| 243 | + | ||
| 244 | + B, T, H, K, BT = shape | ||
| 245 | + dtype = torch.bfloat16 | ||
| 246 | + k = torch.randn(B, T, H, K, device=device, dtype=dtype) * 0.1 | ||
| 247 | + g = torch.randn(B, T, H, device=device, dtype=dtype) * 0.01 | ||
| 248 | + beta = torch.randn(B, T, H, device=device, dtype=dtype) * 0.1 | ||
| 249 | + | ||
| 250 | + fused = chunk_gated_delta_rule_fwd_kkt_solve(k, g=g, beta=beta, chunk_size=BT, output_dtype=dtype) | ||
| 251 | + unfused_A = chunk_scaled_dot_kkt_fwd(k=k, g=g, beta=beta, chunk_size=BT, output_dtype=torch.float32) | ||
| 252 | + unfused = solve_tril(A=unfused_A, output_dtype=dtype) | ||
| 253 | + | ||
| 254 | + ratio, atol = _dtype_thresholds(dtype) | ||
| 255 | + print_diff("fwd_kkt_solve_vs_unfused", unfused, fused, atol) | ||
| 256 | + assert_close("fwd_kkt_solve_vs_unfused", unfused, fused, ratio, err_atol=atol) | ||
| 257 | + | ||
| 258 | + def test_non64_chunk_size_fallback(self): | ||
| 259 | + """chunk_size != 64 routes to the unfused chain and must stay equivalent.""" | ||
| 260 | + device = get_available_device() | ||
| 261 | + if device == "cpu": | ||
| 262 | + pytest.skip("Triton requires NPU/GPU device") | ||
| 263 | + k = torch.randn(1, 64, 2, 64, device=device, dtype=torch.float32) * 0.1 | ||
| 264 | + g = torch.randn(1, 64, 2, device=device, dtype=torch.float32) * 0.01 | ||
| 265 | + beta = torch.randn(1, 64, 2, device=device, dtype=torch.float32) * 0.1 | ||
| 266 | + | ||
| 267 | + out32 = chunk_gated_delta_rule_fwd_kkt_solve(k, g=g, beta=beta, chunk_size=32, output_dtype=torch.float32) | ||
| 268 | + ref32 = solve_tril( | ||
| 269 | + A=chunk_scaled_dot_kkt_fwd(k=k, g=g, beta=beta, chunk_size=32, output_dtype=torch.float32), | ||
| 270 | + output_dtype=torch.float32, | ||
| 271 | + ) | ||
| 272 | + ratio, atol = _dtype_thresholds(torch.float32) | ||
| 273 | + assert_close("fallback_cs32", ref32, out32, ratio, err_atol=atol) | ||
| 274 | + | ||
| 275 | + | ||
| 276 | +if __name__ == "__main__": | ||
| 277 | + pytest.main([__file__, "-v", "--tb=short"]) | ||