kernelbench.com

KernelBench hard · H100

KDA CUTLASS Claude Fable 5

5.27%geomean peak fraction across shapes

manually audited: clean

harnessor-fableagent session1h 13mtotal wall1h 13mcheck19sbenchmark3soutput tokensgpu-lock wait0sgpu-lock held13mregimecompute

Per-shape vs governing ceilingeach shape graded against whichever binds — bf16 compute or HBM bandwidth

2×1024×8×128×128×640.049 ms5.8%0.51 TB/s · 25% of 2.0 TB/s HBM · also 43 TFLOPS (6% of compute)
2×2048×8×128×128×640.088 ms6.5%0.57 TB/s · 28% of 2.0 TB/s HBM · also 49 TFLOPS (6% of compute)
1×4096×8×128×128×640.091 ms6.2%0.55 TB/s · 27% of 2.0 TB/s HBM · also 47 TFLOPS (6% of compute)
1×2048×4×128×128×640.043 ms3.3%0.30 TB/s · 15% of 2.0 TB/s HBM · also 25 TFLOPS (3% of compute)

compute-bound memory-bound · bar + right column = official fraction of the ceiling (the geomean input)

geomean(5.8% · 6.5% · 6.2% · 3.3%) = 5.3%

Kernel source (redacted)
"""Custom Triton implementation of Kimi Delta Attention forward (chunk form).

Pipeline (three kernels; shapes here have BT = 64, K = V = 128):
  1. `_kda_prepare` (parallel over B*H*NT chunks): in-chunk cumsum of the
     log-decay in log2 space, WY representation via a Newton/Neumann doubling
     inversion of (I + L) on tensor cores, emitting w, u, the masked
     intra-chunk Aqk, decay-weighted qg / kg and per-chunk terminal decay dn.
  2. `_kda_seg_op` (parallel over segments x column blocks): composes the
     per-chunk affine state update S -> diag(dn) S + kg^T (u - w S) over each
     segment of P chunks, producing the segment operator M_seg (K x K, column
     blocks with X0 = I) and offset C_seg (K x V, column blocks with X0 = 0)
     in one launch with branch-free inner loops.
  3. `_kda_scan_seg` (parallel over B*H*NSEG segments x V-blocks): recomputes
     its segment's entry state h0 from the few segment operators in a short
     prologue (redundant-but-parallel work instead of a serial scan kernel),
     then replays the chunks inside the segment, producing v_new and the
     output o = qg @ S + Aqk @ v_new with the state held in registers.

The two-level (segmented) scan turns the NT-long sequential chunk recurrence
into an NSEG-long one plus parallel work, which is what makes the thin-batch
shapes fast on 132 SMs.

forward() captures the launches into a CUDA graph when the same input buffers
are passed repeatedly (the benchmark harness pattern).
"""
from __future__ import annotations

import torch
import torch.nn as nn
import triton
import triton.language as tl

OP_TYPE = "linear_attention"
SUPPORTED_PRECISIONS = ["bf16"]
HARDWARE_REQUIRED = ["RTX_PRO_6000", "H100", "B200"]

RCP_LN2 = tl.constexpr(1.4426950408889634)  # 1 / ln(2)


