Old project/workflow names and browser settings migrate on load; /funpack redirects to /fun. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016acRss69eH1235eRdFTZHY
168 lines
5.9 KiB
Python
168 lines
5.9 KiB
Python
"""Block-sparse attention forward kernel for MiniMax-H3.
|
|
|
|
Vendored into FunUI from ComfyUI-H3-SLA-Attention, which vendored it from
|
|
LightX2V (Apache-2.0). The provenance note and change list below are the
|
|
original author's and still describe this file exactly.
|
|
|
|
Vendored and reduced from LightX2V (Apache-2.0):
|
|
lightx2v/common/ops/attn/kernels/sla_kernel_ar.py -- ``_attn_fwd``
|
|
https://github.com/ModelTC/LightX2V
|
|
|
|
Only the forward pass survives the port; sampling runs under ``no_grad`` so the
|
|
backward half and the ``autograd.Function`` wrapper are dead weight here.
|
|
|
|
The ``_ar`` name upstream refers to the models it was written for, not to any
|
|
causal structure: the kernel walks whatever key blocks the lookup table names
|
|
and applies no triangular mask. That is what makes it usable for H3, whose
|
|
packed ``[text | cond/ref | audio | video]`` sequence is fully bidirectional.
|
|
|
|
Layout is BLHD -- ``(B, L, H, D)`` -- which is deliberate. H3 materialises q/k/v
|
|
as ``[S, H, D]`` and only then transposes to ``[1, H, S, D]`` for the attention
|
|
call, so transposing back is free, while a BHSD kernel would force a real
|
|
``.contiguous()`` copy costing ~1.3 GB per tensor at 768p/15s.
|
|
|
|
Two changes against upstream, both marked FIX below: masked loads now pass
|
|
``other=0.0``, because Triton leaves masked lanes undefined and ``0 * NaN`` is
|
|
NaN, not zero -- the sequence-tail block can poison a whole row otherwise.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import torch
|
|
import triton
|
|
import triton.language as tl
|
|
|
|
|
|
@triton.jit
|
|
def _attn_fwd(
|
|
Q,
|
|
K,
|
|
V,
|
|
qk_scale: tl.constexpr,
|
|
topk: tl.constexpr,
|
|
LUT,
|
|
OS,
|
|
H: tl.constexpr,
|
|
LQ: tl.constexpr,
|
|
LK: tl.constexpr,
|
|
M_BLOCKS: tl.constexpr,
|
|
D: tl.constexpr,
|
|
BLOCK_M: tl.constexpr,
|
|
BLOCK_N: tl.constexpr,
|
|
):
|
|
idx_m = tl.program_id(0).to(tl.int64)
|
|
idx_bh = tl.program_id(1).to(tl.int64)
|
|
|
|
idx_b = idx_bh // H
|
|
idx_h = idx_bh % H
|
|
|
|
HD: tl.constexpr = H * D
|
|
|
|
# Q/K/V/O: (B, L, H, D) contiguous.
|
|
q_offset = idx_b * LQ * HD + idx_h * D
|
|
kv_offset = idx_b * LK * HD + idx_h * D
|
|
lut_offset = (idx_bh * M_BLOCKS + idx_m) * topk
|
|
|
|
offs_m = idx_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
|
offs_n = tl.arange(0, BLOCK_N)
|
|
offs_d = tl.arange(0, D)
|
|
|
|
Q_ptrs = Q + q_offset + offs_m[:, None] * HD + offs_d[None, :]
|
|
OS_ptrs = OS + q_offset + offs_m[:, None] * HD + offs_d[None, :]
|
|
LUT_ptr = LUT + lut_offset
|
|
|
|
m_i = tl.full([BLOCK_M], -float("inf"), dtype=tl.float32)
|
|
l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
|
|
o_s = tl.zeros([BLOCK_M, D], dtype=tl.float32)
|
|
|
|
# FIX vs upstream: other=0.0 on every masked load.
|
|
q = tl.load(Q_ptrs, mask=offs_m[:, None] < LQ, other=0.0)
|
|
for block_idx in tl.range(topk):
|
|
idx_n = tl.load(LUT_ptr + block_idx).to(tl.int64)
|
|
k_start = idx_n * BLOCK_N
|
|
k_mask = (k_start + offs_n) < LK
|
|
|
|
K_ptrs = K + kv_offset + (k_start + offs_n)[None, :] * HD + offs_d[:, None]
|
|
V_ptrs = V + kv_offset + (k_start + offs_n)[:, None] * HD + offs_d[None, :]
|
|
|
|
k = tl.load(K_ptrs, mask=k_mask[None, :], other=0.0)
|
|
qk = tl.dot(q, k) * (qk_scale * 1.4426950408889634) # 1/ln(2), for exp2
|
|
qk = tl.where(k_mask[None, :], qk, float("-inf"))
|
|
|
|
v = tl.load(V_ptrs, mask=k_mask[:, None], other=0.0)
|
|
local_m = tl.max(qk, 1)
|
|
new_m = tl.maximum(m_i, local_m)
|
|
qk = qk - new_m[:, None]
|
|
|
|
p = tl.math.exp2(qk)
|
|
l_ij = tl.sum(p, 1)
|
|
alpha = tl.math.exp2(m_i - new_m)
|
|
o_s = o_s * alpha[:, None]
|
|
o_s += tl.dot(p.to(v.dtype), v)
|
|
|
|
l_i = l_i * alpha + l_ij
|
|
m_i = new_m
|
|
|
|
o_s = o_s / l_i[:, None]
|
|
tl.store(OS_ptrs, o_s.to(OS.type.element_ty), mask=offs_m[:, None] < LQ)
|
|
|
|
|
|
# (BLOCK_M, BLOCK_N) -> ladder of (num_warps, num_stages) to try, best first.
|
|
#
|
|
# These are not upstream's numbers. LightX2V hardcodes num_warps=4, num_stages=3,
|
|
# which on sm_120 needs 160 KB of shared memory against a 99 KB limit -- it does
|
|
# not merely run slowly, it fails to launch. Consumer Blackwell has less shared
|
|
# memory than the datacentre parts these kernels were tuned on, so we probe and
|
|
# fall back. Measured on a 5090 at S=32768, H=56, D=128 (ms, lower better):
|
|
# 128x64 w8 s3 -> 22.4 128x64 w4 s3 -> 23.9 128x128 w8 s2 -> 25.4
|
|
# 64x64 w4 s1 -> 26.8 64x128 w4 s2 -> 30.5 128x128 w4 s1 -> 40.5
|
|
_LADDER = {
|
|
(128, 64): ((8, 3), (4, 3), (8, 2), (4, 1)),
|
|
(128, 128): ((8, 2), (4, 2), (8, 1), (4, 1)),
|
|
(64, 128): ((4, 2), (8, 2), (4, 1)),
|
|
(64, 64): ((4, 1), (4, 3), (8, 3), (8, 1)),
|
|
# Not measured here: ComfyUI-H3-SLA-Attention's order, which measured 32x32 as fast as 64x64.
|
|
(32, 32): ((4, 2), (2, 1), (4, 1), (2, 2)),
|
|
}
|
|
_CHOSEN: dict = {}
|
|
|
|
|
|
def block_sparse_attention(q, k, v, lut, topk, BLOCK_M, BLOCK_N, qk_scale=None):
|
|
"""Attend each query block to only the key blocks named in ``lut``.
|
|
|
|
``q``/``k``/``v`` are ``(B, L, H, D)`` contiguous; ``lut`` is
|
|
``(B, H, M_BLOCKS, topk)`` int32, contiguous. Returns ``(B, L, H, D)``.
|
|
"""
|
|
assert q.is_contiguous() and k.is_contiguous() and v.is_contiguous()
|
|
assert lut.is_contiguous()
|
|
assert BLOCK_M in (32, 64, 128) and BLOCK_N in (32, 64, 128)
|
|
|
|
B, LQ, H, D = q.shape
|
|
LK = k.shape[1]
|
|
if qk_scale is None:
|
|
qk_scale = D**-0.5
|
|
|
|
M_BLOCKS = triton.cdiv(LQ, BLOCK_M)
|
|
o_s = torch.empty_like(q)
|
|
grid = (M_BLOCKS, B * H)
|
|
|
|
key = (BLOCK_M, BLOCK_N, D)
|
|
ladder = (_CHOSEN[key],) if key in _CHOSEN else _LADDER[(BLOCK_M, BLOCK_N)]
|
|
|
|
last = None
|
|
for cfg in ladder:
|
|
num_warps, num_stages = cfg
|
|
try:
|
|
_attn_fwd[grid](
|
|
q, k, v, qk_scale, topk, lut, o_s,
|
|
H, LQ, LK, M_BLOCKS, D, BLOCK_M, BLOCK_N,
|
|
num_warps=num_warps, num_stages=num_stages,
|
|
)
|
|
except triton.runtime.errors.OutOfResources as exc:
|
|
last = exc
|
|
continue
|
|
_CHOSEN[key] = cfg
|
|
return o_s
|
|
|
|
raise last if last is not None else RuntimeError("no viable launch config")
|