liangsu9988 commited on
Commit
b22e03e
·
verified ·
1 Parent(s): 42a06d3

Add package source and torch-universal build variant

Browse files
CARD.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # flashrt-flex-attention-train
2
+
3
+ FlexAttention replacement API for PI-style prefix/action training masks.
4
+
5
+ ## Available functions
6
+
7
+ - `flex_attention(q, k, v, prefix_len, action_block_size, ...)`
8
+ - `flex_attention_forward(...)`
9
+ - `reference_flex_attention(...)`
10
+ - `build_block_sparse_bool_masks(...)`
11
+
12
+ ## Acceptance status
13
+
14
+ - Reference/SDPA autograd path: available.
15
+ - CUDA optimized implementation: pending A100/5090 acceptance.
16
+ - Precision mode: bf16/fp32 training reference, no FP8/FP4.
17
+ - Fallback: unsupported shapes route to SDPA.
18
+
19
+ Use this package to lock Lerobot/PI052 integration and run correctness and
20
+ benchmark gates before replacing the internal reference path with optimized
21
+ CUDA kernels.
README.md CHANGED
@@ -1,9 +1,32 @@
1
- # flashrt/flashrt-flex-attention-train
2
 
3
- This repository is a compatibility mirror for older `kernels` clients
4
- that resolve repositories through the default Hugging Face model repo API.
5
 
6
- Canonical Kernel Hub repo: https://huggingface.co/kernels/flashrt/flashrt-flex-attention-train
7
 
8
- Do not edit this mirror by hand. It is generated from the Kernel Hub
9
- `vN` branches and contains the same `build/**` artifacts.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # flashrt-flex-attention-train
2
 
3
+ FlexAttention replacement training package for PI-style dual-expert
4
+ transformers.
5
 
6
+ Hub repo: `flashrt/flashrt-flex-attention-train`
7
 