@triton.jit
def _kda_prepare(
    q, k, v, g, beta,
    w, u, qg, kg, aqk, dn,
    scale, bh_off,
    T: tl.constexpr, H: tl.constexpr, K: tl.constexpr, V: tl.constexpr,
    BT: tl.constexpr, NT: tl.constexpr,
):
    i_t = tl.program_id(0)
    i_bh = tl.program_id(1) + bh_off
    i_b = i_bh // H
    i_h = i_bh % H

    o_t = tl.arange(0, BT)
    o_k = tl.arange(0, K)
    o_v = tl.arange(0, V)

    base_tok = (i_b * T + i_t * BT) * H + i_h
    p_g = g + base_tok * K + o_t[:, None] * (H * K) + o_k[None, :]
    p_q = q + base_tok * K + o_t[:, None] * (H * K) + o_k[None, :]
    p_k = k + base_tok * K + o_t[:, None] * (H * K) + o_k[None, :]
    p_v = v + base_tok * V + o_t[:, None] * (H * V) + o_v[None, :]
    p_beta = beta + base_tok + o_t * H

    base_out = (i_bh * T + i_t * BT)
    p_w = w + base_out * K + o_t[:, None] * K + o_k[None, :]
    p_u = u + base_out * V + o_t[:, None] * V + o_v[None, :]
    p_qg = qg + base_out * K + o_t[:, None] * K + o_k[None, :]
    p_kg = kg + base_out * K + o_t[:, None] * K + o_k[None, :]
    p_aqk = aqk + base_out * BT + o_t[:, None] * BT + o_t[None, :]
    p_dn = dn + (i_bh * NT + i_t) * K + o_k

    b_beta = tl.load(p_beta).to(tl.float32)

    b_gr = tl.load(p_g).to(tl.float32) * RCP_LN2
    b_gn = tl.sum(b_gr, 0)                       # (K,) total log2 decay
    b_gc = tl.cumsum(b_gr, 0)                    # (BT, K)
    b_e = tl.exp2(b_gc)
    b_en = tl.exp2(b_gn[None, :] - b_gc)

    b_k = tl.load(p_k).to(tl.float32)
    b_kneg = (b_k * tl.exp2(-b_gc)).to(tl.bfloat16)   # k * 2^-gc
    tl.store(p_kg, (b_k * b_en).to(tl.bfloat16))      # k * 2^(gN-gc)
    tl.store(p_dn, tl.exp2(b_gn))
    b_kpos = (b_k * b_e).to(tl.bfloat16)              # k * 2^gc

    # scaled decayed q; Aqk right away so qg can be freed
    b_qg = (tl.load(p_q).to(tl.float32) * (b_e * scale)).to(tl.bfloat16)
    tl.store(p_qg, b_qg)
    b_Aqk = tl.dot(b_qg, tl.trans(b_kneg))
    b_Aqk = tl.where(o_t[:, None] >= o_t[None, :], b_Aqk, 0.0)
    tl.store(p_aqk, b_Aqk.to(tl.bfloat16))

    # strictly-lower K-K matrix, beta on rows
    b_A = tl.dot(b_kpos, tl.trans(b_kneg))
    m_lo = o_t[:, None] > o_t[None, :]
    b_L = tl.where(m_lo, b_A, 0.0) * b_beta[:, None]

    # X = (I + L)^{-1} via doubling: X += X @ R; R = R @ R (bf16 operands,
    # fp32 accumulation -- validated against the fp32 reference).
    m_eye = o_t[:, None] == o_t[None, :]
    b_X = tl.where(m_eye, 1.0, 0.0) - b_L
    b_Lb = b_L.to(tl.bfloat16)
    b_R = tl.dot(b_Lb, b_Lb).to(tl.bfloat16)          # L^2
    b_X += tl.dot(b_X.to(tl.bfloat16), b_R)           # covers L^3
    b_R = tl.dot(b_R, b_R).to(tl.bfloat16)            # L^4
    b_X += tl.dot(b_X.to(tl.bfloat16), b_R)           # covers L^7
    b_R = tl.dot(b_R, b_R).to(tl.bfloat16)            # L^8
    b_X += tl.dot(b_X.to(tl.bfloat16), b_R)           # covers L^15
    b_R = tl.dot(b_R, b_R).to(tl.bfloat16)            # L^16
    b_X += tl.dot(b_X.to(tl.bfloat16), b_R)           # covers L^31
    b_R = tl.dot(b_R, b_R).to(tl.bfloat16)            # L^32
    b_X += tl.dot(b_X.to(tl.bfloat16), b_R)           # covers L^63

    b_Xh = b_X.to(tl.bfloat16)

    # w = X @ (beta * k * 2^gc), u = X @ (beta * v)
    b_w = tl.dot(b_Xh, (b_kpos.to(tl.float32) * b_beta[:, None]).to(tl.bfloat16))
    tl.store(p_w, b_w.to(tl.bfloat16))
    b_v = tl.load(p_v)
    b_u = tl.dot(b_Xh, (b_v.to(tl.float32) * b_beta[:, None]).to(tl.bfloat16))
    tl.store(p_u, b_u.to(tl.bfloat16))


