exp10a: AOTI diag endpoint
Browse files- aoti_attention.py +133 -0
- app.py +126 -1
aoti_attention.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AOTI (Ahead-Of-Time Inductor) support for the VSA-H3 sparse attention soup.
|
| 2 |
+
|
| 3 |
+
Phase A of the AOTI experiment (torch 2.11.0+cu130 on ZeroGPU, where AOTI is officially supported):
|
| 4 |
+
|
| 5 |
+
1. The two raw Triton kernels the sparse path depends on — `map_to_index` and
|
| 6 |
+
`triton_block_sparse_attn_forward` — are wrapped as `torch.library` custom ops with fake
|
| 7 |
+
implementations, which is what lets `torch.export` trace through them.
|
| 8 |
+
2. `sparse_attention_functional` reimplements `vsa_h3.sparse_attention` as a *pure tensor function*:
|
| 9 |
+
op-for-op the same math (fresh tile buffers, fp32 pooled scores, prefix-exempt top-k mask,
|
| 10 |
+
sparse kernel, untile, compression branch with the trained per-row gate), but the cached
|
| 11 |
+
`VSAGeometry` tensors arrive as plain arguments. No weights, no globals — small artifacts.
|
| 12 |
+
3. `/aoti_diag` on the Space (a) checks the functional reimplementation against the eager
|
| 13 |
+
reference on the real bench geometry, and (b) when `H3_AOTI=1`, exports + AOTI-compiles the
|
| 14 |
+
function for this GPU and checks the compiled artifact against eager, with timings.
|
| 15 |
+
|
| 16 |
+
The compiled function is only valid for one static layout (the frozen benchmark spec's). Any other
|
| 17 |
+
canvas falls back to the eager path.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import math
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
|
| 26 |
+
_LIB = None # registered-once torch.library fragment
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _ns() -> torch.library.Library:
|
| 30 |
+
"""Register the custom ops once and return the library fragment."""
|
| 31 |
+
global _LIB
|
| 32 |
+
if _LIB is not None:
|
| 33 |
+
return _LIB
|
| 34 |
+
|
| 35 |
+
from vsa_kernel import map_to_index as _map_to_index
|
| 36 |
+
from vsa_kernel import triton_block_sparse_attn_forward as _fwd
|
| 37 |
+
|
| 38 |
+
lib = torch.library.Library("fasth3", "FRAGMENT")
|
| 39 |
+
|
| 40 |
+
def map_fake(block_map: torch.Tensor):
|
| 41 |
+
return (
|
| 42 |
+
torch.empty_like(block_map, dtype=torch.int32),
|
| 43 |
+
torch.empty(block_map.shape[:-1], dtype=torch.int32, device=block_map.device),
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
def fwd_fake(q, k, v, q2k_index, q2k_num, vbs):
|
| 47 |
+
return (
|
| 48 |
+
torch.empty_like(q),
|
| 49 |
+
torch.empty(q.shape[:3], dtype=torch.float32, device=q.device),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
lib.define("map_to_index(Tensor block_map) -> (Tensor, Tensor)")
|
| 53 |
+
lib.impl("map_to_index", _map_to_index, "CUDA")
|
| 54 |
+
lib._register_fake("map_to_index", map_fake)
|
| 55 |
+
|
| 56 |
+
lib.define(
|
| 57 |
+
"block_sparse_fwd(Tensor q, Tensor k, Tensor v, Tensor q2k_index, Tensor q2k_num, "
|
| 58 |
+
"Tensor vbs) -> (Tensor, Tensor)"
|
| 59 |
+
)
|
| 60 |
+
lib.impl("block_sparse_fwd", _fwd, "CUDA")
|
| 61 |
+
lib._register_fake("block_sparse_fwd", fwd_fake)
|
| 62 |
+
|
| 63 |
+
_LIB = lib
|
| 64 |
+
return lib
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def sparse_attention_functional(
|
| 68 |
+
query: torch.Tensor,
|
| 69 |
+
key: torch.Tensor,
|
| 70 |
+
value: torch.Tensor,
|
| 71 |
+
gate: torch.Tensor | None,
|
| 72 |
+
untile_index: torch.Tensor,
|
| 73 |
+
variable_block_sizes: torch.Tensor,
|
| 74 |
+
tile_divisor: torch.Tensor,
|
| 75 |
+
topk: int,
|
| 76 |
+
num_prefix_tiles: int,
|
| 77 |
+
) -> torch.Tensor:
|
| 78 |
+
"""`vsa_h3.sparse_attention` with every cached buffer inlined as an argument.
|
| 79 |
+
|
| 80 |
+
Op-for-op the same math as the eager path: fp32 pooled tile scores, top-k video tiles with the
|
| 81 |
+
prefix exempt (prefix key tiles always selected, prefix query tiles dense), the sparse kernel,
|
| 82 |
+
untiling, then the compression branch scaled by the trained per-row gate. `gate` is
|
| 83 |
+
`[B, H, S_real, D]` (the eager path's `gate.transpose(1, 2)`), applied after untililing.
|
| 84 |
+
"""
|
| 85 |
+
batch, heads, padded_len, dim = query.shape
|
| 86 |
+
n_tiles = variable_block_sizes.numel()
|
| 87 |
+
n_real = untile_index.numel()
|
| 88 |
+
num_q_video = n_tiles - num_prefix_tiles
|
| 89 |
+
|
| 90 |
+
# Tile: scatter the packed rows into the kernels' `[B, H, S_pad, D]` layout, pad slots zero.
|
| 91 |
+
query_tiled = torch.index_copy(
|
| 92 |
+
torch.zeros((batch, heads, padded_len, dim), dtype=query.dtype, device=query.device),
|
| 93 |
+
2, untile_index, query.transpose(1, 2),
|
| 94 |
+
)
|
| 95 |
+
key_tiled = torch.index_copy(
|
| 96 |
+
torch.zeros_like(query_tiled), 2, untile_index, key.transpose(1, 2),
|
| 97 |
+
)
|
| 98 |
+
value_tiled = torch.index_copy(
|
| 99 |
+
torch.zeros_like(query_tiled), 2, untile_index, value.transpose(1, 2),
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Per-head fp32 pooled tile scores.
|
| 103 |
+
q_pool = query_tiled.view(batch, heads, n_tiles, 64, dim).sum(dim=3, dtype=torch.float32) / tile_divisor
|
| 104 |
+
k_pool = key_tiled.view(batch, heads, n_tiles, 64, dim).sum(dim=3, dtype=torch.float32) / tile_divisor
|
| 105 |
+
scores = torch.matmul(q_pool, k_pool.transpose(-2, -1)) / math.sqrt(dim)
|
| 106 |
+
|
| 107 |
+
# Prefix-exempt top-k mask, assembled from fresh tensors (functional, export-traceable).
|
| 108 |
+
video_scores = scores[..., num_prefix_tiles:, num_prefix_tiles:]
|
| 109 |
+
indices = video_scores.topk(topk, dim=-1, sorted=False).indices
|
| 110 |
+
mask_video = torch.zeros_like(video_scores, dtype=torch.bool).scatter(-1, indices, True)
|
| 111 |
+
prefix_cols = torch.zeros_like(video_scores, dtype=torch.bool)
|
| 112 |
+
prefix_cols[..., :num_prefix_tiles] = True
|
| 113 |
+
mask_video = mask_video | prefix_cols
|
| 114 |
+
mask = torch.cat(
|
| 115 |
+
[
|
| 116 |
+
torch.ones((batch, heads, num_prefix_tiles, n_tiles), dtype=torch.bool, device=query.device),
|
| 117 |
+
mask_video,
|
| 118 |
+
],
|
| 119 |
+
dim=2,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
q2k_index, q2k_num = torch.ops.fasth3.map_to_index(mask)
|
| 123 |
+
out_tiled, _ = torch.ops.fasth3.block_sparse_fwd(
|
| 124 |
+
query_tiled, key_tiled, value_tiled, q2k_index, q2k_num, variable_block_sizes
|
| 125 |
+
)
|
| 126 |
+
out = out_tiled.index_select(2, untile_index)
|
| 127 |
+
|
| 128 |
+
if gate is not None:
|
| 129 |
+
v_pool = value_tiled.view(batch, heads, n_tiles, 64, dim).sum(dim=3, dtype=torch.float32) / tile_divisor
|
| 130 |
+
pooled = torch.matmul(torch.softmax(scores, dim=-1), v_pool).to(out.dtype)
|
| 131 |
+
out = out + pooled.index_select(2, untile_index // 64) * gate
|
| 132 |
+
|
| 133 |
+
return out.transpose(1, 2)
|
app.py
CHANGED
|
@@ -427,6 +427,129 @@ def _generate(prompt_embeds, text_token_tags, height: int, width: int, num_frame
|
|
| 427 |
)
|
| 428 |
|
| 429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
@spaces.GPU(duration=120, size=GPU_SIZE)
|
| 431 |
def selftest() -> str:
|
| 432 |
"""Check the vendored VSA-H3 kernels against dense attention on this GPU.
|
|
@@ -710,9 +833,11 @@ with gr.Blocks(title="FastH3 v1 (VSA)") as demo:
|
|
| 710 |
demo.load(status, None, banner, api_name="status")
|
| 711 |
|
| 712 |
# No UI, API only: the sparse-attention equivalence check, so the kernel can be verified on this pool without
|
| 713 |
-
# spending a full generation.
|
| 714 |
diagnose = gr.Button(visible=False)
|
| 715 |
diagnose.click(selftest, None, gr.Markdown(visible=False), api_name="selftest")
|
|
|
|
|
|
|
| 716 |
|
| 717 |
|
| 718 |
if __name__ == "__main__":
|
|
|
|
| 427 |
)
|
| 428 |
|
| 429 |
|
| 430 |
+
@spaces.GPU(duration=300, size=GPU_SIZE)
|
| 431 |
+
def aoti_diag() -> str:
|
| 432 |
+
"""Phase A of the AOTI experiment: validate the traceable functional reimplementation of the VSA
|
| 433 |
+
sparse-attention soup, and (when `H3_AOTI=1`) export + AOTI-compile it for this GPU and check the
|
| 434 |
+
artifact against eager, with timings.
|
| 435 |
+
|
| 436 |
+
Uses the *real bench geometry* (1344x768, 124 frames, 203 text + 414 audio + 37 296 video rows),
|
| 437 |
+
synthesized directly — no generation, no weights.
|
| 438 |
+
"""
|
| 439 |
+
import torch
|
| 440 |
+
|
| 441 |
+
import aoti_attention
|
| 442 |
+
|
| 443 |
+
device = torch.device("cuda")
|
| 444 |
+
heads, dim = 56, 128
|
| 445 |
+
text_rows, audio_rows = 203, 414
|
| 446 |
+
grid_t, grid_h, grid_w = 37, 24, 42
|
| 447 |
+
n_video = grid_t * grid_h * grid_w
|
| 448 |
+
seq_len = text_rows + audio_rows + n_video
|
| 449 |
+
|
| 450 |
+
tags = torch.tensor([1] * text_rows + [2] * audio_rows + [0] * n_video, dtype=torch.long)
|
| 451 |
+
position_ids = torch.zeros(seq_len, 3, dtype=torch.float64)
|
| 452 |
+
video_start = text_rows + audio_rows
|
| 453 |
+
frame = torch.cartesian_prod(torch.arange(float(grid_h)), torch.arange(float(grid_w)))
|
| 454 |
+
position_ids[video_start:, 0] = torch.arange(float(grid_t)).repeat_interleave(grid_h * grid_w)
|
| 455 |
+
position_ids[video_start:, 1:] = frame.repeat(grid_t, 1)
|
| 456 |
+
|
| 457 |
+
import vsa_h3
|
| 458 |
+
|
| 459 |
+
geometry = vsa_h3.geometry_from_layout(tags, position_ids, 0.9)
|
| 460 |
+
if geometry is None:
|
| 461 |
+
return "**FAILED**: synthetic bench layout did not yield VSA geometry."
|
| 462 |
+
|
| 463 |
+
def timed(fn, *args, repeats=3):
|
| 464 |
+
fn(*args) # warm (JIT/compile)
|
| 465 |
+
torch.cuda.synchronize()
|
| 466 |
+
import time as _t
|
| 467 |
+
|
| 468 |
+
started = _t.perf_counter()
|
| 469 |
+
for _ in range(repeats):
|
| 470 |
+
fn(*args)
|
| 471 |
+
torch.cuda.synchronize()
|
| 472 |
+
return (_t.perf_counter() - started) / repeats
|
| 473 |
+
|
| 474 |
+
generator = torch.Generator(device=device).manual_seed(0)
|
| 475 |
+
shape = (1, seq_len, heads, dim)
|
| 476 |
+
q, k, v = (torch.randn(shape, generator=generator, device=device, dtype=torch.bfloat16) for _ in range(3))
|
| 477 |
+
gate = torch.randn(shape, generator=generator, device=device, dtype=torch.bfloat16) * 0.05
|
| 478 |
+
|
| 479 |
+
lines = [f"`{torch.cuda.get_device_name()}` · seq {seq_len}, padded {geometry.padded_len}, "
|
| 480 |
+
f"tiles {geometry.n_tiles}, topk {geometry.topk}", ""]
|
| 481 |
+
|
| 482 |
+
def eager(gate_h):
|
| 483 |
+
return vsa_h3.sparse_attention(q, k, v, gate_h.transpose(1, 2), geometry)
|
| 484 |
+
|
| 485 |
+
def functional(gate_h):
|
| 486 |
+
return aoti_attention.sparse_attention_functional(
|
| 487 |
+
q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), gate_h.transpose(1, 2),
|
| 488 |
+
geometry.untile_index, geometry.variable_block_sizes, geometry.tile_divisor,
|
| 489 |
+
geometry.topk, geometry.num_prefix_tiles,
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
for label, gate_h in (("gate=None", None), ("gate=rand*0.05", gate)):
|
| 493 |
+
ref, got = eager(gate_h), functional(gate_h)
|
| 494 |
+
error = (ref.float() - got.float()).abs().max().item()
|
| 495 |
+
scale = ref.float().abs().max().item()
|
| 496 |
+
cosine = torch.nn.functional.cosine_similarity(
|
| 497 |
+
ref.float().flatten(), got.float().flatten(), dim=0
|
| 498 |
+
).item()
|
| 499 |
+
t_eager = timed(eager, gate_h)
|
| 500 |
+
t_fn = timed(functional, gate_h)
|
| 501 |
+
lines.append(
|
| 502 |
+
f"| {label} | eager {t_eager * 1000:.0f} ms | functional {t_fn * 1000:.0f} ms | "
|
| 503 |
+
f"rel err {error / scale:.2e} | cosine {cosine:.6f} |"
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
report = (
|
| 507 |
+
"VSA soup: eager vs functional (op-for-op reimplementation)\n\n"
|
| 508 |
+
"| case | eager | functional | rel err | cosine |\n|---|---|---|---|---|\n" + "\n".join(lines[2:])
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
if os.environ.get("H3_AOTI") == "1":
|
| 512 |
+
try:
|
| 513 |
+
from torch._inductor import aoti_compile_and_package, aoti_load_package
|
| 514 |
+
|
| 515 |
+
with torch.no_grad():
|
| 516 |
+
exported = torch.export.export(
|
| 517 |
+
aoti_attention.sparse_attention_functional,
|
| 518 |
+
args=(
|
| 519 |
+
q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2),
|
| 520 |
+
gate.transpose(1, 2), geometry.untile_index,
|
| 521 |
+
geometry.variable_block_sizes, geometry.tile_divisor,
|
| 522 |
+
),
|
| 523 |
+
kwargs={"topk": geometry.topk, "num_prefix_tiles": geometry.num_prefix_tiles},
|
| 524 |
+
)
|
| 525 |
+
package = "/tmp/vsa_sparse_aoti.pt2"
|
| 526 |
+
aoti_compile_and_package(exported, package_path=package)
|
| 527 |
+
compiled = aoti_load_package(package)
|
| 528 |
+
got = compiled(
|
| 529 |
+
q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), gate.transpose(1, 2),
|
| 530 |
+
geometry.untile_index, geometry.variable_block_sizes, geometry.tile_divisor,
|
| 531 |
+
)
|
| 532 |
+
ref = eager(gate)
|
| 533 |
+
error = (ref.float() - got.float()).abs().max().item()
|
| 534 |
+
scale = ref.float().abs().max().item()
|
| 535 |
+
cosine = torch.nn.functional.cosine_similarity(
|
| 536 |
+
ref.float().flatten(), got.float().flatten(), dim=0
|
| 537 |
+
).item()
|
| 538 |
+
t_eager = timed(eager, gate)
|
| 539 |
+
t_compiled = timed(lambda *a: compiled(*a), (
|
| 540 |
+
q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), gate.transpose(1, 2),
|
| 541 |
+
geometry.untile_index, geometry.variable_block_sizes, geometry.tile_divisor,
|
| 542 |
+
))
|
| 543 |
+
report += (
|
| 544 |
+
f"\n\nAOTI artifact: `{package}`\n\n| eager | compiled | rel err | cosine |\n|---|---|---|---|\n"
|
| 545 |
+
f"| {t_eager * 1000:.0f} ms | {t_compiled * 1000:.0f} ms | {error / scale:.2e} | {cosine:.6f} |"
|
| 546 |
+
)
|
| 547 |
+
except Exception as error: # noqa: BLE001 - surfaced verbatim for the log
|
| 548 |
+
report += f"\n\n**AOTI FAILED**: `{type(error).__name__}: {error}`"
|
| 549 |
+
|
| 550 |
+
return report
|
| 551 |
+
|
| 552 |
+
|
| 553 |
@spaces.GPU(duration=120, size=GPU_SIZE)
|
| 554 |
def selftest() -> str:
|
| 555 |
"""Check the vendored VSA-H3 kernels against dense attention on this GPU.
|
|
|
|
| 833 |
demo.load(status, None, banner, api_name="status")
|
| 834 |
|
| 835 |
# No UI, API only: the sparse-attention equivalence check, so the kernel can be verified on this pool without
|
| 836 |
+
# spending a full generation; and the AOTI Phase A validation.
|
| 837 |
diagnose = gr.Button(visible=False)
|
| 838 |
diagnose.click(selftest, None, gr.Markdown(visible=False), api_name="selftest")
|
| 839 |
+
diagnose2 = gr.Button(visible=False)
|
| 840 |
+
diagnose2.click(aoti_diag, None, gr.Markdown(visible=False), api_name="aoti_diag")
|
| 841 |
|
| 842 |
|
| 843 |
if __name__ == "__main__":
|