Mike0021 commited on
Commit
d401c91
·
verified ·
1 Parent(s): 0d17e21

exp10a: AOTI diag endpoint

Browse files
Files changed (2) hide show
  1. aoti_attention.py +133 -0
  2. 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__":