@triton.jit
def _kda_seg_op(
    w, u, kg, dn, mseg, cseg, bh_off,
    T: tl.constexpr, H: tl.constexpr, K: tl.constexpr, V: tl.constexpr,
    BT: tl.constexpr, NT: tl.constexpr, P: tl.constexpr, NSEG: tl.constexpr,
    BX: tl.constexpr,
):
    """Segment operator column block.

    Column blocks [0, K) build M_seg (X0 = I, no u term); blocks [K, K+V)
    build C_seg (X0 = 0, with u). Update per chunk:
        X <- dn[:, None] * X + kg^T @ ((u or 0) - w @ X)
    The M/C decision is uniform per program, so each loop body stays
    branch-free and pipelines.
    """
    i_x = tl.program_id(0)
    i_p = tl.program_id(1)
    i_bh = tl.program_id(2) + bh_off

    o_t = tl.arange(0, BT)
    o_k = tl.arange(0, K)
    o_x = tl.arange(0, BX)

    NXM: tl.constexpr = K // BX

    if i_x < NXM:
        # ---- M part: segments 1 .. NSEG-2 ----
        i_s = i_p + 1
        if i_s > NSEG - 2:
            return
        b_X = (o_k[:, None] == (i_x * BX + o_x)[None, :]).to(tl.float32)
        for t in range(i_s * P, i_s * P + P):
            base_out = (i_bh * T + t * BT)
            b_w = tl.load(w + base_out * K + o_t[:, None] * K + o_k[None, :])
            b_kgt = tl.load(kg + base_out * K + o_k[:, None] + o_t[None, :] * K)
            b_dn = tl.load(dn + (i_bh * NT + t) * K + o_k)
            b_wX = tl.dot(b_w, b_X.to(tl.bfloat16))
            b_X = b_X * b_dn[:, None] - tl.dot(b_kgt, b_wX.to(tl.bfloat16))
        p_m = mseg + (i_bh * NSEG + i_s) * K * K \
            + o_k[:, None] * K + i_x * BX + o_x[None, :]
        tl.store(p_m, b_X.to(tl.bfloat16))
    else:
        # ---- C part: segments 0 .. NSEG-2 ----
        i_s = i_p
        i_xc = i_x - NXM
        b_X = tl.zeros([K, BX], dtype=tl.float32)
        for t in range(i_s * P, i_s * P + P):
            base_out = (i_bh * T + t * BT)
            b_w = tl.load(w + base_out * K + o_t[:, None] * K + o_k[None, :])
            b_kgt = tl.load(kg + base_out * K + o_k[:, None] + o_t[None, :] * K)
            b_dn = tl.load(dn + (i_bh * NT + t) * K + o_k)
            b_u = tl.load(u + base_out * V + o_t[:, None] * V
                          + i_xc * BX + o_x[None, :])
            b_r = b_u.to(tl.float32) - tl.dot(b_w, b_X.to(tl.bfloat16))
            b_X = b_X * b_dn[:, None] + tl.dot(b_kgt, b_r.to(tl.bfloat16))
        p_c = cseg + (i_bh * NSEG + i_s) * K * V \
            + o_k[:, None] * V + i_xc * BX + o_x[None, :]
        tl.store(p_c, b_X.to(tl.bfloat16))