8
+ ## Public API
9
+
10
+ - `flex_attention`
11
+ - `flex_attention_forward`
12
+ - `reference_flex_attention`
13
+ - `build_block_sparse_bool_masks`
14
+ - `backend_marker`
15
+
16
+ ## Scope
17
+
18
+ This package locks the public Tensor API and correctness harness for a native
19
+ replacement of the PI052 FlexAttention/SDPA attention path:
20
+
21
+ - prefix self-attention rows
22
+ - action-to-prefix rows plus block-diagonal action rows
23
+ - `head_dim=256`
24
+ - BF16 forward/backward through PyTorch autograd fallback
25
+ - detached-prefix semantics for action rows reading prefix K/V
26
+ - prefix mask, prefix padding mask, action block mask, and action padding mask
27
+ - automatic SDPA fallback for unsupported shapes
28
+
29
+ The current implementation is the SDPA-backed training reference. It is meant
30
+ to be the stable integration target for native CUDA fwd/bwd kernels; no native
31
+ performance claim is made until the benchmark gates in `VALIDATION.md` pass on
32
+ both A100 and RTX 5090.
VALIDATION.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Validation
2
+
3
+ Correctness smoke:
4
+
5
+ ```bash
6
+ python flashrt-flex-attention-train/tests/test_flashrt_flex_attention_train.py --backend source --mode smoke
7
+ python flashrt-flex-attention-train/tests/test_flashrt_flex_attention_train.py --backend source --mode full
8
+ ```
9
+
10
+ Installed artifact smoke:
11
+
12
+ ```bash
13
+ python flashrt-flex-attention-train/tests/test_flashrt_flex_attention_train.py --backend installed --mode full
14
+ python scripts/prebuild_check.py --package flashrt-flex-attention-train --check-config
15
+ ```
16
+
17
+ Microbenchmark and acceptance gates:
18
+
19
+ ```bash
20
+ python flashrt-flex-attention-train/benchmarks/benchmark.py --device cuda --dtype bf16 --mode all --output /tmp/flex_attention_a100.json
21
+ python flashrt-flex-attention-train/benchmarks/benchmark.py --device cuda --dtype bf16 --mode all --require-gates
22
+ ```
23
+
24
+ Shape/tile matrix:
25
+
26
+ ```bash
27
+ python flashrt-flex-attention-train/benchmarks/shape_matrix.py --presets a100 --output /tmp/a100_flex_matrix.jsonl
28
+ python flashrt-flex-attention-train/benchmarks/shape_matrix.py --presets consumer --output /tmp/consumer_flex_matrix.jsonl
29
+ ```
30
+
31
+ Minimum gate for connecting native kernels:
32
+
33
+ - forward time <= `0.95 * SDPA`
34
+ - forward+backward time <= `0.95 * SDPA`
35
+ - peak memory <= `1.03 * SDPA`
36
+ - forward max abs diff <= `2e-3`
37
+ - gradient norm relative diff <= `1e-2`
38
+
39
+ Higher gate for publishing native kernels:
40
+
41
+ - full PI052 step speedup >= `1.08x` on A100 and RTX 5090
42
+ - isolated attention forward+backward speedup >= `1.25x`
43
+ - no text/flow step regression
44
+ - unsupported shapes automatically fall back to SDPA
45
+
46
+ The current package provides the reference/fallback path and therefore is not
47
+ expected to pass the performance gates until native CUDA fwd/bwd kernels are
48
+ added behind the same API.
benchmarks/README.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Benchmarks
2
+
3
+ `benchmark.py` compares the package API against the current two-call SDPA
4
+ baseline at PI052 FlexAttention replacement shapes. It records forward time, forward+backward time,
5
+ peak memory, forward max absolute diff, and gradient norm relative diff.
6
+
7
+ The default shape is the measured PI052 flow-only shape:
8
+
9
+ - `B=4`
10
+ - `heads=8`
11
+ - `head_dim=256`
12
+ - `prefix_len=700`
13
+ - `action_blocks=5`
14
+ - `action_block_size=50`
15
+
16
+ For hardware/tile sweeps, run one process per shape and preset to avoid
17
+ `torch.compile` guard reuse noise:
18
+
19
+ ```bash
20
+ python flashrt-flex-attention-train/benchmarks/shape_matrix.py --presets a100 --output /tmp/a100_flex_matrix.jsonl
21
+ python flashrt-flex-attention-train/benchmarks/shape_matrix.py --presets consumer --output /tmp/consumer_flex_matrix.jsonl
22
+ ```
benchmarks/RESULTS.md ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Results
2
+
3
+ ## Headline (RTX 5090, real PI052 shapes, GQA kv_heads=1)
4
+
5
+ At the REAL attention shapes (q 8 heads / kv 1 head, D=256, bf16) the
6
+ materialized-logits `manual` backend (cuBLAS GEMMs + one compiled masked
7
+ softmax, native autograd) beats every fused path on fwd+bwd:
8
+
9
+ | shape | SDPA fwd/fwd+bwd | best flex fwd | best flex fwd+bwd | manual fwd/fwd+bwd | manual vs flex (fwd+bwd) |
10
+ | --- | ---: | ---: | ---: | ---: | ---: |
11
+ | b4_p700_k5_c50 | 1.581 / 4.006 | 0.262 | 2.597 | 0.360 / **1.285** | **2.0x** |
12
+ | b2_p700_k5_c50 | 0.775 / 2.011 | 0.166 | 2.486 | 0.221 / **0.864** | **2.9x** |
13
+ | b1_p700_k5_c50 | 0.453 / 1.292 | 0.139 | 2.180 | 0.127 / **0.977** | 2.2x |
14
+ | b4_p512_k5_c50 | 0.967 / 2.601 | 0.179 | 1.934 | 0.245 / **1.057** | 1.8x |
15
+ | b4_p896_k5_c50 | 2.332 / 5.667 | 0.323 | 2.634 | 0.515 / **1.854** | 1.4x |
16
+ | b4_p700_k1_c50 | 1.009 / 2.660 | ERROR | ERROR | 0.279 / **0.975** | flex NaN/err at K=1 |
17
+ | b4_p700_k8_c50 | 2.116 / 5.212 | 0.301 | 2.673 | 0.497 / **1.719** | 1.6x |
18
+
19
+ (5090, torch 2.11.0+cu128, median of 10 after 5 warmup, harness includes
20
+ per-iter leaf clones + loss; flex rows are the best of
21
+ {default, bwd_shrunk_only} x {64x64, 128x128}.)
22
+
23
+ - vs the SDPA dense-mask baseline the manual backend is **2.3-3.1x** on
24
+ fwd+bwd, on every shape.
25
+ - manual peak (fwd) memory is **0.35-0.43x** of the SDPA path — the dense
26
+ additive mask the SDPA path materializes costs more than the transient
27
+ logits.
28
+ - `TORCHINDUCTOR_MAX_AUTOTUNE=1` trims manual fwd another ~14%
29
+ (0.360 -> 0.311 at b4_p700_k5); fwd+bwd unchanged.
30
+
31
+ ## Why (mechanism, not vibes)
32
+
33
+ - **GQA is the fidelity key.** With 8 kv heads (earlier sweeps) flex
34
+ fwd+bwd looked best. At the real kv_heads=1: the flex BACKWARD
35
+ autotune has no valid config on the 5090 (needs 112 KB shared memory,
36
+ hardware 101 KB) and even the shrunken-tile fallback runs 2.2-2.7 ms —
37
+ the backward dominates everything. The manual backward is four cuBLAS
38
+ GEMMs + a fused softmax-grad chain: 0.5-1.4 ms.
39
+ - flex **forward** stays the fastest single direction (autotune,
40
+ 128x128 or 64x64 masks) — but you cannot combine flex-fwd with
41
+ manual-bwd, and fwd+bwd is what training pays.
42
+ - Precision class: manual materializes logits in bf16, so outputs differ
43
+ from the fp32-softmax fused paths by up to ~1.2e-2 max-abs (flex class:
44
+ ~2e-3). Model-level parity gates (loss rel <= 1e-3, grads <= 1%) are
45
+ the ship test; the fwd-diff gate for manual is tracked separately.
46
+
47
+ ## Per-architecture verdict (final, 2026-07-09)
48
+
49
+ | arch | local microbench | real-model E2E | verdict |
50
+ | --- | --- | --- | --- |
51
+ | RTX 5090 (sm120 consumer) | manual 2.3-3.1x vs SDPA, 1.4-2.9x vs flex | flow -7.8% / text -11.6% | **manual ON** |
52
+ | A100 (sm80) | manual wins, incl. vs the repeat-interleave production baseline (B1/P1024: 1.98 vs 3.60 ms) | LOSES (text 436 vs 386 ms; action-only scope and eager-vs-compiled both exonerated) | manual OFF — integration-level interaction, parked |
53
+ | H200 (sm90) | manual LOSES the microbench outright (1.80 vs sdpa_repeat 1.18 at B1/P1024) — Hopper FMHA is too strong | not needed | manual OFF |
54
+ | RTX PRO 6000 (sm120 workstation) | pending | pending | expected ON (5090 arch family) |
55
+
56
+ `flex_attention(impl="auto")` encodes this: manual only on sm120-class
57
+ CUDA with no dropout, SDPA elsewhere.
58
+
59
+ ## Dispatch recommendation (as of 2026-07-09)
60
+
61
+ - 5090-class training (fwd+bwd): `manual` everywhere;
62
+ `TORCHINDUCTOR_MAX_AUTOTUNE` optional (+14% fwd).
63
+ - fwd-only (inference prefill): flex `default` autotune, mask 64x64
64
+ (128x128 for P>=896-class shapes).
65
+ - A100/H100/H200: SDPA path (see the verdict table).
66
+
67
+ ## Next levers (not yet implemented)
68
+
69
+ 1. Structural 3-GEMM split: the prefix part computes a dense P x P even
70
+ though the (att=0 rows x att=1 cols) quadrant is fully masked
71
+ (~20% wasted FLOPs at p700) and the action part's cross-chunk block
72
+ is near-empty. Splitting Q into (group-0, group-1, action) rows cuts
73
+ both.
74
+ 2. Custom autograd saving bf16 probabilities only (halves saved-activation
75
+ bytes and backward traffic).
76
+ 3. Native CUDA fused kernel: manual sits at ~20% of bf16 peak on the
77
+ 5090 (harness-inclusive); an FA2-style specialized kernel
78
+ (D=256, GQA, prefix-dense + action-block) targeting 40-50% would be
79
+ another ~2x. Entry per house rules: only after 1-2 are in and the
80
+ remaining gap is confirmed on both archs.
81
+
82
+ ## History
83
+
84
+ Earlier 8-kv-head sweeps (superseded — wrong KV shape for PI052):
85
+ 5090 flex fwd+bwd best 1.48-2.55 ms vs SDPA 3.97; A100 matrix at 8 kv
86
+ heads showed flex positive at real shapes with 64x64 masks. Those runs
87
+ also established: torch autotune beats every hand preset at 8 heads;
88
+ `torch_default_explicit` NaNs at K=1; ROWS_GUARANTEED_SAFE /
89
+ BLOCKS_ARE_CONTIGUOUS / PRESCALE_QK no help on 5090.
90
+
91
+ No native CUDA performance results are claimed yet.
benchmarks/benchmark.py ADDED
@@ -0,0 +1,779 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ import torch
11
+ import torch.nn.functional as F
12
+
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ sys.path.insert(0, str(ROOT / "torch-ext"))
16
+ import flashrt_flex_attention_train as flex_ops # noqa: E402
17
+
18
+
19
+ FLEX_TILE_PRESETS = {
20
+ "default": None,
21
+ # fwd fully autotuned; backward constrained to the consumer-GPU tiles
22
+ # (autotuned backward at GQA/D=256 exceeds 5090 shared memory).
23
+ "bwd_shrunk_only": {
24
+ "bwd_BLOCK_M1": 32,
25
+ "bwd_BLOCK_N1": 64,
26
+ "bwd_BLOCK_M2": 64,
27
+ "bwd_BLOCK_N2": 32,
28
+ },
29
+ "torch_default_explicit": {
30
+ "fwd_BLOCK_M": 32,
31
+ "fwd_BLOCK_N": 64,
32
+ "fwd_num_stages": 2,
33
+ "fwd_num_warps": 4,
34
+ "bwd_BLOCK_M1": 32,
35
+ "bwd_BLOCK_N1": 64,
36
+ "bwd_BLOCK_M2": 64,
37
+ "bwd_BLOCK_N2": 32,
38
+ "bwd_num_stages": 1,
39
+ "bwd_num_warps": 4,
40
+ },
41
+ "a100_d256_bwd_32x64": {
42
+ "fwd_BLOCK_M": 32,
43
+ "fwd_BLOCK_N": 64,
44
+ "fwd_num_stages": 2,
45
+ "fwd_num_warps": 4,
46
+ "bwd_BLOCK_M1": 32,
47
+ "bwd_BLOCK_N1": 64,
48
+ "bwd_BLOCK_M2": 64,
49
+ "bwd_BLOCK_N2": 32,
50
+ "bwd_num_stages": 3,
51
+ "bwd_num_warps": 4,
52
+ },
53
+ "a100_d256_bwd_32x128": {
54
+ "fwd_BLOCK_M": 32,
55
+ "fwd_BLOCK_N": 64,
56
+ "fwd_num_stages": 2,
57
+ "fwd_num_warps": 4,
58
+ "bwd_BLOCK_M1": 32,
59
+ "bwd_BLOCK_N1": 128,
60
+ "bwd_BLOCK_M2": 128,
61
+ "bwd_BLOCK_N2": 32,
62
+ "bwd_num_stages": 3,
63
+ "bwd_num_warps": 8,
64
+ },
65
+ "a100_d256_bwd_64x64": {
66
+ "fwd_BLOCK_M": 64,
67
+ "fwd_BLOCK_N": 64,
68
+ "fwd_num_stages": 3,
69
+ "fwd_num_warps": 4,
70
+ "bwd_BLOCK_M1": 64,
71
+ "bwd_BLOCK_N1": 64,
72
+ "bwd_BLOCK_M2": 64,
73
+ "bwd_BLOCK_N2": 64,
74
+ "bwd_num_stages": 3,
75
+ "bwd_num_warps": 4,
76
+ },
77
+ "a100_d256_bwd_64x128": {
78
+ "fwd_BLOCK_M": 64,
79
+ "fwd_BLOCK_N": 128,
80
+ "fwd_num_stages": 3,
81
+ "fwd_num_warps": 4,
82
+ "bwd_BLOCK_M1": 64,
83
+ "bwd_BLOCK_N1": 128,
84
+ "bwd_BLOCK_M2": 128,
85
+ "bwd_BLOCK_N2": 64,
86
+ "bwd_num_stages": 3,
87
+ "bwd_num_warps": 8,
88
+ },
89
+ "a100_d256_bwd_write_dq_false": {
90
+ "fwd_BLOCK_M": 32,
91
+ "fwd_BLOCK_N": 64,
92
+ "fwd_num_stages": 2,
93
+ "fwd_num_warps": 4,
94
+ "bwd_BLOCK_M1": 32,
95
+ "bwd_BLOCK_N1": 64,
96
+ "bwd_BLOCK_M2": 64,
97
+ "bwd_BLOCK_N2": 32,
98
+ "bwd_num_stages": 3,
99
+ "bwd_num_warps": 4,
100
+ "WRITE_DQ": False,
101
+ },
102
+ "a100_d256_prescale_safe": {
103
+ "fwd_BLOCK_M": 32,
104
+ "fwd_BLOCK_N": 64,
105
+ "fwd_num_stages": 2,
106
+ "fwd_num_warps": 4,
107
+ "bwd_BLOCK_M1": 32,
108
+ "bwd_BLOCK_N1": 64,
109
+ "bwd_BLOCK_M2": 64,
110
+ "bwd_BLOCK_N2": 32,
111
+ "bwd_num_stages": 3,
112
+ "bwd_num_warps": 4,
113
+ "PRESCALE_QK": True,
114
+ "ROWS_GUARANTEED_SAFE": True,
115
+ },
116
+ "a100_d256_contig_safe": {
117
+ "fwd_BLOCK_M": 32,
118
+ "fwd_BLOCK_N": 64,
119
+ "fwd_num_stages": 2,
120
+ "fwd_num_warps": 4,
121
+ "bwd_BLOCK_M1": 32,
122
+ "bwd_BLOCK_N1": 64,
123
+ "bwd_BLOCK_M2": 64,
124
+ "bwd_BLOCK_N2": 32,
125
+ "bwd_num_stages": 3,
126
+ "bwd_num_warps": 4,
127
+ "ROWS_GUARANTEED_SAFE": True,
128
+ "BLOCKS_ARE_CONTIGUOUS": True,
129
+ },
130
+ "a100_d256_contig_prescale": {
131
+ "fwd_BLOCK_M": 32,
132
+ "fwd_BLOCK_N": 64,
133
+ "fwd_num_stages": 2,
134
+ "fwd_num_warps": 4,
135
+ "bwd_BLOCK_M1": 32,
136
+ "bwd_BLOCK_N1": 64,
137
+ "bwd_BLOCK_M2": 64,
138
+ "bwd_BLOCK_N2": 32,
139
+ "bwd_num_stages": 3,
140
+ "bwd_num_warps": 4,
141
+ "PRESCALE_QK": True,
142
+ "ROWS_GUARANTEED_SAFE": True,
143
+ "BLOCKS_ARE_CONTIGUOUS": True,
144
+ },
145
+ "a100_d256_contig_write_dq_false": {
146
+ "fwd_BLOCK_M": 32,
147
+ "fwd_BLOCK_N": 64,
148
+ "fwd_num_stages": 2,
149
+ "fwd_num_warps": 4,
150
+ "bwd_BLOCK_M1": 32,
151
+ "bwd_BLOCK_N1": 64,
152
+ "bwd_BLOCK_M2": 64,
153
+ "bwd_BLOCK_N2": 32,
154
+ "bwd_num_stages": 3,
155
+ "bwd_num_warps": 4,
156
+ "ROWS_GUARANTEED_SAFE": True,
157
+ "BLOCKS_ARE_CONTIGUOUS": True,
158
+ "WRITE_DQ": False,
159
+ },
160
+ }
161
+
162
+
163
+ def bench(fn, warmup: int, iters: int) -> float:
164
+ for _ in range(warmup):
165
+ fn()
166
+ if torch.cuda.is_available():
167
+ torch.cuda.synchronize()
168
+ times = []
169
+ for _ in range(iters):
170
+ if torch.cuda.is_available():
171
+ t0 = torch.cuda.Event(enable_timing=True)
172
+ t1 = torch.cuda.Event(enable_timing=True)
173
+ t0.record()
174
+ fn()
175
+ t1.record()
176
+ torch.cuda.synchronize()
177
+ times.append(t0.elapsed_time(t1))
178
+ else:
179
+ start = time.perf_counter()
180
+ fn()
181
+ times.append((time.perf_counter() - start) * 1000.0)
182
+ times.sort()
183
+ return times[len(times) // 2]
184
+
185
+
186
+ def peak_bytes(fn, device: str) -> int:
187
+ if not device.startswith("cuda"):
188
+ fn()
189
+ return 0
190
+ torch.cuda.reset_peak_memory_stats()
191
+ fn()
192
+ torch.cuda.synchronize()
193
+ return int(torch.cuda.max_memory_allocated())
194
+
195
+
196
+ def best_nested_speedup(nested: dict) -> dict | None:
197
+ best = None
198
+ for block_key, by_preset in nested.items():
199
+ for preset, speedup in by_preset.items():
200
+ if not isinstance(speedup, float):
201
+ continue
202
+ if best is None or speedup > best["speedup"]:
203
+ best = {"block_mask": block_key, "preset": preset, "speedup": speedup}
204
+ return best
205
+
206
+
207
+ def make_inputs(args):
208
+ dtype = torch.bfloat16 if args.dtype == "bf16" else torch.float32
209
+ action_len = args.action_blocks * args.action_block_size
210
+ total = args.prefix_len + action_len
211
+ torch.manual_seed(args.seed)
212
+ q = torch.randn(args.batch, args.heads, total, args.head_dim, device=args.device, dtype=dtype)
213
+ kv_heads = args.kv_heads or args.heads
214
+ k = torch.randn(args.batch, kv_heads, total, args.head_dim, device=args.device, dtype=dtype)
215
+ v = torch.randn_like(k)
216
+ prefix_valid = torch.ones(args.batch, args.prefix_len, device=args.device, dtype=torch.bool)
217
+ prefix_att = torch.zeros_like(prefix_valid)
218
+ prefix_att[:, args.prefix_len // 2 :] = True
219
+ if args.no_prefix_mask:
220
+ prefix_valid = None
221
+ prefix_att = None
222
+ return q, k, v, prefix_valid, prefix_att
223
+
224
+
225
+ def make_flex_bundle(args, prefix_valid, prefix_att, block_size: tuple[int, int]):
226
+ try:
227
+ from torch.nn.attention.flex_attention import create_block_mask, flex_attention
228
+ except Exception:
229
+ return None
230
+
231
+ compiled_mask = torch.compile(create_block_mask, dynamic=False)
232
+ batch = args.batch
233
+ prefix_len = args.prefix_len
234
+ action_len = args.action_blocks * args.action_block_size
235
+ total_len = prefix_len + action_len
236
+ chunk = args.action_block_size
237
+ pad = prefix_valid
238
+ cum = torch.cumsum(prefix_att.to(torch.long), dim=1) if prefix_att is not None else None
239
+
240
+ def prefix_rows(b, h, q_idx, kv_idx):
241
+ if cum is None:
242
+ return kv_idx < prefix_len
243
+ kv_p = kv_idx.clamp(max=prefix_len - 1)
244
+ ok = (cum[b, kv_p] <= cum[b, q_idx]) & pad[b, kv_p] & pad[b, q_idx]
245
+ return (kv_idx < prefix_len) & ok
246
+
247
+ def action_rows(b, h, q_idx, kv_idx):
248
+ if pad is None:
249
+ to_prefix = kv_idx < prefix_len
250
+ else:
251
+ kv_p = kv_idx.clamp(max=prefix_len - 1)
252
+ to_prefix = (kv_idx < prefix_len) & pad[b, kv_p]
253
+ same_block = (q_idx // chunk) == ((kv_idx - prefix_len) // chunk)
254
+ return to_prefix | ((kv_idx >= prefix_len) & same_block)
255
+
256
+ block_prefix = compiled_mask(
257
+ prefix_rows,
258
+ B=batch,
259
+ H=None,
260
+ Q_LEN=prefix_len,
261
+ KV_LEN=total_len,
262
+ device=torch.device(args.device),
263
+ BLOCK_SIZE=block_size,
264
+ )
265
+ block_action = compiled_mask(
266
+ action_rows,
267
+ B=batch,
268
+ H=None,
269
+ Q_LEN=action_len,
270
+ KV_LEN=total_len,
271
+ device=torch.device(args.device),
272
+ BLOCK_SIZE=block_size,
273
+ )
274
+ compiled_calls = {}
275
+ scale = args.head_dim**-0.5
276
+ gqa = (args.kv_heads or args.heads) != args.heads
277
+ for name, options in FLEX_TILE_PRESETS.items():
278
+ def prefix_call(q, k, v, block_mask, options=options):
279
+ return flex_attention(
280
+ q,
281
+ k,
282
+ v,
283
+ block_mask=block_mask,
284
+ scale=scale,
285
+ enable_gqa=gqa,
286
+ kernel_options=options,
287
+ )
288
+
289
+ def action_call(q, k, v, block_mask, options=options):
290
+ return flex_attention(
291
+ q,
292
+ k,
293
+ v,
294
+ block_mask=block_mask,
295
+ scale=scale,
296
+ enable_gqa=gqa,
297
+ kernel_options=options,
298
+ )
299
+
300
+ compiled_calls[name] = (
301
+ torch.compile(prefix_call, dynamic=False),
302
+ torch.compile(action_call, dynamic=False),
303
+ )
304
+ return compiled_calls, block_prefix, block_action
305
+
306
+
307
+ def _manual_part(qs, ks, vs, m, scale):
308
+ """Materialized-logits attention: cuBLAS GEMMs + fused masked softmax.
309
+
310
+ Exact SDPA semantics (fp32 softmax); grouped-query handled as a strided
311
+ batched GEMM over (kv_head, group*Sq) without materializing repeated K/V.
312
+ """
313
+ B, H, Sq, D = qs.shape
314
+ Hk = ks.shape[1]
315
+ if Hk != H:
316
+ g = H // Hk
317
+ q2 = qs.reshape(B, Hk, g * Sq, D)
318
+ logits = (q2 @ ks.transpose(-1, -2)).reshape(B, H, Sq, -1)
319
+ else:
320
+ logits = qs @ ks.transpose(-1, -2)
321
+ logits = logits * scale + m
322
+ p = logits.float().softmax(dim=-1).to(qs.dtype)
323
+ if Hk != H:
324
+ out = (p.reshape(B, Hk, g * Sq, -1) @ vs).reshape(B, H, Sq, D)
325
+ else:
326
+ out = p @ vs
327
+ return out
328
+
329
+
330
+ _manual_part_compiled = None
331
+
332
+
333
+ def get_manual_part():
334
+ global _manual_part_compiled
335
+ if _manual_part_compiled is None:
336
+ _manual_part_compiled = torch.compile(_manual_part, dynamic=False)
337
+ return _manual_part_compiled
338
+
339
+
340
+ def parse_args() -> argparse.Namespace:
341
+ parser = argparse.ArgumentParser()
342
+ parser.add_argument("--device", default="cuda")
343
+ parser.add_argument("--dtype", choices=["bf16", "fp32"], default="bf16")
344
+ parser.add_argument("--batch", type=int, default=4)
345
+ parser.add_argument("--heads", type=int, default=8)
346
+ parser.add_argument("--kv-heads", type=int, default=None, help="KV heads for GQA (default: same as --heads)")
347
+ parser.add_argument("--head-dim", type=int, default=256)
348
+ parser.add_argument("--prefix-len", type=int, default=700)
349
+ parser.add_argument("--action-blocks", type=int, default=5)
350
+ parser.add_argument("--action-block-size", type=int, default=50)
351
+ parser.add_argument("--warmup", type=int, default=10)
352
+ parser.add_argument("--iters", type=int, default=30)
353
+ parser.add_argument("--seed", type=int, default=0)
354
+ parser.add_argument("--mode", choices=["fwd", "fwdbwd", "all"], default="all")
355
+ parser.add_argument(
356
+ "--backend",
357
+ default="all",
358
+ help="comma-separated subset of {package, torch-flex, manual} or 'all'",
359
+ )
360
+ parser.add_argument("--flex-preset", choices=sorted(FLEX_TILE_PRESETS), default="a100_d256_bwd_32x64")
361
+ parser.add_argument("--sweep-flex-presets", action="store_true")
362
+ parser.add_argument("--block-mask-q", type=int, default=128)
363
+ parser.add_argument("--block-mask-kv", type=int, default=128)
364
+ parser.add_argument("--sweep-block-mask-sizes", action="store_true")
365
+ parser.add_argument("--output")
366
+ parser.add_argument("--require-gates", action="store_true")
367
+ parser.add_argument("--no-prefix-mask", action="store_true")
368
+ return parser.parse_args()
369
+
370
+
371
+ def main() -> None:
372
+ args = parse_args()
373
+ if args.device.startswith("cuda") and not torch.cuda.is_available():
374
+ raise SystemExit("CUDA requested but not available")
375
+ backends = (
376
+ {"package", "torch-flex", "manual"}
377
+ if args.backend == "all"
378
+ else {b.strip() for b in args.backend.split(",") if b.strip()}
379
+ )
380
+
381
+ q, k, v, prefix_valid, prefix_att = make_inputs(args)
382
+ action_len = args.action_blocks * args.action_block_size
383
+ scale = args.head_dim**-0.5
384
+ pm, am = flex_ops.build_block_sparse_bool_masks(
385
+ prefix_valid,
386
+ prefix_att,
387
+ batch=args.batch,
388
+ prefix_len=args.prefix_len,
389
+ action_len=action_len,
390
+ action_block_size=args.action_block_size,
391
+ device=q.device,
392
+ )
393
+ full = torch.cat([pm, am], dim=1)
394
+ add_mask = torch.where(
395
+ full[:, None],
396
+ torch.zeros((), device=q.device, dtype=q.dtype),
397
+ torch.full((), flex_ops.MASK_VALUE_F32, device=q.device, dtype=q.dtype),
398
+ )
399
+ block_sizes = [(args.block_mask_q, args.block_mask_kv)]
400
+ if args.sweep_block_mask_sizes:
401
+ block_sizes = [(q, kv) for q in (16, 32, 64, 128) for kv in (32, 64, 128)]
402
+ flex_bundles = {
403
+ f"{q}x{kv}": make_flex_bundle(args, prefix_valid, prefix_att, (q, kv))
404
+ for q, kv in block_sizes
405
+ } if "torch-flex" in backends else {}
406
+ flex_presets = sorted(FLEX_TILE_PRESETS) if args.sweep_flex_presets else [args.flex_preset]
407
+
408
+ gqa = (args.kv_heads or args.heads) != args.heads
409
+
410
+ def sdpa_fwd():
411
+ out_p = F.scaled_dot_product_attention(
412
+ q[:, :, : args.prefix_len],
413
+ k,
414
+ v,
415
+ attn_mask=add_mask[:, :, : args.prefix_len],
416
+ scale=scale,
417
+ enable_gqa=gqa,
418
+ )
419
+ kd = torch.cat([k[:, :, : args.prefix_len].detach(), k[:, :, args.prefix_len :]], dim=2)
420
+ vd = torch.cat([v[:, :, : args.prefix_len].detach(), v[:, :, args.prefix_len :]], dim=2)
421
+ out_a = F.scaled_dot_product_attention(
422
+ q[:, :, args.prefix_len :],
423
+ kd,
424
+ vd,
425
+ attn_mask=add_mask[:, :, args.prefix_len :],
426
+ scale=scale,
427
+ enable_gqa=gqa,
428
+ )
429
+ return torch.cat([out_p, out_a], dim=2)
430
+
431
+ manual_part = get_manual_part() if "manual" in backends else None
432
+
433
+ def manual_eager_fwd():
434
+ out_p = _manual_part(q[:, :, : args.prefix_len], k, v, add_mask[:, :, : args.prefix_len], scale)
435
+ kd = torch.cat([k[:, :, : args.prefix_len].detach(), k[:, :, args.prefix_len :]], dim=2)
436
+ vd = torch.cat([v[:, :, : args.prefix_len].detach(), v[:, :, args.prefix_len :]], dim=2)
437
+ out_a = _manual_part(
438
+ q[:, :, args.prefix_len :], kd, vd, add_mask[:, :, args.prefix_len :], scale
439
+ )
440
+ return torch.cat([out_p, out_a], dim=2)
441
+
442
+ def manual_eager_fwdbwd():
443
+ qq = q.detach().clone().requires_grad_(True)
444
+ kk = k.detach().clone().requires_grad_(True)
445
+ vv = v.detach().clone().requires_grad_(True)
446
+ out_p = _manual_part(
447
+ qq[:, :, : args.prefix_len], kk, vv, add_mask[:, :, : args.prefix_len], scale
448
+ )
449
+ kd = torch.cat([kk[:, :, : args.prefix_len].detach(), kk[:, :, args.prefix_len :]], dim=2)
450
+ vd = torch.cat([vv[:, :, : args.prefix_len].detach(), vv[:, :, args.prefix_len :]], dim=2)
451
+ out_a = _manual_part(
452
+ qq[:, :, args.prefix_len :], kd, vd, add_mask[:, :, args.prefix_len :], scale
453
+ )
454
+ torch.cat([out_p, out_a], dim=2).float().square().mean().backward()
455
+
456
+ def _repeat_kv(t):
457
+ # the model's current path: materialize K/V to the q-head count
458
+ return t.repeat_interleave(args.heads // t.shape[1], dim=1)
459
+
460
+ def sdpa_repeat_fwd():
461
+ kr, vr = _repeat_kv(k), _repeat_kv(v)
462
+ out_p = F.scaled_dot_product_attention(
463
+ q[:, :, : args.prefix_len], kr, vr, attn_mask=add_mask[:, :, : args.prefix_len], scale=scale
464
+ )
465
+ kd = torch.cat([kr[:, :, : args.prefix_len].detach(), kr[:, :, args.prefix_len :]], dim=2)
466
+ vd = torch.cat([vr[:, :, : args.prefix_len].detach(), vr[:, :, args.prefix_len :]], dim=2)
467
+ out_a = F.scaled_dot_product_attention(
468
+ q[:, :, args.prefix_len :], kd, vd, attn_mask=add_mask[:, :, args.prefix_len :], scale=scale
469
+ )
470
+ return torch.cat([out_p, out_a], dim=2)
471
+
472
+ def manual_fwd():
473
+ out_p = manual_part(q[:, :, : args.prefix_len], k, v, add_mask[:, :, : args.prefix_len], scale)
474
+ kd = torch.cat([k[:, :, : args.prefix_len].detach(), k[:, :, args.prefix_len :]], dim=2)
475
+ vd = torch.cat([v[:, :, : args.prefix_len].detach(), v[:, :, args.prefix_len :]], dim=2)
476
+ out_a = manual_part(
477
+ q[:, :, args.prefix_len :], kd, vd, add_mask[:, :, args.prefix_len :], scale
478
+ )
479
+ return torch.cat([out_p, out_a], dim=2)
480
+
481
+ def package_fwd():
482
+ return flex_ops.flex_attention(
483
+ q,
484
+ k,
485
+ v,
486
+ prefix_len=args.prefix_len,
487
+ action_block_size=args.action_block_size,
488
+ prefix_valid=prefix_valid,
489
+ prefix_att=prefix_att,
490
+ scale=scale,
491
+ )
492
+
493
+ def torch_flex_fwd(preset: str, block_key: str):
494
+ if not flex_bundles:
495
+ raise RuntimeError("PyTorch FlexAttention is unavailable")
496
+ compiled_calls, block_prefix, block_action = flex_bundles[block_key]
497
+ prefix_call, action_call = compiled_calls[preset]
498
+ out_p = prefix_call(
499
+ q[:, :, : args.prefix_len],
500
+ k,
501
+ v,
502
+ block_prefix,
503
+ )
504
+ kd = torch.cat([k[:, :, : args.prefix_len].detach(), k[:, :, args.prefix_len :]], dim=2)
505
+ vd = torch.cat([v[:, :, : args.prefix_len].detach(), v[:, :, args.prefix_len :]], dim=2)
506
+ out_a = action_call(
507
+ q[:, :, args.prefix_len :],
508
+ kd,
509
+ vd,
510
+ block_action,
511
+ )
512
+ return torch.cat([out_p, out_a], dim=2)
513
+
514
+ report = {
515
+ "gpu": torch.cuda.get_device_name() if args.device.startswith("cuda") else "cpu",
516
+ "torch": torch.__version__,
517
+ "shape": {
518
+ "B": args.batch,
519
+ "heads": args.heads,
520
+ "kv_heads": args.kv_heads or args.heads,
521
+ "head_dim": args.head_dim,
522
+ "prefix_len": args.prefix_len,
523
+ "action_len": action_len,
524
+ "action_block_size": args.action_block_size,
525
+ },
526
+ "backend": args.backend,
527
+ "flex_presets": flex_presets,
528
+ "block_mask_sizes": list(flex_bundles) if flex_bundles else [],
529
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
530
+ }
531
+ report["package_has_native_ops"] = False
532
+ report["package_native_supported"] = False
533
+
534
+ with torch.no_grad():
535
+ sdpa_out = sdpa_fwd().float()
536
+ if "package" in backends:
537
+ report["package_fwd_max_abs_diff"] = float((sdpa_out - package_fwd().float()).abs().max())
538
+ if "manual" in backends:
539
+ report["manual_fwd_max_abs_diff"] = float((sdpa_out - manual_fwd().float()).abs().max())
540
+ if flex_bundles:
541
+ report["torch_flex_fwd_max_abs_diff"] = {}
542
+ for block_key in flex_bundles:
543
+ report["torch_flex_fwd_max_abs_diff"][block_key] = {}
544
+ for block_key in flex_bundles:
545
+ for preset in flex_presets:
546
+ try:
547
+ report["torch_flex_fwd_max_abs_diff"][block_key][preset] = float(
548
+ (sdpa_out - torch_flex_fwd(preset, block_key).float()).abs().max()
549
+ )
550
+ except Exception as exc:
551
+ report["torch_flex_fwd_max_abs_diff"][block_key][preset] = f"ERROR: {type(exc).__name__}: {exc}"
552
+ if args.mode in {"fwd", "all"}:
553
+ report["sdpa_fwd_ms"] = bench(sdpa_fwd, args.warmup, args.iters)
554
+ report["sdpa_peak_bytes"] = peak_bytes(sdpa_fwd, args.device)
555
+ if "package" in backends:
556
+ report["package_fwd_ms"] = bench(package_fwd, args.warmup, args.iters)
557
+ report["package_fwd_speedup"] = report["sdpa_fwd_ms"] / report["package_fwd_ms"]
558
+ report["package_peak_bytes"] = peak_bytes(package_fwd, args.device)
559
+ if "manual" in backends:
560
+ report["manual_fwd_ms"] = bench(manual_fwd, args.warmup, args.iters)
561
+ report["manual_fwd_speedup"] = report["sdpa_fwd_ms"] / report["manual_fwd_ms"]
562
+ report["manual_peak_bytes"] = peak_bytes(manual_fwd, args.device)
563
+ if gqa:
564
+ report["sdpa_repeat_fwd_ms"] = bench(sdpa_repeat_fwd, args.warmup, args.iters)
565
+ if "manual" in backends:
566
+ report["manual_eager_fwd_ms"] = bench(manual_eager_fwd, args.warmup, args.iters)
567
+ if flex_bundles:
568
+ report["torch_flex_fwd_ms"] = {}
569
+ report["torch_flex_fwd_speedup"] = {}
570
+ for block_key in flex_bundles:
571
+ report["torch_flex_fwd_ms"][block_key] = {}
572
+ report["torch_flex_fwd_speedup"][block_key] = {}
573
+ for block_key in flex_bundles:
574
+ for preset in flex_presets:
575
+ try:
576
+ ms = bench(lambda preset=preset, block_key=block_key: torch_flex_fwd(preset, block_key), args.warmup, args.iters)
577
+ except Exception as exc:
578
+ report["torch_flex_fwd_ms"][block_key][preset] = f"ERROR: {type(exc).__name__}: {exc}"
579
+ else:
580
+ report["torch_flex_fwd_ms"][block_key][preset] = ms
581
+ report["torch_flex_fwd_speedup"][block_key][preset] = report["sdpa_fwd_ms"] / ms
582
+ report["best_torch_flex_fwd"] = best_nested_speedup(report["torch_flex_fwd_speedup"])
583
+
584
+ if args.mode in {"fwdbwd", "all"}:
585
+ def sdpa_fwdbwd():
586
+ qq = q.detach().clone().requires_grad_(True)
587
+ kk = k.detach().clone().requires_grad_(True)
588
+ vv = v.detach().clone().requires_grad_(True)
589
+ out_p = F.scaled_dot_product_attention(
590
+ qq[:, :, : args.prefix_len],
591
+ kk,
592
+ vv,
593
+ attn_mask=add_mask[:, :, : args.prefix_len],
594
+ scale=scale,
595
+ enable_gqa=gqa,
596
+ )
597
+ kd = torch.cat([kk[:, :, : args.prefix_len].detach(), kk[:, :, args.prefix_len :]], dim=2)
598
+ vd = torch.cat([vv[:, :, : args.prefix_len].detach(), vv[:, :, args.prefix_len :]], dim=2)
599
+ out_a = F.scaled_dot_product_attention(
600
+ qq[:, :, args.prefix_len :],
601
+ kd,
602
+ vd,
603
+ attn_mask=add_mask[:, :, args.prefix_len :],
604
+ scale=scale,
605
+ enable_gqa=gqa,
606
+ )
607
+ torch.cat([out_p, out_a], dim=2).float().square().mean().backward()
608
+
609
+ def manual_fwdbwd():
610
+ qq = q.detach().clone().requires_grad_(True)
611
+ kk = k.detach().clone().requires_grad_(True)
612
+ vv = v.detach().clone().requires_grad_(True)
613
+ out_p = manual_part(
614
+ qq[:, :, : args.prefix_len], kk, vv, add_mask[:, :, : args.prefix_len], scale
615
+ )
616
+ kd = torch.cat([kk[:, :, : args.prefix_len].detach(), kk[:, :, args.prefix_len :]], dim=2)
617
+ vd = torch.cat([vv[:, :, : args.prefix_len].detach(), vv[:, :, args.prefix_len :]], dim=2)
618
+ out_a = manual_part(
619
+ qq[:, :, args.prefix_len :], kd, vd, add_mask[:, :, args.prefix_len :], scale
620
+ )
621
+ torch.cat([out_p, out_a], dim=2).float().square().mean().backward()
622
+
623
+ def sdpa_repeat_fwdbwd():
624
+ qq = q.detach().clone().requires_grad_(True)
625
+ kk = k.detach().clone().requires_grad_(True)
626
+ vv = v.detach().clone().requires_grad_(True)
627
+ kr, vr = _repeat_kv(kk), _repeat_kv(vv)
628
+ out_p = F.scaled_dot_product_attention(
629
+ qq[:, :, : args.prefix_len], kr, vr, attn_mask=add_mask[:, :, : args.prefix_len], scale=scale
630
+ )
631
+ kd = torch.cat([kr[:, :, : args.prefix_len].detach(), kr[:, :, args.prefix_len :]], dim=2)
632
+ vd = torch.cat([vr[:, :, : args.prefix_len].detach(), vr[:, :, args.prefix_len :]], dim=2)
633
+ out_a = F.scaled_dot_product_attention(
634
+ qq[:, :, args.prefix_len :], kd, vd, attn_mask=add_mask[:, :, args.prefix_len :], scale=scale
635
+ )
636
+ torch.cat([out_p, out_a], dim=2).float().square().mean().backward()
637
+
638
+ def package_fwdbwd():
639
+ qq = q.detach().clone().requires_grad_(True)
640
+ kk = k.detach().clone().requires_grad_(True)
641
+ vv = v.detach().clone().requires_grad_(True)
642
+ flex_ops.flex_attention(
643
+ qq,
644
+ kk,
645
+ vv,
646
+ prefix_len=args.prefix_len,
647
+ action_block_size=args.action_block_size,
648
+ prefix_valid=prefix_valid,
649
+ prefix_att=prefix_att,
650
+ scale=scale,
651
+ ).float().square().mean().backward()
652
+
653
+ def torch_flex_fwdbwd(preset: str, block_key: str):
654
+ if not flex_bundles:
655
+ raise RuntimeError("PyTorch FlexAttention is unavailable")
656
+ compiled_calls, block_prefix, block_action = flex_bundles[block_key]
657
+ prefix_call, action_call = compiled_calls[preset]
658
+ qq = q.detach().clone().requires_grad_(True)
659
+ kk = k.detach().clone().requires_grad_(True)
660
+ vv = v.detach().clone().requires_grad_(True)
661
+ out_p = prefix_call(
662
+ qq[:, :, : args.prefix_len],
663
+ kk,
664
+ vv,
665
+ block_prefix,
666
+ )
667
+ kd = torch.cat([kk[:, :, : args.prefix_len].detach(), kk[:, :, args.prefix_len :]], dim=2)
668
+ vd = torch.cat([vv[:, :, : args.prefix_len].detach(), vv[:, :, args.prefix_len :]], dim=2)
669
+ out_a = action_call(
670
+ qq[:, :, args.prefix_len :],
671
+ kd,
672
+ vd,
673
+ block_action,
674
+ )
675
+ torch.cat([out_p, out_a], dim=2).float().square().mean().backward()
676
+
677
+ report["sdpa_fwdbwd_ms"] = bench(sdpa_fwdbwd, args.warmup, args.iters)
678
+ if "package" in backends:
679
+ report["package_fwdbwd_ms"] = bench(package_fwdbwd, args.warmup, args.iters)
680
+ report["package_fwdbwd_speedup"] = report["sdpa_fwdbwd_ms"] / report["package_fwdbwd_ms"]
681
+ if "manual" in backends:
682
+ report["manual_fwdbwd_ms"] = bench(manual_fwdbwd, args.warmup, args.iters)
683
+ report["manual_fwdbwd_speedup"] = report["sdpa_fwdbwd_ms"] / report["manual_fwdbwd_ms"]
684
+ if gqa:
685
+ report["sdpa_repeat_fwdbwd_ms"] = bench(sdpa_repeat_fwdbwd, args.warmup, args.iters)
686
+ if "manual" in backends:
687
+ report["manual_eager_fwdbwd_ms"] = bench(manual_eager_fwdbwd, args.warmup, args.iters)
688
+ if flex_bundles:
689
+ report["torch_flex_fwdbwd_ms"] = {}
690
+ report["torch_flex_fwdbwd_speedup"] = {}
691
+ for block_key in flex_bundles:
692
+ report["torch_flex_fwdbwd_ms"][block_key] = {}
693
+ report["torch_flex_fwdbwd_speedup"][block_key] = {}
694
+ for block_key in flex_bundles:
695
+ for preset in flex_presets:
696
+ try:
697
+ ms = bench(lambda preset=preset, block_key=block_key: torch_flex_fwdbwd(preset, block_key), args.warmup, args.iters)
698
+ except Exception as exc:
699
+ report["torch_flex_fwdbwd_ms"][block_key][preset] = f"ERROR: {type(exc).__name__}: {exc}"
700
+ else:
701
+ report["torch_flex_fwdbwd_ms"][block_key][preset] = ms
702
+ report["torch_flex_fwdbwd_speedup"][block_key][preset] = report["sdpa_fwdbwd_ms"] / ms
703
+ report["best_torch_flex_fwdbwd"] = best_nested_speedup(report["torch_flex_fwdbwd_speedup"])
704
+
705
+ if "package" not in backends:
706
+ _finish(report, args)
707
+ return
708
+ q1 = q.detach().clone().requires_grad_(True)
709
+ k1 = k.detach().clone().requires_grad_(True)
710
+ v1 = v.detach().clone().requires_grad_(True)
711
+ q2 = q.detach().clone().requires_grad_(True)
712
+ k2 = k.detach().clone().requires_grad_(True)
713
+ v2 = v.detach().clone().requires_grad_(True)
714
+ out1_p = F.scaled_dot_product_attention(
715
+ q1[:, :, : args.prefix_len], k1, v1, attn_mask=add_mask[:, :, : args.prefix_len], scale=scale
716
+ )
717
+ k1d = torch.cat([k1[:, :, : args.prefix_len].detach(), k1[:, :, args.prefix_len :]], dim=2)
718
+ v1d = torch.cat([v1[:, :, : args.prefix_len].detach(), v1[:, :, args.prefix_len :]], dim=2)
719
+ out1_a = F.scaled_dot_product_attention(
720
+ q1[:, :, args.prefix_len :], k1d, v1d, attn_mask=add_mask[:, :, args.prefix_len :], scale=scale
721
+ )
722
+ torch.cat([out1_p, out1_a], dim=2).float().square().mean().backward()
723
+ flex_ops.flex_attention(
724
+ q2,
725
+ k2,
726
+ v2,
727
+ prefix_len=args.prefix_len,
728
+ action_block_size=args.action_block_size,
729
+ prefix_valid=prefix_valid,
730
+ prefix_att=prefix_att,
731
+ scale=scale,
732
+ ).float().square().mean().backward()
733
+ denom = torch.linalg.vector_norm(torch.cat([q1.grad.flatten(), k1.grad.flatten(), v1.grad.flatten()])).clamp_min(1e-12)
734
+ numer = torch.linalg.vector_norm(
735
+ torch.cat([(q1.grad - q2.grad).flatten(), (k1.grad - k2.grad).flatten(), (v1.grad - v2.grad).flatten()])
736
+ )
737
+ report["package_grad_norm_rel_diff"] = float(numer / denom)
738
+
739
+ _finish(report, args)
740
+
741
+
742
+ def _finish(report, args):
743
+ gates = {
744
+ "package_fwd_max_abs_diff": report.get("package_fwd_max_abs_diff", 0.0) <= 2e-3,
745
+ "package_grad_norm_rel_diff": report.get("package_grad_norm_rel_diff", 0.0) <= 1e-2,
746
+ }
747
+ if "torch_flex_fwd_max_abs_diff" in report:
748
+ vals = [v for by_block in report["torch_flex_fwd_max_abs_diff"].values() for v in by_block.values()]
749
+ gates["torch_flex_fwd_max_abs_diff"] = any(isinstance(v, float) and v <= 2e-3 for v in vals)
750
+ if "manual_fwd_max_abs_diff" in report:
751
+ gates["manual_fwd_max_abs_diff"] = report["manual_fwd_max_abs_diff"] <= 2e-3
752
+ if "manual_fwd_speedup" in report:
753
+ gates["manual_fwd_speedup_ge_1p0526"] = report["manual_fwd_speedup"] >= (1.0 / 0.95)
754
+ if "manual_fwdbwd_speedup" in report:
755
+ gates["manual_fwdbwd_speedup_ge_1p0526"] = report["manual_fwdbwd_speedup"] >= (1.0 / 0.95)
756
+ if "package_fwd_speedup" in report:
757
+ gates["package_fwd_speedup_ge_1p0526"] = report["package_fwd_speedup"] >= (1.0 / 0.95)
758
+ if report.get("sdpa_peak_bytes", 0) > 0:
759
+ gates["package_peak_memory_le_sdpa_plus_3pct"] = report["package_peak_bytes"] <= int(report["sdpa_peak_bytes"] * 1.03)
760
+ if "package_fwdbwd_speedup" in report:
761
+ gates["package_fwdbwd_speedup_ge_1p0526"] = report["package_fwdbwd_speedup"] >= (1.0 / 0.95)
762
+ if "torch_flex_fwd_speedup" in report:
763
+ vals = [v for by_block in report["torch_flex_fwd_speedup"].values() for v in by_block.values() if isinstance(v, float)]
764
+ gates["torch_flex_fwd_speedup_ge_1p0526"] = bool(vals) and max(vals) >= (1.0 / 0.95)
765
+ if "torch_flex_fwdbwd_speedup" in report:
766
+ vals = [v for by_block in report["torch_flex_fwdbwd_speedup"].values() for v in by_block.values() if isinstance(v, float)]
767
+ gates["torch_flex_fwdbwd_speedup_ge_1p0526"] = bool(vals) and max(vals) >= (1.0 / 0.95)
768
+ report["gates"] = gates
769
+
770
+ text = json.dumps(report, indent=2)
771
+ if args.output:
772
+ Path(args.output).write_text(text + "\n", encoding="utf-8")
773
+ print(text)
774
+ if args.require_gates and not all(gates.values()):
775
+ raise SystemExit("one or more Flex attention acceptance gates failed")
776
+
777
+
778
+ if __name__ == "__main__":
779
+ main()
benchmarks/shape_matrix.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ BENCH = ROOT / "benchmarks" / "benchmark.py"
13
+
14
+
15
+ DEFAULT_SHAPES = [
16
+ {"name": "pi052_b4_p700_k5_c50", "batch": 4, "prefix_len": 700, "action_blocks": 5, "action_block_size": 50},
17
+ {"name": "pi052_b2_p700_k5_c50", "batch": 2, "prefix_len": 700, "action_blocks": 5, "action_block_size": 50},
18
+ {"name": "pi052_b1_p700_k5_c50", "batch": 1, "prefix_len": 700, "action_blocks": 5, "action_block_size": 50},
19
+ {"name": "pi052_b4_p512_k5_c50", "batch": 4, "prefix_len": 512, "action_blocks": 5, "action_block_size": 50},
20
+ {"name": "pi052_b4_p896_k5_c50", "batch": 4, "prefix_len": 896, "action_blocks": 5, "action_block_size": 50},
21
+ {"name": "pi052_b4_p700_k1_c50", "batch": 4, "prefix_len": 700, "action_blocks": 1, "action_block_size": 50},
22
+ {"name": "pi052_b4_p700_k8_c50", "batch": 4, "prefix_len": 700, "action_blocks": 8, "action_block_size": 50},
23
+ ]
24
+
25
+
26
+ def parse_presets(text: str) -> list[str]:
27
+ if text == "a100":
28
+ return [
29
+ "default",
30
+ "torch_default_explicit",
31
+ "a100_d256_bwd_32x64",
32
+ "a100_d256_bwd_32x128",
33
+ "a100_d256_bwd_64x64",
34
+ "a100_d256_contig_safe",
35
+ "a100_d256_contig_prescale",
36
+ "a100_d256_contig_write_dq_false",
37
+ ]
38
+ if text == "consumer":
39
+ return [
40
+ "default",
41
+ "torch_default_explicit",
42
+ "a100_d256_bwd_32x64",
43
+ "a100_d256_bwd_64x128",
44
+ "a100_d256_contig_safe",
45
+ "a100_d256_contig_prescale",
46
+ ]
47
+ return [x.strip() for x in text.split(",") if x.strip()]
48
+
49
+
50
+ def parse_block_sizes(text: str) -> list[tuple[int, int]]:
51
+ if text == "default":
52
+ return [(128, 128)]
53
+ if text == "a100":
54
+ return [(64, 64), (64, 128), (128, 64), (128, 128)]
55
+ if text == "full":
56
+ return [(q, kv) for q in (16, 32, 64, 128) for kv in (32, 64, 128)]
57
+ out = []
58
+ for item in text.split(","):
59
+ item = item.strip().lower()
60
+ if not item:
61
+ continue
62
+ q, kv = item.split("x", 1)
63
+ out.append((int(q), int(kv)))
64
+ return out
65
+
66
+
67
+ def load_shapes(path: str | None) -> list[dict]:
68
+ if path is None:
69
+ return DEFAULT_SHAPES
70
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
71
+ if not isinstance(data, list):
72
+ raise SystemExit("shape file must be a JSON list")
73
+ return data
74
+
75
+
76
+ def extract_json(stdout: str) -> dict:
77
+ start = stdout.find("{")
78
+ end = stdout.rfind("}")
79
+ if start < 0 or end < start:
80
+ return {"raw_stdout": stdout}
81
+ return json.loads(stdout[start : end + 1])
82
+
83
+
84
+ def main() -> int:
85
+ parser = argparse.ArgumentParser()
86
+ parser.add_argument("--device", default="cuda")
87
+ parser.add_argument("--dtype", choices=["bf16", "fp32"], default="bf16")
88
+ parser.add_argument("--heads", type=int, default=8)
89
+ parser.add_argument("--kv-heads", type=int, default=None)
90
+ parser.add_argument("--head-dim", type=int, default=256)
91
+ parser.add_argument("--warmup", type=int, default=5)
92
+ parser.add_argument("--iters", type=int, default=10)
93
+ parser.add_argument("--mode", choices=["fwd", "fwdbwd", "all"], default="all")
94
+ parser.add_argument("--backend", default="torch-flex", help="comma-separated subset of {package, torch-flex, manual} or 'all'")
95
+ parser.add_argument("--presets", default="consumer", help="'consumer', 'a100', or comma-separated preset names")
96
+ parser.add_argument("--block-mask-sizes", default="default", help="'default', 'a100', 'full', or comma-separated QxKV sizes")
97
+ parser.add_argument("--shapes-json")
98
+ parser.add_argument("--output", default=str(ROOT / "benchmarks" / "matrix_results.jsonl"))
99
+ parser.add_argument("--fail-fast", action="store_true")
100
+ args = parser.parse_args()
101
+
102
+ shapes = load_shapes(args.shapes_json)
103
+ presets = parse_presets(args.presets)
104
+ block_sizes = parse_block_sizes(args.block_mask_sizes)
105
+ output = Path(args.output)
106
+ output.parent.mkdir(parents=True, exist_ok=True)
107
+ output.write_text("", encoding="utf-8")
108
+
109
+ for shape in shapes:
110
+ for preset in presets:
111
+ for block_q, block_kv in block_sizes:
112
+ cmd = [
113
+ sys.executable,
114
+ str(BENCH),
115
+ "--device",
116
+ args.device,
117
+ "--dtype",
118
+ args.dtype,
119
+ "--batch",
120
+ str(shape["batch"]),
121
+ "--heads",
122
+ str(args.heads),
123
+ "--kv-heads",
124
+ str(args.kv_heads if args.kv_heads is not None else args.heads),
125
+ "--head-dim",
126
+ str(args.head_dim),
127
+ "--prefix-len",
128
+ str(shape["prefix_len"]),
129
+ "--action-blocks",
130
+ str(shape["action_blocks"]),
131
+ "--action-block-size",
132
+ str(shape["action_block_size"]),
133
+ "--warmup",
134
+ str(args.warmup),
135
+ "--iters",
136
+ str(args.iters),
137
+ "--mode",
138
+ args.mode,
139
+ "--backend",
140
+ args.backend,
141
+ "--flex-preset",
142
+ preset,
143
+ "--block-mask-q",
144
+ str(block_q),
145
+ "--block-mask-kv",
146
+ str(block_kv),
147
+ ]
148
+ proc = subprocess.run(cmd, cwd=ROOT.parent, text=True, capture_output=True)
149
+ row = {
150
+ "shape_name": shape.get("name", ""),
151
+ "preset": preset,
152
+ "block_mask": f"{block_q}x{block_kv}",
153
+ "returncode": proc.returncode,
154
+ }
155
+ if proc.returncode == 0:
156
+ row.update(extract_json(proc.stdout))
157
+ else:
158
+ row["stdout"] = proc.stdout[-4000:]
159
+ row["stderr"] = proc.stderr[-4000:]
160
+ with output.open("a", encoding="utf-8") as f:
161
+ f.write(json.dumps(row, sort_keys=True) + "\n")
162
+ print(json.dumps(row, sort_keys=True))
163
+ if proc.returncode != 0 and args.fail_fast:
164
+ return proc.returncode
165
+ return 0
166
+
167
+
168
+ if __name__ == "__main__":
169
+ raise SystemExit(main())
build.toml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [general]
2
+ name = "flashrt-flex-attention-train"
3
+ license = "Apache-2.0"
4
+ version = 1
5
+ backends = ["cuda"]
6
+
7
+ [general.cuda]
8
+ minver = "12.8"
9
+
10
+ [general.hub]
11
+ repo-id = "flashrt/flashrt-flex-attention-train"
12
+
13
+ [torch]
14
+ include = ["csrc"]
15
+ src = [
16
+ "torch-ext/torch_binding.cpp",
17
+ "torch-ext/torch_binding.h",
18
+ ]
19
+
20
+ [kernel.flashrt_flex_attention_train_stub]
21
+ backend = "cuda"
22
+ depends = ["torch"]
23
+ include = ["csrc"]
24
+ cuda-minver = "12.8"
25
+ src = [
26
+ "csrc/stub.cu",
27
+ "csrc/stub.cuh",
28
+ ]
build/torch-universal/flashrt_flex_attention_train/__init__.py ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FlashRT Flex-style block-sparse attention training API.
2
+
3
+ The public function implements the PI052 prefix/action mask pattern:
4
+
5
+ * prefix query rows use the original K/V tensors, so prefix losses keep normal
6
+ gradients into prefix K/V;
7
+ * action query rows read detached prefix K/V plus normal action K/V by default,
8
+ matching the current training semantics.
9
+
10
+ Unsupported shapes route to the SDPA reference path. Native CUDA kernels are
11
+ not exposed until a shape-specialized implementation beats SDPA on the target
12
+ A100/5090 validation matrix.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+ import torch
20
+ import torch.nn.functional as F
21
+
22
+ try:
23
+ from ._ops import ops
24
+
25
+ _HAS_OPS = hasattr(ops, "_flashrt_training_package_marker")
26
+ except Exception: # source-tree tests before kernel-builder creates _ops.py
27
+ ops = None
28
+ _HAS_OPS = False
29
+
30
+
31
+ MASK_VALUE_F32 = -2.3819763e38
32
+
33
+
34
+ def _use_ops(namespace_ops) -> None:
35
+ """Install a manually built extension (dev/testing path)."""
36
+ global ops, _HAS_OPS
37
+ ops = namespace_ops
38
+ _HAS_OPS = hasattr(ops, "_flashrt_training_package_marker")
39
+
40
+
41
+ def _check_qkv(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> None:
42
+ if q.dim() != 4 or k.dim() != 4 or v.dim() != 4:
43
+ raise ValueError("q, k, and v must be shaped (B, H, S, D)")
44
+ if q.shape[0] != k.shape[0] or q.shape[0] != v.shape[0]:
45
+ raise ValueError("q, k, and v batch dimensions must match")
46
+ if k.shape != v.shape:
47
+ raise ValueError("k and v shapes must match")
48
+ if q.shape[2] != k.shape[2] or q.shape[3] != k.shape[3]:
49
+ raise ValueError("q, k, and v sequence/head_dim dimensions must match")
50
+ if q.device != k.device or q.device != v.device:
51
+ raise ValueError("q, k, and v must be on the same device")
52
+
53
+
54
+ def _as_valid(mask: Optional[torch.Tensor], batch: int, length: int, device: torch.device) -> torch.Tensor:
55
+ if mask is None:
56
+ return torch.ones((batch, length), dtype=torch.bool, device=device)
57
+ if mask.shape != (batch, length):
58
+ raise ValueError(f"mask must be shaped {(batch, length)}, got {tuple(mask.shape)}")
59
+ return mask.to(device=device, dtype=torch.bool)
60
+
61
+
62
+ def build_block_sparse_bool_masks(
63
+ prefix_valid: Optional[torch.Tensor],
64
+ prefix_att: Optional[torch.Tensor],
65
+ *,
66
+ batch: int,
67
+ prefix_len: int,
68
+ action_len: int,
69
+ action_block_size: int,
70
+ non_fast_prefix_len: Optional[int] = None,
71
+ action_valid: Optional[torch.Tensor] = None,
72
+ device: Optional[torch.device] = None,
73
+ ) -> tuple[torch.Tensor, torch.Tensor]:
74
+ """Build boolean masks for the split FlexAttention SDPA calls.
75
+
76
+ Returns ``(prefix_rows, action_rows)`` with shapes ``(B, P, S)`` and
77
+ ``(B, A, S)``. Boolean True means the key/value position is visible.
78
+
79
+ ``prefix_att`` follows Lerobot's cumulative-block convention: prefix key
80
+ ``j`` is visible to prefix query ``i`` when ``cumsum(prefix_att)[j] <=
81
+ cumsum(prefix_att)[i]`` and both rows are valid. When omitted, prefix rows
82
+ attend to all valid prefix tokens.
83
+ """
84
+ if action_block_size <= 0:
85
+ raise ValueError("action_block_size must be positive")
86
+ if prefix_len < 0 or action_len < 0:
87
+ raise ValueError("prefix_len and action_len must be non-negative")
88
+ total_len = prefix_len + action_len
89
+ dev = device
90
+ if dev is None:
91
+ for t in (prefix_valid, prefix_att, action_valid):
92
+ if t is not None:
93
+ dev = t.device
94
+ break
95
+ if dev is None:
96
+ dev = torch.device("cpu")
97
+
98
+ p_valid = _as_valid(prefix_valid, batch, prefix_len, dev)
99
+ a_valid = _as_valid(action_valid, batch, action_len, dev)
100
+
101
+ if prefix_att is None:
102
+ prefix_rows = p_valid[:, :, None] & p_valid[:, None, :]
103
+ else:
104
+ if prefix_att.shape != (batch, prefix_len):
105
+ raise ValueError(
106
+ f"prefix_att must be shaped {(batch, prefix_len)}, got {tuple(prefix_att.shape)}"
107
+ )
108
+ cum = torch.cumsum(prefix_att.to(device=dev, dtype=torch.long), dim=1)
109
+ prefix_rows = (cum[:, None, :] <= cum[:, :, None]) & p_valid[:, :, None] & p_valid[:, None, :]
110
+
111
+ prefix_pad = torch.zeros((batch, prefix_len, action_len), dtype=torch.bool, device=dev)
112
+ prefix_rows = torch.cat([prefix_rows, prefix_pad], dim=2)
113
+
114
+ nf = prefix_len if non_fast_prefix_len is None else int(non_fast_prefix_len)
115
+ nf = max(0, min(nf, prefix_len))
116
+ action_to_prefix = torch.zeros((batch, action_len, prefix_len), dtype=torch.bool, device=dev)
117
+ if nf > 0:
118
+ action_to_prefix[:, :, :nf] = p_valid[:, None, :nf]
119
+ action_to_prefix &= a_valid[:, :, None]
120
+
121
+ q_block = torch.arange(action_len, device=dev) // int(action_block_size)
122
+ kv_block = q_block
123
+ action_block = q_block[:, None] == kv_block[None, :]
124
+ action_block = action_block[None, :, :].expand(batch, -1, -1)
125
+ action_block = action_block & a_valid[:, :, None] & a_valid[:, None, :]
126
+ action_rows = torch.cat([action_to_prefix, action_block], dim=2)
127
+
128
+ if prefix_rows.shape != (batch, prefix_len, total_len):
129
+ raise AssertionError("internal prefix mask shape error")
130
+ if action_rows.shape != (batch, action_len, total_len):
131
+ raise AssertionError("internal action mask shape error")
132
+ return prefix_rows, action_rows
133
+
134
+
135
+ def _bool_to_sdpa_mask(mask: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
136
+ value = MASK_VALUE_F32
137
+ if q.dtype.is_floating_point:
138
+ finfo = torch.finfo(q.dtype)
139
+ value = max(MASK_VALUE_F32, finfo.min)
140
+ return torch.where(
141
+ mask[:, None, :, :],
142
+ torch.zeros((), dtype=q.dtype, device=q.device),
143
+ torch.full((), value, dtype=q.dtype, device=q.device),
144
+ )
145
+
146
+
147
+ def _slice_attention_mask(
148
+ attention_mask: torch.Tensor,
149
+ start: int,
150
+ end: int,
151
+ q: torch.Tensor,
152
+ ) -> torch.Tensor:
153
+ if attention_mask.dim() == 3:
154
+ mask = attention_mask[:, start:end, :]
155
+ if mask.dtype == torch.bool:
156
+ return mask[:, None, :, :]
157
+ return mask[:, None, :, :].to(dtype=q.dtype)
158
+ if attention_mask.dim() == 4:
159
+ mask = attention_mask[:, :, start:end, :]
160
+ return mask if mask.dtype == torch.bool else mask.to(dtype=q.dtype)
161
+ raise ValueError("attention_mask must be (B, S, S) or (B, 1|H, S, S)")
162
+
163
+
164
+ def _sdpa(
165
+ q: torch.Tensor,
166
+ k: torch.Tensor,
167
+ v: torch.Tensor,
168
+ mask: Optional[torch.Tensor],
169
+ *,
170
+ scale: Optional[float],
171
+ dropout_p: float,
172
+ enable_gqa: bool,
173
+ ) -> torch.Tensor:
174
+ kwargs = {"attn_mask": mask, "dropout_p": float(dropout_p), "scale": scale}
175
+ if enable_gqa:
176
+ kwargs["enable_gqa"] = True
177
+ try:
178
+ return F.scaled_dot_product_attention(q, k, v, **kwargs)
179
+ except TypeError:
180
+ kwargs.pop("enable_gqa", None)
181
+ return F.scaled_dot_product_attention(q, k, v, **kwargs)
182
+
183
+
184
+ def reference_flex_attention(
185
+ q: torch.Tensor,
186
+ k: torch.Tensor,
187
+ v: torch.Tensor,
188
+ *,
189
+ prefix_len: int,
190
+ action_block_size: int,
191
+ attention_mask: Optional[torch.Tensor] = None,
192
+ prefix_valid: Optional[torch.Tensor] = None,
193
+ prefix_att: Optional[torch.Tensor] = None,
194
+ non_fast_prefix_len: Optional[int] = None,
195
+ action_valid: Optional[torch.Tensor] = None,
196
+ detach_prefix_kv_for_action: bool = True,
197
+ scale: Optional[float] = None,
198
+ dropout_p: float = 0.0,
199
+ enable_gqa: Optional[bool] = None,
200
+ ) -> torch.Tensor:
201
+ """SDPA reference for the PI052 FlexAttention replacement shape.
202
+
203
+ Args:
204
+ q, k, v: ``(B, Hq/Hkv, S, D)`` tensors.
205
+ prefix_len: number of prefix rows/columns at the start of sequence.
206
+ action_block_size: size of each block-diagonal action segment.
207
+ attention_mask: optional prebuilt additive or boolean full mask.
208
+ prefix_valid: optional ``(B, P)`` valid prefix positions.
209
+ prefix_att: optional ``(B, P)`` cumulative-block markers.
210
+ non_fast_prefix_len: prefix columns visible to action rows.
211
+ action_valid: optional ``(B, A)`` valid action positions.
212
+ detach_prefix_kv_for_action: detach prefix K/V on the action-row path.
213
+ scale: SDPA scale. Defaults to ``D ** -0.5``.
214
+ dropout_p: SDPA dropout probability.
215
+ enable_gqa: pass SDPA GQA mode when q heads and kv heads differ.
216
+ """
217
+ _check_qkv(q, k, v)
218
+ batch, _, total_len, head_dim = q.shape
219
+ if not (0 <= int(prefix_len) <= total_len):
220
+ raise ValueError("prefix_len must be in [0, S]")
221
+ prefix_len = int(prefix_len)
222
+ action_len = total_len - prefix_len
223
+ if scale is None:
224
+ scale = head_dim**-0.5
225
+ if enable_gqa is None:
226
+ enable_gqa = q.shape[1] != k.shape[1]
227
+
228
+ q_prefix = q[:, :, :prefix_len, :]
229
+ q_action = q[:, :, prefix_len:, :]
230
+ k_prefix = k[:, :, :prefix_len, :]
231
+ k_action = k[:, :, prefix_len:, :]
232
+ v_prefix = v[:, :, :prefix_len, :]
233
+ v_action = v[:, :, prefix_len:, :]
234
+
235
+ if attention_mask is None:
236
+ prefix_bool, action_bool = build_block_sparse_bool_masks(
237
+ prefix_valid,
238
+ prefix_att,
239
+ batch=batch,
240
+ prefix_len=prefix_len,
241
+ action_len=action_len,
242
+ action_block_size=action_block_size,
243
+ non_fast_prefix_len=non_fast_prefix_len,
244
+ action_valid=action_valid,
245
+ device=q.device,
246
+ )
247
+ prefix_mask = _bool_to_sdpa_mask(prefix_bool, q)
248
+ action_mask = _bool_to_sdpa_mask(action_bool, q)
249
+ else:
250
+ prefix_mask = _slice_attention_mask(attention_mask, 0, prefix_len, q)
251
+ action_mask = _slice_attention_mask(attention_mask, prefix_len, total_len, q)
252
+
253
+ out_parts = []
254
+ if prefix_len:
255
+ out_parts.append(
256
+ _sdpa(
257
+ q_prefix,
258
+ k,
259
+ v,
260
+ prefix_mask,
261
+ scale=scale,
262
+ dropout_p=dropout_p,
263
+ enable_gqa=bool(enable_gqa),
264
+ )
265
+ )
266
+ if action_len:
267
+ prefix_k = k_prefix.detach() if detach_prefix_kv_for_action else k_prefix
268
+ prefix_v = v_prefix.detach() if detach_prefix_kv_for_action else v_prefix
269
+ k_for_action = torch.cat([prefix_k, k_action], dim=2)
270
+ v_for_action = torch.cat([prefix_v, v_action], dim=2)
271
+ out_parts.append(
272
+ _sdpa(
273
+ q_action,
274
+ k_for_action,
275
+ v_for_action,
276
+ action_mask,
277
+ scale=scale,
278
+ dropout_p=dropout_p,
279
+ enable_gqa=bool(enable_gqa),
280
+ )
281
+ )
282
+ if not out_parts:
283
+ return q.new_empty(q.shape)
284
+ return torch.cat(out_parts, dim=2) if len(out_parts) == 2 else out_parts[0]
285
+
286
+
287
+ def _manual_attention_part(qs, ks, vs, mask, scale):
288
+ """Materialized-logits attention part: cuBLAS GEMMs + fused masked softmax.
289
+
290
+ Same math as SDPA with an additive mask (fp32 softmax; logits stored in
291
+ the io dtype between the GEMM and the softmax). Grouped queries run as a
292
+ strided batched GEMM over the KV heads, so a 1-head K/V is never
293
+ repeated. At PI052 training shapes (GQA 8:1, D=256, bf16) this beats
294
+ both SDPA-with-dense-mask (2.3-3.1x) and the best FlexAttention
295
+ configuration (1.4-2.9x) on fwd+bwd — see benchmarks/RESULTS.md.
296
+ """
297
+ B, H, Sq, D = qs.shape
298
+ Hk = ks.shape[1]
299
+ if Hk != H:
300
+ g = H // Hk
301
+ q2 = qs.reshape(B, Hk, g * Sq, D)
302
+ logits = (q2 @ ks.transpose(-1, -2)).reshape(B, H, Sq, -1)
303
+ else:
304
+ logits = qs @ ks.transpose(-1, -2)
305
+ logits = logits * scale
306
+ if mask is not None:
307
+ logits = logits + mask
308
+ p = logits.float().softmax(dim=-1).to(qs.dtype)
309
+ if Hk != H:
310
+ out = (p.reshape(B, Hk, g * Sq, -1) @ vs).reshape(B, H, Sq, D)
311
+ else:
312
+ out = p @ vs
313
+ return out
314
+
315
+
316
+ # Public alias: integrations (e.g. the LeRobot pi052 flag) consume the raw
317
+ # per-part op and assemble masks/splits themselves.
318
+ manual_attention_part = _manual_attention_part
319
+
320
+ _manual_part_compiled = None
321
+
322
+
323
+ def _get_manual_part():
324
+ global _manual_part_compiled
325
+ if _manual_part_compiled is None:
326
+ _manual_part_compiled = torch.compile(_manual_attention_part, dynamic=False)
327
+ return _manual_part_compiled
328
+
329
+
330
+ def manual_attention(
331
+ q: torch.Tensor,
332
+ k: torch.Tensor,
333
+ v: torch.Tensor,
334
+ *,
335
+ prefix_len: int,
336
+ action_block_size: int,
337
+ attention_mask: Optional[torch.Tensor] = None,
338
+ prefix_valid: Optional[torch.Tensor] = None,
339
+ prefix_att: Optional[torch.Tensor] = None,
340
+ non_fast_prefix_len: Optional[int] = None,
341
+ action_valid: Optional[torch.Tensor] = None,
342
+ detach_prefix_kv_for_action: bool = True,
343
+ scale: Optional[float] = None,
344
+ dropout_p: float = 0.0,
345
+ compile_part: bool = True,
346
+ ) -> torch.Tensor:
347
+ """Materialized-logits implementation of :func:`reference_flex_attention`.
348
+
349
+ Same mask semantics and prefix/action split; each part runs through
350
+ :func:`_manual_attention_part` instead of SDPA. ``dropout_p`` must be 0
351
+ (training attention dropout is unused in PI052); other values raise so
352
+ callers fall back explicitly.
353
+ """
354
+ if dropout_p:
355
+ raise ValueError("manual_attention does not support dropout; use the reference path")
356
+ _check_qkv(q, k, v)
357
+ batch, _, total_len, head_dim = q.shape
358
+ if not (0 <= int(prefix_len) <= total_len):
359
+ raise ValueError("prefix_len must be in [0, S]")
360
+ prefix_len = int(prefix_len)
361
+ action_len = total_len - prefix_len
362
+ if scale is None:
363
+ scale = head_dim**-0.5
364
+
365
+ if attention_mask is None:
366
+ prefix_bool, action_bool = build_block_sparse_bool_masks(
367
+ prefix_valid,
368
+ prefix_att,
369
+ batch=batch,
370
+ prefix_len=prefix_len,
371
+ action_len=action_len,
372
+ action_block_size=action_block_size,
373
+ non_fast_prefix_len=non_fast_prefix_len,
374
+ action_valid=action_valid,
375
+ device=q.device,
376
+ )
377
+ prefix_mask = _bool_to_sdpa_mask(prefix_bool, q)
378
+ action_mask = _bool_to_sdpa_mask(action_bool, q)
379
+ else:
380
+ prefix_mask = _slice_attention_mask(attention_mask, 0, prefix_len, q)
381
+ action_mask = _slice_attention_mask(attention_mask, prefix_len, total_len, q)
382
+ if prefix_mask.dtype == torch.bool:
383
+ prefix_mask = _bool_to_sdpa_mask(prefix_mask[:, 0], q)
384
+ if action_mask.dtype == torch.bool:
385
+ action_mask = _bool_to_sdpa_mask(action_mask[:, 0], q)
386
+
387
+ part = _get_manual_part() if compile_part else _manual_attention_part
388
+ out_parts = []
389
+ if prefix_len:
390
+ out_parts.append(part(q[:, :, :prefix_len, :], k, v, prefix_mask, scale))
391
+ if action_len:
392
+ k_prefix = k[:, :, :prefix_len, :]
393
+ v_prefix = v[:, :, :prefix_len, :]
394
+ if detach_prefix_kv_for_action:
395
+ k_prefix = k_prefix.detach()
396
+ v_prefix = v_prefix.detach()
397
+ k_for_action = torch.cat([k_prefix, k[:, :, prefix_len:, :]], dim=2)
398
+ v_for_action = torch.cat([v_prefix, v[:, :, prefix_len:, :]], dim=2)
399
+ out_parts.append(part(q[:, :, prefix_len:, :], k_for_action, v_for_action, action_mask, scale))
400
+ if not out_parts:
401
+ return q.new_empty(q.shape)
402
+ return torch.cat(out_parts, dim=2) if len(out_parts) == 2 else out_parts[0]
403
+
404
+
405
+ def flex_attention(
406
+ q: torch.Tensor,
407
+ k: torch.Tensor,
408
+ v: torch.Tensor,
409
+ *,
410
+ prefix_len: int,
411
+ action_block_size: int,
412
+ attention_mask: Optional[torch.Tensor] = None,
413
+ prefix_valid: Optional[torch.Tensor] = None,
414
+ prefix_att: Optional[torch.Tensor] = None,
415
+ non_fast_prefix_len: Optional[int] = None,
416
+ action_valid: Optional[torch.Tensor] = None,
417
+ detach_prefix_kv_for_action: bool = True,
418
+ scale: Optional[float] = None,
419
+ dropout_p: float = 0.0,
420
+ enable_gqa: Optional[bool] = None,
421
+ force_fallback: bool = False,
422
+ impl: str = "sdpa",
423
+ ) -> torch.Tensor:
424
+ """Flex-style block-sparse attention.
425
+
426
+ ``impl="sdpa"`` (default) keeps the SDPA reference path;
427
+ ``impl="manual"`` routes through the materialized-logits
428
+ implementation; ``impl="auto"`` picks manual only where it has been
429
+ measured to win end-to-end — consumer Blackwell (sm120-class) with
430
+ no dropout. On A100 (sm80) the manual math wins microbenches but
431
+ loses training-step integration, and on H100/H200 (sm90) the fused
432
+ FMHA kernels win outright, so auto keeps SDPA there.
433
+ """
434
+ _ = force_fallback
435
+ if impl == "auto":
436
+ sm120 = q.is_cuda and torch.cuda.get_device_capability(q.device)[0] == 12
437
+ impl = "manual" if (sm120 and not dropout_p) else "sdpa"
438
+ if impl == "manual":
439
+ return manual_attention(
440
+ q,
441
+ k,
442
+ v,
443
+ prefix_len=prefix_len,
444
+ action_block_size=action_block_size,
445
+ attention_mask=attention_mask,
446
+ prefix_valid=prefix_valid,
447
+ prefix_att=prefix_att,
448
+ non_fast_prefix_len=non_fast_prefix_len,
449
+ action_valid=action_valid,
450
+ detach_prefix_kv_for_action=detach_prefix_kv_for_action,
451
+ scale=scale,
452
+ dropout_p=dropout_p,
453
+ )
454
+ return reference_flex_attention(
455
+ q,
456
+ k,
457
+ v,
458
+ prefix_len=prefix_len,
459
+ action_block_size=action_block_size,
460
+ attention_mask=attention_mask,
461
+ prefix_valid=prefix_valid,
462
+ prefix_att=prefix_att,
463
+ non_fast_prefix_len=non_fast_prefix_len,
464
+ action_valid=action_valid,
465
+ detach_prefix_kv_for_action=detach_prefix_kv_for_action,
466
+ scale=scale,
467
+ dropout_p=dropout_p,
468
+ enable_gqa=enable_gqa,
469
+ )
470
+
471
+
472
+ def flex_attention_forward(*args, **kwargs) -> torch.Tensor:
473
+ """Forward-only compatibility wrapper."""
474
+ return flex_attention(*args, **kwargs)
475
+
476
+
477
+ def backend_marker(x: torch.Tensor) -> torch.Tensor:
478
+ if ops is None:
479
+ return x
480
+ return ops._flashrt_training_package_marker(x)
481
+
482
+
483
+ __all__ = [
484
+ "MASK_VALUE_F32",
485
+ "backend_marker",
486
+ "build_block_sparse_bool_masks",
487
+ "flex_attention",
488
+ "flex_attention_forward",
489
+ "manual_attention",
490
+ "manual_attention_part",
491
+ "reference_flex_attention",
492
+ ]
csrc/README.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # csrc
2
+
3
+ CUDA extension placeholder for the FlexAttention replacement package.
4
+
5
+ The v1 package currently exposes the stable Python Tensor API and SDPA-backed
6
+ autograd fallback. Native CUDA forward/backward kernels should be added here
7
+ without changing the public functions in `torch-ext/flashrt_flex_attention_train`.
csrc/stub.cu ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ #include "stub.cuh"
csrc/stub.cuh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ #pragma once
flake.nix ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ description = "Flake for FlashRT Flex attention training kernels";
3
+
4
+ inputs = {
5
+ kernel-builder.url = "github:huggingface/kernels/432702bfbfbb17d3a1bd2c2743d004e21e769ab7";
6
+ };
7
+
8
+ outputs =
9
+ {
10
+ self,
11
+ kernel-builder,
12
+ }:
13
+ kernel-builder.lib.genKernelFlakeOutputs {
14
+ inherit self;
15
+ path = ./.;
16
+ };
17
+ }
tests/test_flashrt_flex_attention_train.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import importlib
6
+ import sys
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+
11
+
12
+ def load_ops(backend: str, artifact: str | None):
13
+ if backend == "source":
14
+ sys.path.insert(0, "flashrt-flex-attention-train/torch-ext")
15
+ try:
16
+ return importlib.import_module("flashrt_flex_attention_train")
17
+ finally:
18
+ sys.path.remove("flashrt-flex-attention-train/torch-ext")
19
+ if artifact:
20
+ sys.path.insert(0, artifact)
21
+ try:
22
+ return importlib.import_module("flashrt_flex_attention_train")
23
+ finally:
24
+ if artifact:
25
+ sys.path.remove(artifact)
26
+
27
+
28
+ def _shape(mode: str):
29
+ if mode == "full" and torch.cuda.is_available():
30
+ return "cuda", torch.bfloat16, 2, 8, 13, 6, 256
31
+ return "cpu", torch.float32, 2, 2, 5, 4, 16
32
+
33
+
34
+ def test_matches_explicit_sdpa(flex_ops, mode: str) -> None:
35
+ device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
36
+ torch.manual_seed(11)
37
+ q = torch.randn(bsz, heads, prefix + action, dim, device=device, dtype=dtype, requires_grad=True)
38
+ k = torch.randn_like(q, requires_grad=True)
39
+ v = torch.randn_like(q, requires_grad=True)
40
+ prefix_valid = torch.ones(bsz, prefix, device=device, dtype=torch.bool)
41
+ prefix_valid[0, -1] = False
42
+ prefix_att = torch.zeros(bsz, prefix, device=device, dtype=torch.bool)
43
+ prefix_att[:, prefix // 2 :] = True
44
+ action_valid = torch.ones(bsz, action, device=device, dtype=torch.bool)
45
+ action_valid[1, -1] = False
46
+
47
+ out = flex_ops.flex_attention(
48
+ q,
49
+ k,
50
+ v,
51
+ prefix_len=prefix,
52
+ action_block_size=2,
53
+ prefix_valid=prefix_valid,
54
+ prefix_att=prefix_att,
55
+ action_valid=action_valid,
56
+ non_fast_prefix_len=prefix - 1,
57
+ )
58
+
59
+ pm, am = flex_ops.build_block_sparse_bool_masks(
60
+ prefix_valid,
61
+ prefix_att,
62
+ batch=bsz,
63
+ prefix_len=prefix,
64
+ action_len=action,
65
+ action_block_size=2,
66
+ non_fast_prefix_len=prefix - 1,
67
+ action_valid=action_valid,
68
+ device=q.device,
69
+ )
70
+ pm = torch.where(
71
+ pm[:, None],
72
+ torch.zeros((), device=q.device, dtype=q.dtype),
73
+ torch.full((), flex_ops.MASK_VALUE_F32, device=q.device, dtype=q.dtype),
74
+ )
75
+ am = torch.where(
76
+ am[:, None],
77
+ torch.zeros((), device=q.device, dtype=q.dtype),
78
+ torch.full((), flex_ops.MASK_VALUE_F32, device=q.device, dtype=q.dtype),
79
+ )
80
+ expected_p = F.scaled_dot_product_attention(q[:, :, :prefix], k, v, attn_mask=pm, scale=dim**-0.5)
81
+ kd = torch.cat([k[:, :, :prefix].detach(), k[:, :, prefix:]], dim=2)
82
+ vd = torch.cat([v[:, :, :prefix].detach(), v[:, :, prefix:]], dim=2)
83
+ expected_a = F.scaled_dot_product_attention(q[:, :, prefix:], kd, vd, attn_mask=am, scale=dim**-0.5)
84
+ expected = torch.cat([expected_p, expected_a], dim=2)
85
+ tol = 2e-3 if dtype == torch.bfloat16 else 1e-5
86
+ torch.testing.assert_close(out, expected, atol=tol, rtol=tol)
87
+
88
+
89
+ def test_detached_prefix_semantics(flex_ops, mode: str) -> None:
90
+ device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
91
+ torch.manual_seed(17)
92
+ q = torch.randn(bsz, heads, prefix + action, dim, device=device, dtype=dtype, requires_grad=True)
93
+ k = torch.randn_like(q, requires_grad=True)
94
+ v = torch.randn_like(q, requires_grad=True)
95
+ out = flex_ops.flex_attention(q, k, v, prefix_len=prefix, action_block_size=2)
96
+ out[:, :, prefix:].float().square().mean().backward()
97
+ assert torch.count_nonzero(k.grad[:, :, :prefix]) == 0
98
+ assert torch.count_nonzero(v.grad[:, :, :prefix]) == 0
99
+ assert torch.count_nonzero(k.grad[:, :, prefix:]) > 0
100
+ assert torch.count_nonzero(v.grad[:, :, prefix:]) > 0
101
+
102
+
103
+ def test_dense_attention_mask_path(flex_ops, mode: str) -> None:
104
+ device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
105
+ torch.manual_seed(23)
106
+ q = torch.randn(bsz, heads, prefix + action, dim, device=device, dtype=dtype)
107
+ k = torch.randn_like(q)
108
+ v = torch.randn_like(q)
109
+ pm, am = flex_ops.build_block_sparse_bool_masks(
110
+ None,
111
+ None,
112
+ batch=bsz,
113
+ prefix_len=prefix,
114
+ action_len=action,
115
+ action_block_size=2,
116
+ device=q.device,
117
+ )
118
+ full = torch.cat([pm, am], dim=1)
119
+ mask = torch.where(
120
+ full[:, None],
121
+ torch.zeros((), device=q.device, dtype=q.dtype),
122
+ torch.full((), flex_ops.MASK_VALUE_F32, device=q.device, dtype=q.dtype),
123
+ )
124
+ out_from_dense = flex_ops.flex_attention(q, k, v, prefix_len=prefix, action_block_size=2, attention_mask=mask)
125
+ out_from_parts = flex_ops.flex_attention(q, k, v, prefix_len=prefix, action_block_size=2)
126
+ tol = 2e-3 if dtype == torch.bfloat16 else 1e-5
127
+ torch.testing.assert_close(out_from_dense, out_from_parts, atol=tol, rtol=tol)
128
+
129
+
130
+ def test_manual_matches_reference(flex_ops, mode: str) -> None:
131
+ device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
132
+ kv_heads = 1 if mode == "full" else heads # GQA on the real-shape run
133
+ torch.manual_seed(29)
134
+ total = prefix + action
135
+ q1 = torch.randn(bsz, heads, total, dim, device=device, dtype=dtype, requires_grad=True)
136
+ k1 = torch.randn(bsz, kv_heads, total, dim, device=device, dtype=dtype, requires_grad=True)
137
+ v1 = torch.randn_like(k1, requires_grad=True)
138
+ q2 = q1.detach().clone().requires_grad_(True)
139
+ k2 = k1.detach().clone().requires_grad_(True)
140
+ v2 = v1.detach().clone().requires_grad_(True)
141
+ prefix_att = torch.zeros(bsz, prefix, device=device, dtype=torch.bool)
142
+ prefix_att[:, prefix // 2 :] = True
143
+ kwargs = dict(prefix_len=prefix, action_block_size=2, prefix_att=prefix_att)
144
+
145
+ ref = flex_ops.flex_attention(q1, k1, v1, **kwargs)
146
+ got = flex_ops.manual_attention(q2, k2, v2, compile_part=device != "cpu", **kwargs)
147
+ # bf16-logits class: the manual path stores logits in the io dtype
148
+ # between the GEMM and the fp32 softmax.
149
+ tol = 2e-2 if dtype == torch.bfloat16 else 1e-5
150
+ torch.testing.assert_close(got, ref, atol=tol, rtol=tol)
151
+
152
+ ref.float().square().mean().backward()
153
+ got.float().square().mean().backward()
154
+ for a, b in ((q1, q2), (k1, k2), (v1, v2)):
155
+ denom = torch.linalg.vector_norm(a.grad.float()).clamp_min(1e-12)
156
+ rel = torch.linalg.vector_norm((a.grad - b.grad).float()) / denom
157
+ assert float(rel) <= 2e-2, f"grad rel diff {float(rel)}"
158
+
159
+ # impl dispatch reaches the same path
160
+ via_impl = flex_ops.flex_attention(
161
+ q2.detach(), k2.detach(), v2.detach(), impl="manual", **kwargs
162
+ )
163
+ torch.testing.assert_close(via_impl, got.detach(), atol=tol, rtol=tol)
164
+
165
+
166
+ def run(flex_ops, mode: str) -> None:
167
+ test_matches_explicit_sdpa(flex_ops, mode)
168
+ test_detached_prefix_semantics(flex_ops, mode)
169
+ test_dense_attention_mask_path(flex_ops, mode)
170
+ test_manual_matches_reference(flex_ops, mode)
171
+ x = torch.ones(1)
172
+ torch.testing.assert_close(flex_ops.backend_marker(x), x)
173
+ print(f"flashrt-flex-attention-train {mode}: passed")
174
+
175
+
176
+ if __name__ == "__main__":
177
+ parser = argparse.ArgumentParser()
178
+ parser.add_argument("--backend", choices=["source", "installed"], default="source")
179
+ parser.add_argument("--artifact")
180
+ parser.add_argument("--mode", choices=["smoke", "full"], default="smoke")
181
+ args = parser.parse_args()
182
+ run(load_ops(args.backend, args.artifact), args.mode)
torch-ext/README.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # torch-ext
2
+
3
+ PyTorch bindings and Python package for `flashrt-flex-attention-train`.
4
+
5
+ `torch_binding.cpp` registers a package marker so HF `kernel-builder` can load
6
+ the extension. The Python module provides the SDPA fallback and the stable
7
+ training API used by PI052 integration.
torch-ext/flashrt_flex_attention_train/__init__.py ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FlashRT Flex-style block-sparse attention training API.
2
+
3
+ The public function implements the PI052 prefix/action mask pattern:
4
+
5
+ * prefix query rows use the original K/V tensors, so prefix losses keep normal
6
+ gradients into prefix K/V;
7
+ * action query rows read detached prefix K/V plus normal action K/V by default,
8
+ matching the current training semantics.
9
+
10
+ Unsupported shapes route to the SDPA reference path. Native CUDA kernels are
11
+ not exposed until a shape-specialized implementation beats SDPA on the target
12
+ A100/5090 validation matrix.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+ import torch
20
+ import torch.nn.functional as F
21
+
22
+ try:
23
+ from ._ops import ops
24
+
25
+ _HAS_OPS = hasattr(ops, "_flashrt_training_package_marker")
26
+ except Exception: # source-tree tests before kernel-builder creates _ops.py
27
+ ops = None
28
+ _HAS_OPS = False
29
+
30
+
31
+ MASK_VALUE_F32 = -2.3819763e38
32
+
33
+
34
+ def _use_ops(namespace_ops) -> None:
35
+ """Install a manually built extension (dev/testing path)."""
36
+ global ops, _HAS_OPS
37
+ ops = namespace_ops
38
+ _HAS_OPS = hasattr(ops, "_flashrt_training_package_marker")
39
+
40
+
41
+ def _check_qkv(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> None:
42
+ if q.dim() != 4 or k.dim() != 4 or v.dim() != 4:
43
+ raise ValueError("q, k, and v must be shaped (B, H, S, D)")
44
+ if q.shape[0] != k.shape[0] or q.shape[0] != v.shape[0]:
45
+ raise ValueError("q, k, and v batch dimensions must match")
46
+ if k.shape != v.shape:
47
+ raise ValueError("k and v shapes must match")
48
+ if q.shape[2] != k.shape[2] or q.shape[3] != k.shape[3]:
49
+ raise ValueError("q, k, and v sequence/head_dim dimensions must match")
50
+ if q.device != k.device or q.device != v.device:
51
+ raise ValueError("q, k, and v must be on the same device")
52
+
53
+
54
+ def _as_valid(mask: Optional[torch.Tensor], batch: int, length: int, device: torch.device) -> torch.Tensor:
55
+ if mask is None:
56
+ return torch.ones((batch, length), dtype=torch.bool, device=device)
57
+ if mask.shape != (batch, length):
58
+ raise ValueError(f"mask must be shaped {(batch, length)}, got {tuple(mask.shape)}")
59
+ return mask.to(device=device, dtype=torch.bool)
60
+
61
+
62
+ def build_block_sparse_bool_masks(
63
+ prefix_valid: Optional[torch.Tensor],
64
+ prefix_att: Optional[torch.Tensor],
65
+ *,
66
+ batch: int,
67
+ prefix_len: int,
68
+ action_len: int,
69
+ action_block_size: int,
70
+ non_fast_prefix_len: Optional[int] = None,
71
+ action_valid: Optional[torch.Tensor] = None,
72
+ device: Optional[torch.device] = None,
73
+ ) -> tuple[torch.Tensor, torch.Tensor]:
74
+ """Build boolean masks for the split FlexAttention SDPA calls.
75
+
76
+ Returns ``(prefix_rows, action_rows)`` with shapes ``(B, P, S)`` and
77
+ ``(B, A, S)``. Boolean True means the key/value position is visible.
78
+
79
+ ``prefix_att`` follows Lerobot's cumulative-block convention: prefix key
80
+ ``j`` is visible to prefix query ``i`` when ``cumsum(prefix_att)[j] <=
81
+ cumsum(prefix_att)[i]`` and both rows are valid. When omitted, prefix rows
82
+ attend to all valid prefix tokens.
83
+ """
84
+ if action_block_size <= 0:
85
+ raise ValueError("action_block_size must be positive")
86
+ if prefix_len < 0 or action_len < 0:
87
+ raise ValueError("prefix_len and action_len must be non-negative")
88
+ total_len = prefix_len + action_len
89
+ dev = device
90
+ if dev is None:
91
+ for t in (prefix_valid, prefix_att, action_valid):
92
+ if t is not None:
93
+ dev = t.device
94
+ break
95
+ if dev is None:
96
+ dev = torch.device("cpu")
97
+
98
+ p_valid = _as_valid(prefix_valid, batch, prefix_len, dev)
99
+ a_valid = _as_valid(action_valid, batch, action_len, dev)
100
+
101
+ if prefix_att is None:
102
+ prefix_rows = p_valid[:, :, None] & p_valid[:, None, :]
103
+ else:
104
+ if prefix_att.shape != (batch, prefix_len):
105
+ raise ValueError(
106
+ f"prefix_att must be shaped {(batch, prefix_len)}, got {tuple(prefix_att.shape)}"
107
+ )
108
+ cum = torch.cumsum(prefix_att.to(device=dev, dtype=torch.long), dim=1)
109
+ prefix_rows = (cum[:, None, :] <= cum[:, :, None]) & p_valid[:, :, None] & p_valid[:, None, :]
110
+
111
+ prefix_pad = torch.zeros((batch, prefix_len, action_len), dtype=torch.bool, device=dev)
112
+ prefix_rows = torch.cat([prefix_rows, prefix_pad], dim=2)
113
+
114
+ nf = prefix_len if non_fast_prefix_len is None else int(non_fast_prefix_len)
115
+ nf = max(0, min(nf, prefix_len))
116
+ action_to_prefix = torch.zeros((batch, action_len, prefix_len), dtype=torch.bool, device=dev)
117
+ if nf > 0:
118
+ action_to_prefix[:, :, :nf] = p_valid[:, None, :nf]
119
+ action_to_prefix &= a_valid[:, :, None]
120
+
121
+ q_block = torch.arange(action_len, device=dev) // int(action_block_size)
122
+ kv_block = q_block
123
+ action_block = q_block[:, None] == kv_block[None, :]
124
+ action_block = action_block[None, :, :].expand(batch, -1, -1)
125
+ action_block = action_block & a_valid[:, :, None] & a_valid[:, None, :]
126
+ action_rows = torch.cat([action_to_prefix, action_block], dim=2)
127
+
128
+ if prefix_rows.shape != (batch, prefix_len, total_len):
129
+ raise AssertionError("internal prefix mask shape error")
130
+ if action_rows.shape != (batch, action_len, total_len):
131
+ raise AssertionError("internal action mask shape error")
132
+ return prefix_rows, action_rows
133
+
134
+
135
+ def _bool_to_sdpa_mask(mask: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
136
+ value = MASK_VALUE_F32
137
+ if q.dtype.is_floating_point:
138
+ finfo = torch.finfo(q.dtype)
139
+ value = max(MASK_VALUE_F32, finfo.min)
140
+ return torch.where(
141
+ mask[:, None, :, :],
142
+ torch.zeros((), dtype=q.dtype, device=q.device),
143
+ torch.full((), value, dtype=q.dtype, device=q.device),
144
+ )
145
+
146
+
147
+ def _slice_attention_mask(
148
+ attention_mask: torch.Tensor,
149
+ start: int,
150
+ end: int,
151
+ q: torch.Tensor,
152
+ ) -> torch.Tensor:
153
+ if attention_mask.dim() == 3:
154
+ mask = attention_mask[:, start:end, :]
155
+ if mask.dtype == torch.bool:
156
+ return mask[:, None, :, :]
157
+ return mask[:, None, :, :].to(dtype=q.dtype)
158
+ if attention_mask.dim() == 4:
159
+ mask = attention_mask[:, :, start:end, :]
160
+ return mask if mask.dtype == torch.bool else mask.to(dtype=q.dtype)
161
+ raise ValueError("attention_mask must be (B, S, S) or (B, 1|H, S, S)")
162
+
163
+
164
+ def _sdpa(
165
+ q: torch.Tensor,
166
+ k: torch.Tensor,
167
+ v: torch.Tensor,
168
+ mask: Optional[torch.Tensor],
169
+ *,
170
+ scale: Optional[float],
171
+ dropout_p: float,
172
+ enable_gqa: bool,
173
+ ) -> torch.Tensor:
174
+ kwargs = {"attn_mask": mask, "dropout_p": float(dropout_p), "scale": scale}
175
+ if enable_gqa:
176
+ kwargs["enable_gqa"] = True
177
+ try:
178
+ return F.scaled_dot_product_attention(q, k, v, **kwargs)
179
+ except TypeError:
180
+ kwargs.pop("enable_gqa", None)
181
+ return F.scaled_dot_product_attention(q, k, v, **kwargs)
182
+
183
+
184
+ def reference_flex_attention(
185
+ q: torch.Tensor,
186
+ k: torch.Tensor,
187
+ v: torch.Tensor,
188
+ *,
189
+ prefix_len: int,
190
+ action_block_size: int,
191
+ attention_mask: Optional[torch.Tensor] = None,
192
+ prefix_valid: Optional[torch.Tensor] = None,
193
+ prefix_att: Optional[torch.Tensor] = None,
194
+ non_fast_prefix_len: Optional[int] = None,
195
+ action_valid: Optional[torch.Tensor] = None,
196
+ detach_prefix_kv_for_action: bool = True,
197
+ scale: Optional[float] = None,
198
+ dropout_p: float = 0.0,
199
+ enable_gqa: Optional[bool] = None,
200
+ ) -> torch.Tensor:
201
+ """SDPA reference for the PI052 FlexAttention replacement shape.
202
+
203
+ Args:
204
+ q, k, v: ``(B, Hq/Hkv, S, D)`` tensors.
205
+ prefix_len: number of prefix rows/columns at the start of sequence.
206
+ action_block_size: size of each block-diagonal action segment.
207
+ attention_mask: optional prebuilt additive or boolean full mask.
208
+ prefix_valid: optional ``(B, P)`` valid prefix positions.
209
+ prefix_att: optional ``(B, P)`` cumulative-block markers.
210
+ non_fast_prefix_len: prefix columns visible to action rows.
211
+ action_valid: optional ``(B, A)`` valid action positions.
212
+ detach_prefix_kv_for_action: detach prefix K/V on the action-row path.
213
+ scale: SDPA scale. Defaults to ``D ** -0.5``.
214
+ dropout_p: SDPA dropout probability.
215
+ enable_gqa: pass SDPA GQA mode when q heads and kv heads differ.
216
+ """
217
+ _check_qkv(q, k, v)
218
+ batch, _, total_len, head_dim = q.shape
219
+ if not (0 <= int(prefix_len) <= total_len):
220
+ raise ValueError("prefix_len must be in [0, S]")
221
+ prefix_len = int(prefix_len)
222
+ action_len = total_len - prefix_len
223
+ if scale is None:
224
+ scale = head_dim**-0.5
225
+ if enable_gqa is None:
226
+ enable_gqa = q.shape[1] != k.shape[1]
227
+
228
+ q_prefix = q[:, :, :prefix_len, :]
229
+ q_action = q[:, :, prefix_len:, :]
230
+ k_prefix = k[:, :, :prefix_len, :]
231
+ k_action = k[:, :, prefix_len:, :]
232
+ v_prefix = v[:, :, :prefix_len, :]
233
+ v_action = v[:, :, prefix_len:, :]
234
+
235
+ if attention_mask is None:
236
+ prefix_bool, action_bool = build_block_sparse_bool_masks(
237
+ prefix_valid,
238
+ prefix_att,
239
+ batch=batch,
240
+ prefix_len=prefix_len,
241
+ action_len=action_len,
242
+ action_block_size=action_block_size,
243
+ non_fast_prefix_len=non_fast_prefix_len,
244
+ action_valid=action_valid,
245
+ device=q.device,
246
+ )
247
+ prefix_mask = _bool_to_sdpa_mask(prefix_bool, q)
248
+ action_mask = _bool_to_sdpa_mask(action_bool, q)
249
+ else:
250
+ prefix_mask = _slice_attention_mask(attention_mask, 0, prefix_len, q)
251
+ action_mask = _slice_attention_mask(attention_mask, prefix_len, total_len, q)
252
+
253
+ out_parts = []
254
+ if prefix_len:
255
+ out_parts.append(
256
+ _sdpa(
257
+ q_prefix,
258
+ k,
259
+ v,
260
+ prefix_mask,
261
+ scale=scale,
262
+ dropout_p=dropout_p,
263
+ enable_gqa=bool(enable_gqa),
264
+ )
265
+ )
266
+ if action_len:
267
+ prefix_k = k_prefix.detach() if detach_prefix_kv_for_action else k_prefix
268
+ prefix_v = v_prefix.detach() if detach_prefix_kv_for_action else v_prefix
269
+ k_for_action = torch.cat([prefix_k, k_action], dim=2)
270
+ v_for_action = torch.cat([prefix_v, v_action], dim=2)
271
+ out_parts.append(
272
+ _sdpa(
273
+ q_action,
274
+ k_for_action,
275
+ v_for_action,
276
+ action_mask,
277
+ scale=scale,
278
+ dropout_p=dropout_p,
279
+ enable_gqa=bool(enable_gqa),
280
+ )
281
+ )
282
+ if not out_parts:
283
+ return q.new_empty(q.shape)
284
+ return torch.cat(out_parts, dim=2) if len(out_parts) == 2 else out_parts[0]
285
+
286
+
287
+ def _manual_attention_part(qs, ks, vs, mask, scale):
288
+ """Materialized-logits attention part: cuBLAS GEMMs + fused masked softmax.
289
+
290
+ Same math as SDPA with an additive mask (fp32 softmax; logits stored in
291
+ the io dtype between the GEMM and the softmax). Grouped queries run as a
292
+ strided batched GEMM over the KV heads, so a 1-head K/V is never
293
+ repeated. At PI052 training shapes (GQA 8:1, D=256, bf16) this beats
294
+ both SDPA-with-dense-mask (2.3-3.1x) and the best FlexAttention
295
+ configuration (1.4-2.9x) on fwd+bwd — see benchmarks/RESULTS.md.
296
+ """
297
+ B, H, Sq, D = qs.shape
298
+ Hk = ks.shape[1]
299
+ if Hk != H:
300
+ g = H // Hk
301
+ q2 = qs.reshape(B, Hk, g * Sq, D)
302
+ logits = (q2 @ ks.transpose(-1, -2)).reshape(B, H, Sq, -1)
303
+ else:
304
+ logits = qs @ ks.transpose(-1, -2)
305
+ logits = logits * scale
306
+ if mask is not None:
307
+ logits = logits + mask
308
+ p = logits.float().softmax(dim=-1).to(qs.dtype)
309
+ if Hk != H:
310
+ out = (p.reshape(B, Hk, g * Sq, -1) @ vs).reshape(B, H, Sq, D)
311
+ else:
312
+ out = p @ vs
313
+ return out
314
+
315
+
316
+ # Public alias: integrations (e.g. the LeRobot pi052 flag) consume the raw
317
+ # per-part op and assemble masks/splits themselves.
318
+ manual_attention_part = _manual_attention_part
319
+
320
+ _manual_part_compiled = None
321
+
322
+
323
+ def _get_manual_part():
324
+ global _manual_part_compiled
325
+ if _manual_part_compiled is None:
326
+ _manual_part_compiled = torch.compile(_manual_attention_part, dynamic=False)
327
+ return _manual_part_compiled
328
+
329
+
330
+ def manual_attention(
331
+ q: torch.Tensor,
332
+ k: torch.Tensor,
333
+ v: torch.Tensor,
334
+ *,
335
+ prefix_len: int,
336
+ action_block_size: int,
337
+ attention_mask: Optional[torch.Tensor] = None,
338
+ prefix_valid: Optional[torch.Tensor] = None,
339
+ prefix_att: Optional[torch.Tensor] = None,
340
+ non_fast_prefix_len: Optional[int] = None,
341
+ action_valid: Optional[torch.Tensor] = None,
342
+ detach_prefix_kv_for_action: bool = True,
343
+ scale: Optional[float] = None,
344
+ dropout_p: float = 0.0,
345
+ compile_part: bool = True,
346
+ ) -> torch.Tensor:
347
+ """Materialized-logits implementation of :func:`reference_flex_attention`.
348
+
349
+ Same mask semantics and prefix/action split; each part runs through
350
+ :func:`_manual_attention_part` instead of SDPA. ``dropout_p`` must be 0
351
+ (training attention dropout is unused in PI052); other values raise so
352
+ callers fall back explicitly.
353
+ """
354
+ if dropout_p:
355
+ raise ValueError("manual_attention does not support dropout; use the reference path")
356
+ _check_qkv(q, k, v)
357
+ batch, _, total_len, head_dim = q.shape
358
+ if not (0 <= int(prefix_len) <= total_len):
359
+ raise ValueError("prefix_len must be in [0, S]")
360
+ prefix_len = int(prefix_len)
361
+ action_len = total_len - prefix_len
362
+ if scale is None:
363
+ scale = head_dim**-0.5
364
+
365
+ if attention_mask is None:
366
+ prefix_bool, action_bool = build_block_sparse_bool_masks(
367
+ prefix_valid,
368
+ prefix_att,
369
+ batch=batch,
370
+ prefix_len=prefix_len,
371
+ action_len=action_len,
372
+ action_block_size=action_block_size,
373
+ non_fast_prefix_len=non_fast_prefix_len,
374
+ action_valid=action_valid,
375
+ device=q.device,
376
+ )
377
+ prefix_mask = _bool_to_sdpa_mask(prefix_bool, q)
378
+ action_mask = _bool_to_sdpa_mask(action_bool, q)
379
+ else:
380
+ prefix_mask = _slice_attention_mask(attention_mask, 0, prefix_len, q)
381
+ action_mask = _slice_attention_mask(attention_mask, prefix_len, total_len, q)
382
+ if prefix_mask.dtype == torch.bool:
383
+ prefix_mask = _bool_to_sdpa_mask(prefix_mask[:, 0], q)
384
+ if action_mask.dtype == torch.bool:
385
+ action_mask = _bool_to_sdpa_mask(action_mask[:, 0], q)
386
+
387
+ part = _get_manual_part() if compile_part else _manual_attention_part
388
+ out_parts = []
389
+ if prefix_len:
390
+ out_parts.append(part(q[:, :, :prefix_len, :], k, v, prefix_mask, scale))
391
+ if action_len:
392
+ k_prefix = k[:, :, :prefix_len, :]
393
+ v_prefix = v[:, :, :prefix_len, :]
394
+ if detach_prefix_kv_for_action:
395
+ k_prefix = k_prefix.detach()
396
+ v_prefix = v_prefix.detach()
397
+ k_for_action = torch.cat([k_prefix, k[:, :, prefix_len:, :]], dim=2)
398
+ v_for_action = torch.cat([v_prefix, v[:, :, prefix_len:, :]], dim=2)
399
+ out_parts.append(part(q[:, :, prefix_len:, :], k_for_action, v_for_action, action_mask, scale))
400
+ if not out_parts:
401
+ return q.new_empty(q.shape)
402
+ return torch.cat(out_parts, dim=2) if len(out_parts) == 2 else out_parts[0]
403
+
404
+
405
+ def flex_attention(
406
+ q: torch.Tensor,
407
+ k: torch.Tensor,
408
+ v: torch.Tensor,
409
+ *,
410
+ prefix_len: int,
411
+ action_block_size: int,
412
+ attention_mask: Optional[torch.Tensor] = None,
413
+ prefix_valid: Optional[torch.Tensor] = None,
414
+ prefix_att: Optional[torch.Tensor] = None,
415
+ non_fast_prefix_len: Optional[int] = None,
416
+ action_valid: Optional[torch.Tensor] = None,
417
+ detach_prefix_kv_for_action: bool = True,
418
+ scale: Optional[float] = None,
419
+ dropout_p: float = 0.0,
420
+ enable_gqa: Optional[bool] = None,
421
+ force_fallback: bool = False,
422
+ impl: str = "sdpa",
423
+ ) -> torch.Tensor:
424
+ """Flex-style block-sparse attention.
425
+
426
+ ``impl="sdpa"`` (default) keeps the SDPA reference path;
427
+ ``impl="manual"`` routes through the materialized-logits
428
+ implementation; ``impl="auto"`` picks manual only where it has been
429
+ measured to win end-to-end — consumer Blackwell (sm120-class) with
430
+ no dropout. On A100 (sm80) the manual math wins microbenches but
431
+ loses training-step integration, and on H100/H200 (sm90) the fused
432
+ FMHA kernels win outright, so auto keeps SDPA there.
433
+ """
434
+ _ = force_fallback
435
+ if impl == "auto":
436
+ sm120 = q.is_cuda and torch.cuda.get_device_capability(q.device)[0] == 12
437
+ impl = "manual" if (sm120 and not dropout_p) else "sdpa"
438
+ if impl == "manual":
439
+ return manual_attention(
440
+ q,
441
+ k,
442
+ v,
443
+ prefix_len=prefix_len,
444
+ action_block_size=action_block_size,
445
+ attention_mask=attention_mask,
446
+ prefix_valid=prefix_valid,
447
+ prefix_att=prefix_att,
448
+ non_fast_prefix_len=non_fast_prefix_len,
449
+ action_valid=action_valid,
450
+ detach_prefix_kv_for_action=detach_prefix_kv_for_action,
451
+ scale=scale,
452
+ dropout_p=dropout_p,
453
+ )
454
+ return reference_flex_attention(
455
+ q,
456
+ k,
457
+ v,
458
+ prefix_len=prefix_len,
459
+ action_block_size=action_block_size,
460
+ attention_mask=attention_mask,
461
+ prefix_valid=prefix_valid,
462
+ prefix_att=prefix_att,
463
+ non_fast_prefix_len=non_fast_prefix_len,
464
+ action_valid=action_valid,
465
+ detach_prefix_kv_for_action=detach_prefix_kv_for_action,
466
+ scale=scale,
467
+ dropout_p=dropout_p,
468
+ enable_gqa=enable_gqa,
469
+ )
470
+
471
+
472
+ def flex_attention_forward(*args, **kwargs) -> torch.Tensor:
473
+ """Forward-only compatibility wrapper."""
474
+ return flex_attention(*args, **kwargs)
475
+
476
+
477
+ def backend_marker(x: torch.Tensor) -> torch.Tensor:
478
+ if ops is None:
479
+ return x
480
+ return ops._flashrt_training_package_marker(x)
481
+
482
+
483
+ __all__ = [
484
+ "MASK_VALUE_F32",
485
+ "backend_marker",
486
+ "build_block_sparse_bool_masks",
487
+ "flex_attention",
488
+ "flex_attention_forward",
489
+ "manual_attention",
490
+ "manual_attention_part",
491
+ "reference_flex_attention",
492
+ ]
torch-ext/torch_binding.cpp ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ #include <torch/all.h>
4
+ #include <torch/library.h>
5
+
6
+ #include "registration.h"
7
+ #include "torch_binding.h"
8
+
9
+ torch::Tensor flashrt_training_package_marker(torch::Tensor x) { return x; }
10
+
11
+ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
12
+ ops.def("_flashrt_training_package_marker(Tensor x) -> Tensor");
13
+ ops.impl("_flashrt_training_package_marker",
14
+ c10::DispatchKey::CompositeExplicitAutograd,
15
+ &flashrt_training_package_marker);
16
+ }
17
+
18
+ REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
torch-ext/torch_binding.h ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ #pragma once
4
+
5
+ #include <torch/all.h>
6
+
7
+ torch::Tensor flashrt_training_package_marker(torch::Tensor x);