File size: 17,488 Bytes
59630ba | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 | """Long-Term Memory (LTM) sparse attention processor.
Ported from ar_mem/framear/src/models/attention_processor.py with the STM (sliding
window dense) path removed: in Self-Forcing, STM is computed with flex_attention
outside this module, and only the LTM block-sparse output (``out_v``) flows
through ``LTMAttentionProcessor``.
The H100 SM90 block-sparse kernel is required at import time; non-H100 GPUs
will raise.
"""
import functools
import math
from dataclasses import dataclass
from typing import List, Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
major, minor = torch.cuda.get_device_capability(0)
if major == 9 and minor == 0:
from vsa.block_sparse_wrapper import block_sparse_attn_SM90
block_sparse_attn = block_sparse_attn_SM90
else:
raise RuntimeError(
f"LTMAttentionProcessor requires H100 (SM 9.0); got SM {major}.{minor}"
)
@dataclass
class LTMAttentionMetadata:
dit_seq_shape: List[int]
num_tiles: List[int]
total_seq_length: int
tile_partition_indices: torch.LongTensor
reverse_tile_partition_indices: torch.LongTensor
variable_block_sizes: torch.LongTensor
non_pad_index: torch.LongTensor
ltm_topk: int
@functools.lru_cache(maxsize=10)
def get_tile_partition_indices(
dit_seq_shape: Tuple[int, int, int],
tile_size: Tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
T, H, W = dit_seq_shape
ts, hs, ws = tile_size
indices = torch.arange(T * H * W, device=device, dtype=torch.long).reshape(T, H, W)
ls = []
for t in range(math.ceil(T / ts)):
for h in range(math.ceil(H / hs)):
for w in range(math.ceil(W / ws)):
ls.append(
indices[t * ts:min(t * ts + ts, T),
h * hs:min(h * hs + hs, H),
w * ws:min(w * ws + ws, W)].flatten()
)
return torch.cat(ls, dim=0)
@functools.lru_cache(maxsize=10)
def get_reverse_tile_partition_indices(
dit_seq_shape: Tuple[int, int, int],
tile_size: Tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
return torch.argsort(get_tile_partition_indices(dit_seq_shape, tile_size, device))
@functools.lru_cache(maxsize=10)
def construct_variable_block_sizes(
dit_seq_shape: Tuple[int, int, int],
num_tiles: Tuple[int, int, int],
ltm_tile_size: Tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
t, h, w = dit_seq_shape
ts_t, ts_h, ts_w = ltm_tile_size
n_t, n_h, n_w = num_tiles
def _sizes(dim_len: int, tile: int, n_tiles: int) -> torch.LongTensor:
sizes = torch.full((n_tiles,), tile, dtype=torch.int, device=device)
remainder = dim_len - (n_tiles - 1) * tile
sizes[-1] = remainder if remainder > 0 else tile
return sizes
t_sizes = _sizes(t, ts_t, n_t)
h_sizes = _sizes(h, ts_h, n_h)
w_sizes = _sizes(w, ts_w, n_w)
block_sizes = (
t_sizes[:, None, None] * h_sizes[None, :, None] * w_sizes[None, None, :]
).reshape(-1)
return block_sizes
@functools.lru_cache(maxsize=10)
def get_non_pad_index(variable_block_sizes: torch.LongTensor, max_block_size: int):
n_win = variable_block_sizes.shape[0]
device = variable_block_sizes.device
starts_pad = torch.arange(n_win, device=device) * max_block_size
index_pad = starts_pad[:, None] + torch.arange(max_block_size, device=device)[None, :]
index_mask = (
torch.arange(max_block_size, device=device)[None, :] < variable_block_sizes[:, None]
)
return index_pad[index_mask]
@functools.lru_cache(maxsize=10)
def build_ltm_metadata(
dit_seq_shape: Tuple[int, int, int],
ltm_topk: int,
ltm_tile_size: Tuple[int, int, int],
device: torch.device,
is_tf: bool = False
) -> LTMAttentionMetadata:
num_tiles = (
math.ceil(dit_seq_shape[0] / ltm_tile_size[0]),
math.ceil(dit_seq_shape[1] / ltm_tile_size[1]),
math.ceil(dit_seq_shape[2] / ltm_tile_size[2]),
)
total_seq_length = math.prod(dit_seq_shape)
tile_partition_indices = get_tile_partition_indices(dit_seq_shape, ltm_tile_size, device)
reverse_tile_partition_indices = get_reverse_tile_partition_indices(
dit_seq_shape, ltm_tile_size, device
)
variable_block_sizes = construct_variable_block_sizes(
dit_seq_shape, num_tiles, ltm_tile_size, device
)
non_pad_index = get_non_pad_index(variable_block_sizes, math.prod(ltm_tile_size))
if is_tf:
# offsets are DIFFERENT for the input-space vs output-space index tables:
# - tile_partition_indices / reverse_tile_partition_indices live in the
# unpadded INPUT space, whose second half starts at total_seq_length.
# - non_pad_index lives in the padded OUTPUT space (after tile()'s
# internal zero pad), whose second half starts at padded_total_seq_len.
single_side_unpadded_len = total_seq_length
single_side_padded_len = math.prod(num_tiles) * math.prod(ltm_tile_size)
num_tiles = (num_tiles[0] * 2, num_tiles[1], num_tiles[2])
total_seq_length = total_seq_length * 2
tile_partition_indices = torch.cat(
[tile_partition_indices, tile_partition_indices + single_side_unpadded_len], dim=0
)
reverse_tile_partition_indices = torch.cat(
[reverse_tile_partition_indices, reverse_tile_partition_indices + single_side_unpadded_len], dim=0
)
variable_block_sizes = torch.cat([variable_block_sizes, variable_block_sizes], dim=0)
non_pad_index = torch.cat(
[non_pad_index, non_pad_index + single_side_padded_len], dim=0
)
assert ltm_topk != -1, "ltm_topk must be set"
return LTMAttentionMetadata(
dit_seq_shape=dit_seq_shape,
num_tiles=num_tiles,
total_seq_length=total_seq_length,
tile_partition_indices=tile_partition_indices,
reverse_tile_partition_indices=reverse_tile_partition_indices,
variable_block_sizes=variable_block_sizes,
non_pad_index=non_pad_index,
ltm_topk=ltm_topk,
)
def generate_clean_noise_mask(clean_mask: torch.Tensor, num_frames: int) -> torch.Tensor:
"""Expand a (1,1,l,l) clean-only block mask to a (1,1,2l,2l) interleaved
clean/noisy mask matching ar_mem's teacher-forcing convention.
"""
clean_mask_extend = torch.cat(
[clean_mask, torch.zeros_like(clean_mask)], dim=-1
)
_, _, seq_len = clean_mask.shape
token_per_frame = seq_len // num_frames
idx = torch.arange(seq_len, device=clean_mask.device)
row_idx = idx.unsqueeze(1)
col_idx = idx.unsqueeze(0)
lower = (row_idx // token_per_frame > col_idx // token_per_frame)
diag = (row_idx // token_per_frame == col_idx // token_per_frame)
noise_mask_right = torch.where(diag, clean_mask, torch.zeros_like(clean_mask))
noise_mask_left = torch.where(lower, clean_mask, torch.zeros_like(clean_mask))
noise_mask_extend = torch.cat([noise_mask_left, noise_mask_right], dim=-1)
return torch.cat([clean_mask_extend, noise_mask_extend], dim=1)
@functools.lru_cache(maxsize=10)
def prepare_block_causal_mask(
q_block: int,
kv_block: int,
num_tiles_per_block: int,
enable_clean_noise_mask: bool,
device_str: str,
dtype: torch.dtype = torch.bfloat16,
) -> torch.Tensor:
"""LTM coarse-block mask.
Allowed (=0) positions: ego block (same frame) or kv frame at least
``sliding_window`` frames earlier than q frame. Everything else is -10000.
Output shape: (1, 1, L, L) where L is the number of coarse blocks.
"""
device = torch.device(device_str)
q_frames = q_block // num_tiles_per_block
frame_indices = torch.arange(q_frames, device=device).repeat_interleave(num_tiles_per_block)
diff = frame_indices.unsqueeze(1) - frame_indices.unsqueeze(0)
block_mask = (diff >= 0).float()
if enable_clean_noise_mask:
block_mask = generate_clean_noise_mask(block_mask.unsqueeze(0), q_frames)
block_mask = (1 - block_mask) * -10000.0
return block_mask.unsqueeze(0).to(dtype)
# @torch.compile
def compress_block(x: torch.Tensor, block_size: int, variable_sizes: torch.Tensor) -> torch.Tensor:
B, H, L, D = x.shape
x_reshaped = x.view(B, H, L // block_size, block_size, D)
return (x_reshaped.float().sum(dim=3) / variable_sizes.view(1, 1, -1, 1)).to(x.dtype)
def _coarse_attention(q, k, v, attn_mask) -> Tuple[torch.Tensor, torch.Tensor]:
QK = torch.matmul(q, k.transpose(-2, -1)) / (q.size(-1) ** 0.5)
if attn_mask is not None:
QK = QK + attn_mask
QK = torch.nn.functional.softmax(QK, dim=-1)
return torch.matmul(QK, v), QK
class LTMAttentionProcessor(nn.Module):
"""LTM sparse attention processor.
Forward returns the sparse (long-term) attention output ``out_v`` shaped
``(B, H, L, D)`` in the original (untiled) spatial order. The caller is
responsible for computing the STM/dense path separately and combining the
two (e.g., ``out_s + mem_proj(x) * out_v``).
"""
def __init__(self, ltm_tile_size: Tuple[int, int, int] = (1, 8, 8)):
super().__init__()
self.ltm_tile_size = ltm_tile_size
def tile(self, x, num_tiles, tile_partition_indices, non_pad_index):
t_pad = num_tiles[0] * self.ltm_tile_size[0]
h_pad = num_tiles[1] * self.ltm_tile_size[1]
w_pad = num_tiles[2] * self.ltm_tile_size[2]
x_padded = torch.zeros(
(x.shape[0], x.shape[1], t_pad * h_pad * w_pad, x.shape[-1]),
device=x.device, dtype=x.dtype,
)
x_padded[:, :, non_pad_index] = x[:, :, tile_partition_indices]
return x_padded
def untile(self, x, reverse_tile_partition_indices, non_pad_index):
return x[:, :, non_pad_index][:, :, reverse_tile_partition_indices]
def preprocess_qkv(self, qkv, ltm_metadata):
return self.tile(
qkv,
ltm_metadata.num_tiles,
ltm_metadata.tile_partition_indices,
ltm_metadata.non_pad_index,
)
def postprocess_output(self, output, ltm_metadata):
return self.untile(
output,
ltm_metadata.reverse_tile_partition_indices,
ltm_metadata.non_pad_index,
)
def _merge_current_kv_into_cache(
self,
key: torch.Tensor,
value: torch.Tensor,
ltm_metadata: LTMAttentionMetadata,
kv_cache: Optional[dict],
write_cache: bool,
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Tile/compress the current chunk only, then concat with cached KV."""
block_elements = math.prod(self.ltm_tile_size)
variable_block_sizes_cur = ltm_metadata.variable_block_sizes
k_cur = self.tile(
key,
ltm_metadata.num_tiles,
ltm_metadata.tile_partition_indices,
ltm_metadata.non_pad_index,
)
v_cur = self.tile(
value,
ltm_metadata.num_tiles,
ltm_metadata.tile_partition_indices,
ltm_metadata.non_pad_index,
)
k_comp_cur = compress_block(k_cur, block_elements, variable_block_sizes_cur)
v_comp_cur = compress_block(v_cur, block_elements, variable_block_sizes_cur)
if kv_cache is not None and kv_cache.get("k_tiled") is not None:
k_tiled = torch.cat([kv_cache["k_tiled"], k_cur], dim=2)
v_tiled = torch.cat([kv_cache["v_tiled"], v_cur], dim=2)
k_compress = torch.cat([kv_cache["k_compress"], k_comp_cur], dim=2)
v_compress = torch.cat([kv_cache["v_compress"], v_comp_cur], dim=2)
variable_block_sizes_kv = torch.cat(
[kv_cache["variable_block_sizes"], variable_block_sizes_cur], dim=0
)
else:
k_tiled, v_tiled = k_cur, v_cur
k_compress, v_compress = k_comp_cur, v_comp_cur
variable_block_sizes_kv = variable_block_sizes_cur
if write_cache and kv_cache is not None:
kv_cache["k_tiled"] = k_tiled
kv_cache["v_tiled"] = v_tiled
kv_cache["k_compress"] = k_compress
kv_cache["v_compress"] = v_compress
kv_cache["variable_block_sizes"] = variable_block_sizes_kv
return k_tiled, v_tiled, k_compress, v_compress, variable_block_sizes_kv
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
ltm_metadata: LTMAttentionMetadata,
enable_clean_noise_mask: bool,
num_frame_per_block: int,
kv_cache: Optional[dict] = None,
write_cache: bool = True,
apply_block_causal_mask: bool = True,
) -> torch.Tensor:
"""Args
----
query, key, value : (B, H, L, D) with L == prod(dit_seq_shape).
In teacher-forcing mode they should arrive batch-concatenated as
``(2B, H, L, D)`` (clean half then noisy half); when
``enable_clean_noise_mask=True`` they are internally reshuffled
into ``(B, H, 2L, D)``.
key, value : at inference, only the **current** block (not full history).
History is read from ``kv_cache`` (tiled + compressed tokens).
kv_cache : optional per-layer cache with keys
``k_tiled``, ``v_tiled``, ``k_compress``, ``v_compress``,
``variable_block_sizes``. Updated when ``write_cache=True``.
apply_block_causal_mask : if False, coarse top-k uses no block mask (inference;
causality is already enforced by the KV cache schedule).
Returns
-------
out_v : same shape as ``query`` (after the optional TF unshuffle), in
the original spatial order (un-tiled).
"""
block_elements = math.prod(self.ltm_tile_size)
# Spatial tiles per video frame, INDEPENDENT of TF doubling (num_tiles[0]
# may have been doubled in build_ltm_metadata(is_tf=True), but [1:] is not).
tiles_per_frame = math.prod(ltm_metadata.num_tiles[1:])
num_tiles_per_block = tiles_per_frame * num_frame_per_block
# 1) Tile Q (current block only)
q_tiled = self.tile(
query,
ltm_metadata.num_tiles,
ltm_metadata.tile_partition_indices,
ltm_metadata.non_pad_index,
)
# 2) Tile/compress current K/V and merge with cached history
k_tiled, v_tiled, k_compress, v_compress, variable_block_sizes_kv = (
self._merge_current_kv_into_cache(
key, value, ltm_metadata, kv_cache, write_cache
)
)
B, H, _, D = q_tiled.shape
q_blocks_total = q_tiled.shape[2] // block_elements
k_blocks_total = k_tiled.shape[2] // block_elements
if enable_clean_noise_mask:
assert q_blocks_total % 2 == 0 and k_blocks_total % 2 == 0
q_blocks = q_blocks_total // 2
k_blocks = k_blocks_total // 2
else:
q_blocks = q_blocks_total
k_blocks = k_blocks_total
variable_block_sizes_q = ltm_metadata.variable_block_sizes
# 3) Compress Q blocks (K/V compressed cache built incrementally above)
q_compress = compress_block(q_tiled, block_elements, variable_block_sizes_q)
# 4) Coarse block attention (+ optional LTM block-causal mask for training)
if apply_block_causal_mask:
block_causal_mask = prepare_block_causal_mask(
q_block=q_blocks, kv_block=k_blocks,
num_tiles_per_block=num_tiles_per_block,
enable_clean_noise_mask=enable_clean_noise_mask,
device_str=str(query.device),
dtype=query.dtype,
)
else:
block_causal_mask = None
_, block_attn_score = _coarse_attention(q_compress, k_compress, v_compress, block_causal_mask)
# 5) Top-k block selection + causality fix-up
topk = min(ltm_metadata.ltm_topk, block_attn_score.shape[-1])
if block_causal_mask is not None:
scored = block_attn_score + block_causal_mask
topk_indices = torch.topk(scored, topk, dim=-1).indices
block_mask = torch.zeros_like(scored, dtype=torch.bool).scatter_(-1, topk_indices, True)
block_mask &= (block_causal_mask >= -1e-3)
else:
topk_indices = torch.topk(block_attn_score, topk, dim=-1).indices
block_mask = torch.zeros_like(block_attn_score, dtype=torch.bool).scatter_(
-1, topk_indices, True
)
# 6) Block-sparse fine attention (H100 SM90 kernel)
if not apply_block_causal_mask and q_tiled.shape[0] == 2:
output_select = torch.empty_like(q_tiled, dtype=q_tiled.dtype, device=q_tiled.device)
for i in range(2): # batch size must be 2
output_select[i:i+1] = block_sparse_attn(q_tiled[i:i+1], k_tiled[i:i+1], v_tiled[i:i+1], block_mask[i:i+1], \
variable_block_sizes_kv, variable_block_sizes_q)[0]
else:
output_select, _ = block_sparse_attn(
q_tiled, k_tiled, v_tiled, block_mask,
variable_block_sizes_kv, variable_block_sizes_q,
)
return self.postprocess_output(output_select, ltm_metadata)
|