@triton.jit
def _kda_scan_seg(
    w, u, qg, kg, aqk, dn, mseg, cseg, o, bh_off, seg_off,
    T: tl.constexpr, H: tl.constexpr, K: tl.constexpr, V: tl.constexpr,
    BT: tl.constexpr, NT: tl.constexpr, P: tl.constexpr, NSEG: tl.constexpr,
    BV: tl.constexpr,
):
    i_v = tl.program_id(0)
    i_s = tl.program_id(1) + seg_off
    i_bh = tl.program_id(2) + bh_off
    i_b = i_bh // H
    i_h = i_bh % H

    o_t = tl.arange(0, BT)
    o_k = tl.arange(0, K)
    o_bv = i_v * BV + tl.arange(0, BV)

    # h0 for this segment, recomputed from the segment operators: this
    # replaces a separate serial scan kernel with redundant-but-parallel work.
    b_S = tl.zeros([K, BV], dtype=tl.float32)
    if i_s > 0:
        p_c = cseg + i_bh * NSEG * K * V + o_k[:, None] * V + o_bv[None, :]
        b_S = tl.load(p_c).to(tl.float32)
        for j in range(1, i_s):
            p_m = mseg + (i_bh * NSEG + j) * K * K \
                + o_k[:, None] * K + o_k[None, :]
            p_c = cseg + (i_bh * NSEG + j) * K * V \
                + o_k[:, None] * V + o_bv[None, :]
            b_m = tl.load(p_m)
            b_c = tl.load(p_c).to(tl.float32)
            b_S = tl.dot(b_m, b_S.to(tl.bfloat16)) + b_c

    for t in range(i_s * P, i_s * P + P):
        base_out = (i_bh * T + t * BT)
        b_w = tl.load(w + base_out * K + o_t[:, None] * K + o_k[None, :])
        b_qg = tl.load(qg + base_out * K + o_t[:, None] * K + o_k[None, :])
        b_kgt = tl.load(kg + base_out * K + o_k[:, None] + o_t[None, :] * K)
        b_u = tl.load(u + base_out * V + o_t[:, None] * V + o_bv[None, :])
        b_aqk = tl.load(aqk + base_out * BT + o_t[:, None] * BT + o_t[None, :])
        b_dn = tl.load(dn + (i_bh * NT + t) * K + o_k)

        b_Sb = b_S.to(tl.bfloat16)
        b_vn = b_u.to(tl.float32) - tl.dot(b_w, b_Sb)
        b_vnb = b_vn.to(tl.bfloat16)
        b_o = tl.dot(b_qg, b_Sb) + tl.dot(b_aqk, b_vnb)

        p_o = o + ((i_b * T + t * BT) * H + i_h) * V \
            + o_t[:, None] * (H * V) + o_bv[None, :]
        # o is never re-read: stream the store past L2
        tl.store(p_o, b_o.to(tl.bfloat16), cache_modifier=".cs")

        b_S = b_S * b_dn[:, None] + tl.dot(b_kgt, b_vnb)


# per-(NT, BH) tuned configs: NSEG, seg BX, scan BV
_CONFIGS = {
    (16, 16): dict(nseg=4, bx=64, bv=64, ons=2, sns=3),
    (32, 16): dict(nseg=4, bx=64, bv=64, ons=2, sns=3),
    (64, 8): dict(nseg=8, bx=64, bv=64, ons=2, sns=3),
    (32, 4): dict(nseg=8, bx=64, bv=32, ons=3, sns=3),
}


def _config(NT: int, BH: int) -> dict:
    cfg = _CONFIGS.get((NT, BH))
    if cfg is not None:
        return cfg
    nseg = 1
    while nseg < 8 and NT % (nseg * 2) == 0 and NT // (nseg * 2) >= 2 \
            and nseg * BH < 128:
        nseg *= 2
    return dict(nseg=nseg, bx=64, bv=64, ons=2, sns=3)


