Add package source and torch-universal build variant
Browse files- CARD.md +21 -0
- README.md +29 -6
- VALIDATION.md +48 -0
- benchmarks/README.md +22 -0
- benchmarks/RESULTS.md +91 -0
- benchmarks/benchmark.py +779 -0
- benchmarks/shape_matrix.py +169 -0
- build.toml +28 -0
- build/torch-universal/flashrt_flex_attention_train/__init__.py +492 -0
- csrc/README.md +7 -0
- csrc/stub.cu +3 -0
- csrc/stub.cuh +3 -0
- flake.nix +17 -0
- tests/test_flashrt_flex_attention_train.py +182 -0
- torch-ext/README.md +7 -0
- torch-ext/flashrt_flex_attention_train/__init__.py +492 -0
- torch-ext/torch_binding.cpp +18 -0
- torch-ext/torch_binding.h +7 -0
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
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
|
| 6 |
-
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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);
|