class Model(nn.Module):
    """KDA forward (chunk form) with custom Triton kernels."""

    def __init__(self, B: int, T: int, H: int, K: int, V: int, chunk_size: int = 64):
        super().__init__()
        self.B, self.T, self.H, self.K, self.V = B, T, H, K, V
        self.chunk_size = chunk_size
        self.scale = float(K) ** -0.5
        self.register_buffer("_dummy", torch.zeros(1), persistent=False)
        self._ws = None
        self._graph = None
        self._graph_key = None
        self._last_key = None
        self._repeats = 0

    def _workspaces(self, device):
        if self._ws is not None and self._ws[0] == device:
            return self._ws[1]
        B, T, H, K, V = self.B, self.T, self.H, self.K, self.V
        BT = self.chunk_size
        NT = T // BT
        BH = B * H
        cfg = _config(NT, BH)
        NSEG = cfg["nseg"]
        ws = {
            "w": torch.empty(BH * T * K, dtype=torch.bfloat16, device=device),
            "u": torch.empty(BH * T * V, dtype=torch.bfloat16, device=device),
            "qg": torch.empty(BH * T * K, dtype=torch.bfloat16, device=device),
            "kg": torch.empty(BH * T * K, dtype=torch.bfloat16, device=device),
            "aqk": torch.empty(BH * T * BT, dtype=torch.bfloat16, device=device),
            "dn": torch.empty(BH * NT * K, dtype=torch.float32, device=device),
            "mseg": torch.empty(BH * NSEG * K * K, dtype=torch.bfloat16, device=device),
            "cseg": torch.empty(BH * NSEG * K * V, dtype=torch.bfloat16, device=device),
            "o": torch.empty(B, T, H, V, dtype=torch.bfloat16, device=device),
            "cfg": cfg,
        }
        self._ws = (device, ws)
        return ws

    def _launch(self, q, k, v, g, beta, ws):
        B, T, H, K = q.shape
        V = v.shape[-1]
        BT = self.chunk_size
        NT = T // BT
        BH = B * H
        cfg = ws["cfg"]
        NSEG = cfg["nseg"]
        P = NT // NSEG

        _kda_prepare[(NT, BH)](
            q, k, v, g, beta,
            ws["w"], ws["u"], ws["qg"], ws["kg"], ws["aqk"], ws["dn"],
            self.scale, 0,
            T=T, H=H, K=K, V=V, BT=BT, NT=NT,
            num_warps=4, num_stages=1,
        )
        if NSEG > 1:
            BX = cfg["bx"]
            _kda_seg_op[((K + V) // BX, NSEG - 1, BH)](
                ws["w"], ws["u"], ws["kg"], ws["dn"], ws["mseg"], ws["cseg"],
                0,
                T=T, H=H, K=K, V=V, BT=BT, NT=NT, P=P, NSEG=NSEG, BX=BX,
                num_warps=4, num_stages=cfg["ons"],
            )
        BV = cfg["bv"]
        _kda_scan_seg[(V // BV, NSEG, BH)](
            ws["w"], ws["u"], ws["qg"], ws["kg"], ws["aqk"], ws["dn"],
            ws["mseg"], ws["cseg"], ws["o"], 0, 0,
            T=T, H=H, K=K, V=V, BT=BT, NT=NT, P=P, NSEG=NSEG, BV=BV,
            num_warps=4, num_stages=cfg["sns"],
        )

    def forward(
        self,
        q: torch.Tensor,
        k: torch.Tensor,
        v: torch.Tensor,
        g: torch.Tensor,
        beta: torch.Tensor,
    ) -> torch.Tensor:
        # Lean fast path: same buffers as the captured graph -> just replay.
        # (Matching pointers imply the same tensors we captured with, which
        # were already checked contiguous.)
        graph = self._graph
        if graph is not None and self._graph_key == (
                q.data_ptr(), k.data_ptr(), v.data_ptr(), g.data_ptr(),
                beta.data_ptr()):
            graph.replay()
            return self._graph_out

        if not q.is_contiguous():
            q = q.contiguous()
        if not k.is_contiguous():
            k = k.contiguous()
        if not v.is_contiguous():
            v = v.contiguous()
        if not g.is_contiguous():
            g = g.contiguous()
        if not beta.is_contiguous():
            beta = beta.contiguous()

        ws = self._workspaces(q.device)

        key = (q.data_ptr(), k.data_ptr(), v.data_ptr(), g.data_ptr(),
               beta.data_ptr())
        if key == self._last_key:
            self._repeats += 1
        else:
            self._last_key = key
            self._repeats = 0

        if self._repeats >= 2:
            try:
                graph = torch.cuda.CUDAGraph()
                s = torch.cuda.Stream()
                s.wait_stream(torch.cuda.current_stream())
                with torch.cuda.stream(s):
                    self._launch(q, k, v, g, beta, ws)
                torch.cuda.current_stream().wait_stream(s)
                with torch.cuda.graph(graph):
                    self._launch(q, k, v, g, beta, ws)
                self._graph = graph
                self._graph_key = key
                self._graph_out = ws["o"]
                graph.replay()
                return ws["o"]
            except Exception:
                self._graph = None
                self._graph_key = None

        self._launch(q, k, v, g, beta, ws)
        return ws["o"]


# Module-level shape shims (overridden by check.py / benchmark.py per shape).
B = 2
T = 1024
H = 8
K = 128
V = 128
CHUNK_SIZE = 64


def get_inputs():
    torch.manual_seed(0)
    q = torch.randn(B, T, H, K, dtype=torch.bfloat16) * 0.1
    k = torch.randn(B, T, H, K, dtype=torch.bfloat16) * 0.1
    v = torch.randn(B, T, H, V, dtype=torch.bfloat16) * 0.1
    g = (torch.randn(B, T, H, K, dtype=torch.float32) * 0.1 - 0.05)
    beta = torch.sigmoid(torch.randn(B, T, H, dtype=torch.bfloat16))
    return [q, k, v, g, beta]


def get_init_inputs():
    return [B, T, H, K, V, CHUNK_SIZE]

20260721_223835_or-fable_anthropic_claude-fable-5_02_kda_cutlass