Instructions to use Frosty40/hydra with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Kernels
How to use Frosty40/hydra with Kernels:
# !pip install kernels from kernels import get_kernel kernel = get_kernel("Frosty40/hydra") - Notebooks
- Google Colab
- Kaggle
Publish Hydra kernel source packet
Browse files- CARD.md +67 -0
- README.md +158 -0
- benchmarks/benchmark_hydra_decode.py +81 -0
- build.toml +7 -0
- flake.lock +95 -0
- flake.nix +17 -0
- readme_example.py +47 -0
- tests/conftest.py +9 -0
- tests/test_csr.py +43 -0
- tests/test_decode_parity.py +43 -0
- tests/test_imports.py +22 -0
- torch-ext/hydra/__init__.py +32 -0
- torch-ext/hydra/api.py +160 -0
- torch-ext/hydra/csr.py +131 -0
- torch-ext/hydra/function.py +75 -0
- torch-ext/hydra/kernel_bwd.py +743 -0
- torch-ext/hydra/kernel_decode.py +298 -0
- torch-ext/hydra/kernel_delta.py +188 -0
- torch-ext/hydra/kernel_fwd.py +379 -0
- torch-ext/hydra/policy.py +438 -0
- torch-ext/hydra/sw_sinks_csr.py +93 -0
CARD.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hydra Kernel Card
|
| 2 |
+
|
| 3 |
+
## Summary
|
| 4 |
+
|
| 5 |
+
Hydra provides a bounded-residency decode attention path for long-context
|
| 6 |
+
inference. The implementation is Python plus Triton and is packaged here as a
|
| 7 |
+
Hugging Face universal CUDA kernel source directory.
|
| 8 |
+
|
| 9 |
+
Source code: [https://github.com/newjordan/hydra/tree/main/hf-kernels/hydra](https://github.com/newjordan/hydra/tree/main/hf-kernels/hydra)
|
| 10 |
+
|
| 11 |
+
## Intended Use
|
| 12 |
+
|
| 13 |
+
Use Hydra for experiments where full decode attention over a long KV cache is
|
| 14 |
+
memory-bound and a bounded resident set is acceptable for evaluation.
|
| 15 |
+
|
| 16 |
+
Hydra is not intended as a drop-in universal FlashAttention replacement. Users
|
| 17 |
+
should keep an exact-model fallback path and validate quality for their prompt
|
| 18 |
+
set.
|
| 19 |
+
|
| 20 |
+
## Kernel Interface
|
| 21 |
+
|
| 22 |
+
The exported call is:
|
| 23 |
+
|
| 24 |
+
```python
|
| 25 |
+
hydra.hydra(q, k, v, is_causal=True, sliding_window=None)
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
Inputs are bf16 tensors shaped `(B, H, T, D)` with `D=128`. The decode path
|
| 29 |
+
supports `Tq == 1`; the prefill path requires sequence length to be a multiple
|
| 30 |
+
of the compile-time block size.
|
| 31 |
+
|
| 32 |
+
## Evidence
|
| 33 |
+
|
| 34 |
+
This card separates the kernel contribution from benchmark appendices:
|
| 35 |
+
|
| 36 |
+
- kernel/package validation: import, CSR, CUDA decode parity, builder, example,
|
| 37 |
+
and isolated decode benchmark gates
|
| 38 |
+
- broad Hydra campaign context: multi-GPU bounded-residency testing, comparison
|
| 39 |
+
lanes, capacity/OOM boundaries, and diagnostics in the staging repo
|
| 40 |
+
- exact-Qwen proof-of-concept: summary-backed demo rows for:
|
| 41 |
+
|
| 42 |
+
- RTX PRO 6000 WS with `Qwen/Qwen3.6-35B-A3B-FP8`
|
| 43 |
+
- RTX 3090 with `Qwen/Qwen3.6-35B-A3B-FP8`
|
| 44 |
+
|
| 45 |
+
Use `results/reports/QWEN3P6_FP8_EVIDENCE_TABLE.md` in the staging repo as the
|
| 46 |
+
claim ledger for the exact-Qwen proof-of-concept only. The table is generated
|
| 47 |
+
from raw summary JSON, answer artifacts, and logs. It intentionally excludes
|
| 48 |
+
incomplete scopes from completed benchmark rows.
|
| 49 |
+
|
| 50 |
+
## Non-Claims
|
| 51 |
+
|
| 52 |
+
- no universal speedup claim
|
| 53 |
+
- no production-readiness claim
|
| 54 |
+
- no broad quality-preservation claim without scorer or inspection evidence
|
| 55 |
+
- no proxy/profile/loader-only benchmark claims
|
| 56 |
+
- no results from non-Qwen or non-FP8 runs in the exact-Qwen proof-of-concept table
|
| 57 |
+
- no framing that treats the exact-Qwen proof-of-concept as the full Hydra campaign
|
| 58 |
+
|
| 59 |
+
## Required Validation
|
| 60 |
+
|
| 61 |
+
For source changes, run:
|
| 62 |
+
|
| 63 |
+
- import and CSR tests
|
| 64 |
+
- CUDA decode parity against PyTorch SDPA on small tensors
|
| 65 |
+
- kernel-builder `ci-test`
|
| 66 |
+
- one isolated decode benchmark
|
| 67 |
+
- one exact-model reproduction on a named GPU/config
|
README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
tags:
|
| 4 |
+
- kernel
|
| 5 |
+
- kernels
|
| 6 |
+
- triton
|
| 7 |
+
- attention
|
| 8 |
+
- long-context
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Hydra
|
| 12 |
+
|
| 13 |
+
Hydra is an experimental bounded-residency attention kernel for long-context
|
| 14 |
+
decode. It keeps sink tokens, recent tokens, and selected older pages resident
|
| 15 |
+
instead of forcing each decode step to attend over the full KV cache.
|
| 16 |
+
|
| 17 |
+
Source code: [https://github.com/newjordan/hydra/tree/main/hf-kernels/hydra](https://github.com/newjordan/hydra/tree/main/hf-kernels/hydra)
|
| 18 |
+
|
| 19 |
+
This release is intentionally narrow. It is not a general replacement for
|
| 20 |
+
full attention, and it does not claim universal speedups or broad quality
|
| 21 |
+
preservation. The current target is fit and usability for specific
|
| 22 |
+
long-context inference workloads where the full-attention path is memory-bound.
|
| 23 |
+
|
| 24 |
+
## Usage
|
| 25 |
+
|
| 26 |
+
After the kernel is published:
|
| 27 |
+
|
| 28 |
+
```python
|
| 29 |
+
import torch
|
| 30 |
+
from kernels import get_kernel
|
| 31 |
+
|
| 32 |
+
hydra = get_kernel("Frosty40/hydra")
|
| 33 |
+
|
| 34 |
+
q = torch.randn(1, 32, 1, 128, device="cuda", dtype=torch.bfloat16)
|
| 35 |
+
k = torch.randn(1, 8, 8192, 128, device="cuda", dtype=torch.bfloat16)
|
| 36 |
+
v = torch.randn(1, 8, 8192, 128, device="cuda", dtype=torch.bfloat16)
|
| 37 |
+
|
| 38 |
+
out = hydra.hydra(q, k, v)
|
| 39 |
+
print(out.shape)
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
For local development from the public source checkout:
|
| 43 |
+
|
| 44 |
+
```python
|
| 45 |
+
from pathlib import Path
|
| 46 |
+
import sys
|
| 47 |
+
|
| 48 |
+
sys.path.insert(0, str(Path("hf-kernels") / "hydra" / "torch-ext"))
|
| 49 |
+
import hydra
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
`readme_example.py` uses the local source packet by default so it can run before
|
| 53 |
+
publication. Set `HYDRA_USE_HUB=1` after publication to exercise the Hub-loaded
|
| 54 |
+
path.
|
| 55 |
+
|
| 56 |
+
## API
|
| 57 |
+
|
| 58 |
+
```python
|
| 59 |
+
hydra.hydra(
|
| 60 |
+
q,
|
| 61 |
+
k,
|
| 62 |
+
v,
|
| 63 |
+
*,
|
| 64 |
+
is_causal=True,
|
| 65 |
+
sliding_window=None,
|
| 66 |
+
policy_layer_idx=None,
|
| 67 |
+
precision="high",
|
| 68 |
+
)
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
Current constraints:
|
| 72 |
+
|
| 73 |
+
- CUDA tensors only
|
| 74 |
+
- bf16 `q`, `k`, and `v`
|
| 75 |
+
- shape `(B, H, T, D)` with `D=128`
|
| 76 |
+
- causal attention only
|
| 77 |
+
- decode path supports `Tq == 1` with arbitrary `Tkv`
|
| 78 |
+
- prefill path requires `T % BLOCK_SIZE == 0`
|
| 79 |
+
|
| 80 |
+
## Evidence Boundary
|
| 81 |
+
|
| 82 |
+
Submission-facing evidence must come from checked artifacts, not prose notes.
|
| 83 |
+
Treat evidence in three separate scopes:
|
| 84 |
+
|
| 85 |
+
- kernel/package validation: tests, CUDA parity logs, `kernel-builder` logs, and
|
| 86 |
+
isolated decode benchmarks for this source packet
|
| 87 |
+
- broad Hydra research campaign: capacity, quality, sparse-attention comparison,
|
| 88 |
+
edge/OOM, diagnostic, and model-family reports from the staging repo
|
| 89 |
+
- exact-model proof-of-concept: checked `Qwen/Qwen3.6-35B-A3B-FP8` rows for
|
| 90 |
+
named GPUs only
|
| 91 |
+
|
| 92 |
+
The exact-Qwen proof-of-concept appendix in the staging repo is under:
|
| 93 |
+
|
| 94 |
+
```text
|
| 95 |
+
results/raw/qwen3p6_35b_a3b_fp8/
|
| 96 |
+
results/reports/QWEN3P6_FP8_EVIDENCE_TABLE.md
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
Each cited row must include all three:
|
| 100 |
+
|
| 101 |
+
- fit/headroom: GPU, context length, memory allocated/reserved, and OOM state
|
| 102 |
+
- quality/correctness: prompt/task ID and generated answer artifact
|
| 103 |
+
- speed/usability: wall time, generated tokens, tokens/sec, and comparison target
|
| 104 |
+
|
| 105 |
+
Do not cite proxy models, loader-only probes, failed dependency checks, or
|
| 106 |
+
non-matching model runs as Hydra benchmark results. Do not describe the
|
| 107 |
+
exact-Qwen proof-of-concept subset as the full Hydra validation campaign.
|
| 108 |
+
|
| 109 |
+
## Current Proof-Of-Concept Scope
|
| 110 |
+
|
| 111 |
+
The current exact-Qwen artifact-backed proof-of-concept scope is:
|
| 112 |
+
|
| 113 |
+
| GPU | Model | Scope |
|
| 114 |
+
| --- | --- | --- |
|
| 115 |
+
| RTX PRO 6000 WS | `Qwen/Qwen3.6-35B-A3B-FP8` | 32k/80k/160k repeat packet, 160k c96 warm packet, and frontier/headroom sweeps |
|
| 116 |
+
| RTX 3090 | `Qwen/Qwen3.6-35B-A3B-FP8` | 2k/3k/4k/6k/8k fit probes and completed 10k/12k/14k edge sweep |
|
| 117 |
+
|
| 118 |
+
The 3090 result should be framed as fit/usability evidence, not a speedup
|
| 119 |
+
claim. Token rates are slow in the long-context edge rows. The broader Hydra
|
| 120 |
+
campaign includes additional GPUs, tasks, and comparison lanes outside this
|
| 121 |
+
exact-model appendix.
|
| 122 |
+
|
| 123 |
+
## Validation Required Before Merge
|
| 124 |
+
|
| 125 |
+
Minimum gates for source changes:
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
cd hf-kernels/hydra
|
| 129 |
+
python3 -m pytest -q tests
|
| 130 |
+
nix run .#ci-test
|
| 131 |
+
python3 benchmarks/benchmark_hydra_decode.py --repo .
|
| 132 |
+
python3 readme_example.py
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
Run the CUDA tests on real GPUs. Local syntax checks are not enough for a
|
| 136 |
+
kernel submission.
|
| 137 |
+
|
| 138 |
+
## Benchmark Snapshot
|
| 139 |
+
|
| 140 |
+
The current 8192-token decode smoke/benchmark matrix is intentionally reported
|
| 141 |
+
as kernel/package evidence, not as a universal speedup claim.
|
| 142 |
+
|
| 143 |
+
| GPU | Package smoke decode | HF benchmark mean |
|
| 144 |
+
| --- | ---: | ---: |
|
| 145 |
+
| RTX 3060 | 0.2574 ms | 0.3229 ms |
|
| 146 |
+
| RTX 3070 | 0.1474 ms | 0.2532 ms |
|
| 147 |
+
| RTX 3080 | 0.2051 ms | 0.3157 ms |
|
| 148 |
+
| RTX 3090 | 0.1492 ms | 0.3107 ms |
|
| 149 |
+
| RTX 4070 Ti | 0.1261 ms | 0.2215 ms |
|
| 150 |
+
| RTX 4090 | 0.1132 ms | 0.2245 ms |
|
| 151 |
+
| A100 SXM4 | 0.1408 ms | 0.2568 ms |
|
| 152 |
+
| RTX PRO 6000 Blackwell | 0.1158 ms | 0.1371 ms |
|
| 153 |
+
| RTX A6000 | builder smoke 0.2166 ms | 0.3230 ms |
|
| 154 |
+
|
| 155 |
+
The final `kernel-builder` gate passed on a Vast RTX A6000 with
|
| 156 |
+
`BUILDER_VARIANT=torch210-cxx11-cu128-x86_64-linux`: local pytest `6 passed`,
|
| 157 |
+
decode smoke `0.2166 ms/iter`, builder pytest `4 passed, 2 skipped`, exit
|
| 158 |
+
status `0`.
|
benchmarks/benchmark_hydra_decode.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import importlib
|
| 5 |
+
import sys
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from kernels import get_local_kernel
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_local_kernel(repo: Path):
|
| 14 |
+
for variant in (repo / "build", repo):
|
| 15 |
+
if (variant / "metadata.json").exists():
|
| 16 |
+
return get_local_kernel(variant)
|
| 17 |
+
|
| 18 |
+
sys.path.insert(0, str(repo / "torch-ext"))
|
| 19 |
+
return importlib.import_module("hydra")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def parse_args() -> argparse.Namespace:
|
| 23 |
+
parser = argparse.ArgumentParser(description="Hydra decode microbenchmark")
|
| 24 |
+
parser.add_argument("--repo", default=".", help="Path to the hydra kernel directory")
|
| 25 |
+
parser.add_argument("--batch", type=int, default=1)
|
| 26 |
+
parser.add_argument("--heads", type=int, default=32)
|
| 27 |
+
parser.add_argument("--kv-heads", type=int, default=8)
|
| 28 |
+
parser.add_argument("--tokens", type=int, default=8192)
|
| 29 |
+
parser.add_argument("--head-dim", type=int, default=128)
|
| 30 |
+
parser.add_argument("--iters", type=int, default=100)
|
| 31 |
+
parser.add_argument("--warmup", type=int, default=10)
|
| 32 |
+
parser.add_argument("--window", type=int, default=0)
|
| 33 |
+
return parser.parse_args()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def main() -> None:
|
| 37 |
+
args = parse_args()
|
| 38 |
+
if not torch.cuda.is_available():
|
| 39 |
+
raise SystemExit("CUDA is required")
|
| 40 |
+
|
| 41 |
+
kernel = load_local_kernel(Path(args.repo))
|
| 42 |
+
q = torch.randn(
|
| 43 |
+
args.batch,
|
| 44 |
+
args.heads,
|
| 45 |
+
1,
|
| 46 |
+
args.head_dim,
|
| 47 |
+
device="cuda",
|
| 48 |
+
dtype=torch.bfloat16,
|
| 49 |
+
)
|
| 50 |
+
k = torch.randn(
|
| 51 |
+
args.batch,
|
| 52 |
+
args.kv_heads,
|
| 53 |
+
args.tokens,
|
| 54 |
+
args.head_dim,
|
| 55 |
+
device="cuda",
|
| 56 |
+
dtype=torch.bfloat16,
|
| 57 |
+
)
|
| 58 |
+
v = torch.randn_like(k)
|
| 59 |
+
|
| 60 |
+
window = None if args.window <= 0 else args.window
|
| 61 |
+
for _ in range(args.warmup):
|
| 62 |
+
kernel.hydra(q, k, v, sliding_window=window)
|
| 63 |
+
torch.cuda.synchronize()
|
| 64 |
+
|
| 65 |
+
start = time.perf_counter()
|
| 66 |
+
for _ in range(args.iters):
|
| 67 |
+
kernel.hydra(q, k, v, sliding_window=window)
|
| 68 |
+
torch.cuda.synchronize()
|
| 69 |
+
elapsed = time.perf_counter() - start
|
| 70 |
+
|
| 71 |
+
ms = elapsed * 1000.0 / args.iters
|
| 72 |
+
print(
|
| 73 |
+
"hydra_decode "
|
| 74 |
+
f"B={args.batch} H={args.heads} Hkv={args.kv_heads} "
|
| 75 |
+
f"Tkv={args.tokens} D={args.head_dim} window={window or 0} "
|
| 76 |
+
f"iters={args.iters} ms_per_iter={ms:.4f}"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
main()
|
build.toml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[general]
|
| 2 |
+
name = "hydra"
|
| 3 |
+
license = "MIT"
|
| 4 |
+
backends = ["cuda"]
|
| 5 |
+
|
| 6 |
+
[general.hub]
|
| 7 |
+
repo-id = "Frosty40/hydra"
|
flake.lock
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nodes": {
|
| 3 |
+
"flake-compat": {
|
| 4 |
+
"locked": {
|
| 5 |
+
"lastModified": 1765121682,
|
| 6 |
+
"narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=",
|
| 7 |
+
"owner": "edolstra",
|
| 8 |
+
"repo": "flake-compat",
|
| 9 |
+
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
|
| 10 |
+
"type": "github"
|
| 11 |
+
},
|
| 12 |
+
"original": {
|
| 13 |
+
"owner": "edolstra",
|
| 14 |
+
"repo": "flake-compat",
|
| 15 |
+
"type": "github"
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
"flake-utils": {
|
| 19 |
+
"inputs": {
|
| 20 |
+
"systems": "systems"
|
| 21 |
+
},
|
| 22 |
+
"locked": {
|
| 23 |
+
"lastModified": 1731533236,
|
| 24 |
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
| 25 |
+
"owner": "numtide",
|
| 26 |
+
"repo": "flake-utils",
|
| 27 |
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
| 28 |
+
"type": "github"
|
| 29 |
+
},
|
| 30 |
+
"original": {
|
| 31 |
+
"owner": "numtide",
|
| 32 |
+
"repo": "flake-utils",
|
| 33 |
+
"type": "github"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"kernel-builder": {
|
| 37 |
+
"inputs": {
|
| 38 |
+
"flake-compat": "flake-compat",
|
| 39 |
+
"flake-utils": "flake-utils",
|
| 40 |
+
"nixpkgs": "nixpkgs"
|
| 41 |
+
},
|
| 42 |
+
"locked": {
|
| 43 |
+
"lastModified": 1775482375,
|
| 44 |
+
"narHash": "sha256-RUbxfJGs96jwnwSci3+8h08GB2/katOI67yZTCaV+aE=",
|
| 45 |
+
"owner": "huggingface",
|
| 46 |
+
"repo": "kernel-builder",
|
| 47 |
+
"rev": "dffbce5a048648febb96bbed79f3811ab6d7a577",
|
| 48 |
+
"type": "github"
|
| 49 |
+
},
|
| 50 |
+
"original": {
|
| 51 |
+
"owner": "huggingface",
|
| 52 |
+
"repo": "kernel-builder",
|
| 53 |
+
"type": "github"
|
| 54 |
+
}
|
| 55 |
+
},
|
| 56 |
+
"nixpkgs": {
|
| 57 |
+
"locked": {
|
| 58 |
+
"lastModified": 1766341660,
|
| 59 |
+
"narHash": "sha256-4yG6vx7Dddk9/zh45Y2KM82OaRD4jO3HA9r98ORzysA=",
|
| 60 |
+
"owner": "NixOS",
|
| 61 |
+
"repo": "nixpkgs",
|
| 62 |
+
"rev": "26861f5606e3e4d1400771b513cc63e5f70151a6",
|
| 63 |
+
"type": "github"
|
| 64 |
+
},
|
| 65 |
+
"original": {
|
| 66 |
+
"owner": "NixOS",
|
| 67 |
+
"ref": "nixos-unstable-small",
|
| 68 |
+
"repo": "nixpkgs",
|
| 69 |
+
"type": "github"
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
"root": {
|
| 73 |
+
"inputs": {
|
| 74 |
+
"kernel-builder": "kernel-builder"
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"systems": {
|
| 78 |
+
"locked": {
|
| 79 |
+
"lastModified": 1681028828,
|
| 80 |
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
| 81 |
+
"owner": "nix-systems",
|
| 82 |
+
"repo": "default",
|
| 83 |
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
| 84 |
+
"type": "github"
|
| 85 |
+
},
|
| 86 |
+
"original": {
|
| 87 |
+
"owner": "nix-systems",
|
| 88 |
+
"repo": "default",
|
| 89 |
+
"type": "github"
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
"root": "root",
|
| 94 |
+
"version": 7
|
| 95 |
+
}
|
flake.nix
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
description = "Flake for the Hydra Hugging Face kernel";
|
| 3 |
+
|
| 4 |
+
inputs = {
|
| 5 |
+
kernel-builder.url = "github:huggingface/kernel-builder";
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
outputs =
|
| 9 |
+
{
|
| 10 |
+
self,
|
| 11 |
+
kernel-builder,
|
| 12 |
+
}:
|
| 13 |
+
kernel-builder.lib.genFlakeOutputs {
|
| 14 |
+
path = ./.;
|
| 15 |
+
rev = self.shortRev or self.dirtyShortRev or (self.lastModifiedDate or "unknown");
|
| 16 |
+
};
|
| 17 |
+
}
|
readme_example.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch",
|
| 5 |
+
# "triton",
|
| 6 |
+
# "kernels",
|
| 7 |
+
# ]
|
| 8 |
+
# ///
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
import sys
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
from kernels import get_kernel, get_local_kernel
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def load_hydra_kernel():
|
| 19 |
+
if os.environ.get("HYDRA_USE_HUB") == "1":
|
| 20 |
+
return get_kernel("Frosty40/hydra")
|
| 21 |
+
|
| 22 |
+
root = Path(__file__).resolve().parent
|
| 23 |
+
for variant in (root / "build", root):
|
| 24 |
+
if (variant / "metadata.json").exists():
|
| 25 |
+
return get_local_kernel(variant)
|
| 26 |
+
|
| 27 |
+
sys.path.insert(0, str(root / "torch-ext"))
|
| 28 |
+
import hydra
|
| 29 |
+
|
| 30 |
+
return hydra
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main() -> None:
|
| 34 |
+
if not torch.cuda.is_available():
|
| 35 |
+
raise SystemExit("Hydra requires CUDA for this example")
|
| 36 |
+
|
| 37 |
+
kernel = load_hydra_kernel()
|
| 38 |
+
q = torch.randn(1, 32, 1, 128, device="cuda", dtype=torch.bfloat16)
|
| 39 |
+
k = torch.randn(1, 8, 8192, 128, device="cuda", dtype=torch.bfloat16)
|
| 40 |
+
v = torch.randn(1, 8, 8192, 128, device="cuda", dtype=torch.bfloat16)
|
| 41 |
+
|
| 42 |
+
out = kernel.hydra(q, k, v)
|
| 43 |
+
print(f"Hydra decode: {tuple(q.shape)} x {tuple(k.shape)} -> {tuple(out.shape)}")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
main()
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 8 |
+
TORCH_EXT = ROOT / "torch-ext"
|
| 9 |
+
sys.path.insert(0, str(TORCH_EXT))
|
tests/test_csr.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
pytest.importorskip("torch")
|
| 6 |
+
pytest.importorskip("triton")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_dense_causal_csr_cpu_contract():
|
| 10 |
+
from hydra.csr import build_dense_causal_csr
|
| 11 |
+
|
| 12 |
+
row_ptr, col_idx, seq_lens = build_dense_causal_csr(
|
| 13 |
+
batch_size=1,
|
| 14 |
+
num_heads=2,
|
| 15 |
+
seq_len=128,
|
| 16 |
+
block_size=32,
|
| 17 |
+
device="cpu",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
assert row_ptr.shape == (1, 2, 5)
|
| 21 |
+
assert seq_lens.tolist() == [128]
|
| 22 |
+
assert row_ptr[0, 0].tolist() == [0, 1, 3, 6, 10]
|
| 23 |
+
assert col_idx[0, 0].tolist() == [0, 0, 1, 0, 1, 2, 0, 1, 2, 3]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_sliding_window_csr_keeps_diagonal_last():
|
| 27 |
+
from hydra.csr import build_sliding_window_csr
|
| 28 |
+
|
| 29 |
+
row_ptr, col_idx, _ = build_sliding_window_csr(
|
| 30 |
+
window=64,
|
| 31 |
+
seq_len=128,
|
| 32 |
+
block_size=32,
|
| 33 |
+
batch_size=1,
|
| 34 |
+
num_heads=1,
|
| 35 |
+
device="cpu",
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
rp = row_ptr[0, 0].tolist()
|
| 39 |
+
ci = col_idx[0, 0].tolist()
|
| 40 |
+
for q_block in range(4):
|
| 41 |
+
lo, hi = rp[q_block], rp[q_block + 1]
|
| 42 |
+
assert ci[hi - 1] == q_block
|
| 43 |
+
assert all(k < q_block for k in ci[lo : hi - 1])
|
tests/test_decode_parity.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
torch = pytest.importorskip("torch")
|
| 6 |
+
pytest.importorskip("triton")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
|
| 10 |
+
def test_decode_matches_sdpa_full_kv():
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
import hydra
|
| 13 |
+
|
| 14 |
+
torch.manual_seed(0)
|
| 15 |
+
q = torch.randn(1, 32, 1, 128, device="cuda", dtype=torch.bfloat16)
|
| 16 |
+
k = torch.randn(1, 8, 256, 128, device="cuda", dtype=torch.bfloat16)
|
| 17 |
+
v = torch.randn(1, 8, 256, 128, device="cuda", dtype=torch.bfloat16)
|
| 18 |
+
|
| 19 |
+
out = hydra.hydra(q, k, v)
|
| 20 |
+
k_rep = k.repeat_interleave(4, dim=1)
|
| 21 |
+
v_rep = v.repeat_interleave(4, dim=1)
|
| 22 |
+
ref = F.scaled_dot_product_attention(q, k_rep, v_rep, is_causal=False)
|
| 23 |
+
|
| 24 |
+
torch.testing.assert_close(out, ref, atol=3e-2, rtol=3e-2)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
|
| 28 |
+
def test_decode_matches_sdpa_sliding_window():
|
| 29 |
+
import torch.nn.functional as F
|
| 30 |
+
import hydra
|
| 31 |
+
|
| 32 |
+
torch.manual_seed(1)
|
| 33 |
+
q = torch.randn(1, 32, 1, 128, device="cuda", dtype=torch.bfloat16)
|
| 34 |
+
k = torch.randn(1, 8, 257, 128, device="cuda", dtype=torch.bfloat16)
|
| 35 |
+
v = torch.randn(1, 8, 257, 128, device="cuda", dtype=torch.bfloat16)
|
| 36 |
+
|
| 37 |
+
window = 96
|
| 38 |
+
out = hydra.hydra(q, k, v, sliding_window=window)
|
| 39 |
+
k_rep = k[:, :, -window:, :].repeat_interleave(4, dim=1)
|
| 40 |
+
v_rep = v[:, :, -window:, :].repeat_interleave(4, dim=1)
|
| 41 |
+
ref = F.scaled_dot_product_attention(q, k_rep, v_rep, is_causal=False)
|
| 42 |
+
|
| 43 |
+
torch.testing.assert_close(out, ref, atol=3e-2, rtol=3e-2)
|
tests/test_imports.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
pytest.importorskip("torch")
|
| 6 |
+
pytest.importorskip("triton")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def test_public_imports():
|
| 10 |
+
import hydra
|
| 11 |
+
|
| 12 |
+
assert callable(hydra.hydra)
|
| 13 |
+
assert hydra.hydra_attention is hydra.hydra
|
| 14 |
+
assert hydra.flash_attn_blackwell is hydra.hydra
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_policy_defaults():
|
| 18 |
+
from hydra.policy import RuntimePolicy
|
| 19 |
+
|
| 20 |
+
policy = RuntimePolicy()
|
| 21 |
+
assert policy.mode == "off"
|
| 22 |
+
assert policy.enabled is False
|
torch-ext/hydra/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hydra bounded-residency attention package.
|
| 2 |
+
|
| 3 |
+
This package currently exposes the extracted Triton attention kernels and
|
| 4 |
+
runtime policy layer from the research repo. The public API is deliberately
|
| 5 |
+
small while the benchmark evidence is being consolidated.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from .api import flash_attn_blackwell, hydra, hydra_attention
|
| 9 |
+
from .csr import build_dense_causal_csr, build_sliding_window_csr
|
| 10 |
+
from .kernel_decode import launch_attn_fwd_decode
|
| 11 |
+
from .policy import (
|
| 12 |
+
RuntimePolicy,
|
| 13 |
+
last_policy_decision,
|
| 14 |
+
policy_history,
|
| 15 |
+
set_runtime_policy,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
__version__ = "0.1.0"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
__all__ = [
|
| 22 |
+
"hydra",
|
| 23 |
+
"hydra_attention",
|
| 24 |
+
"flash_attn_blackwell",
|
| 25 |
+
"build_dense_causal_csr",
|
| 26 |
+
"build_sliding_window_csr",
|
| 27 |
+
"launch_attn_fwd_decode",
|
| 28 |
+
"RuntimePolicy",
|
| 29 |
+
"set_runtime_policy",
|
| 30 |
+
"last_policy_decision",
|
| 31 |
+
"policy_history",
|
| 32 |
+
]
|
torch-ext/hydra/api.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public Python API (perf-tuned variant).
|
| 2 |
+
|
| 3 |
+
Same public signature as ``hydra.api.hydra``.
|
| 4 |
+
|
| 5 |
+
Internally:
|
| 6 |
+
|
| 7 |
+
- Caches the (row_ptr, col_idx, seq_lens) tuple keyed on
|
| 8 |
+
``(B, Hq, T, BLOCK_SIZE, window_arg, device, dtype_marker)``. The
|
| 9 |
+
upstream api re-builds the CSR on every call, which (a) pays a Python
|
| 10 |
+
cost per call and (b) defeats any data_ptr()-based downstream cache.
|
| 11 |
+
|
| 12 |
+
Cache invalidation rule
|
| 13 |
+
-----------------------
|
| 14 |
+
A new entry is created whenever any of the following change:
|
| 15 |
+
- batch size B
|
| 16 |
+
- num query heads Hq (CSR is broadcast across H so this is part of key)
|
| 17 |
+
- sequence length T (kernel tile constraint: T % BLOCK_SIZE == 0)
|
| 18 |
+
- BLOCK_SIZE (module-level constant; included for safety)
|
| 19 |
+
- window_arg (the effective in-kernel window; 0 for dense)
|
| 20 |
+
- device (per-CUDA-device pattern; CPU vs CUDA included)
|
| 21 |
+
|
| 22 |
+
The cache is bounded (default 32 entries). Reaching the cap evicts the
|
| 23 |
+
LRU entry. ``seq_lens`` is included in the cached tuple — by design
|
| 24 |
+
``seq_lens`` is a single ``torch.full`` produced by the CSR builders
|
| 25 |
+
and depends only on B / T, which are part of the key.
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import os
|
| 30 |
+
from collections import OrderedDict
|
| 31 |
+
|
| 32 |
+
import torch
|
| 33 |
+
|
| 34 |
+
from .csr import build_dense_causal_csr, build_sliding_window_csr
|
| 35 |
+
from .kernel_fwd import BLOCK_SIZE, HEAD_DIM
|
| 36 |
+
from .function import FlashAttnHydraFunction, FlashAttnHydraDecodeFunction
|
| 37 |
+
from .policy import apply_runtime_policy
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
_CSR_CACHE_MAX = int(os.environ.get("HYDRA_CSR_CACHE_MAX", "32"))
|
| 41 |
+
_CSR_CACHE: "OrderedDict[tuple, tuple[torch.Tensor, torch.Tensor, torch.Tensor]]" = OrderedDict()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _get_csr(
|
| 45 |
+
B: int,
|
| 46 |
+
Hq: int,
|
| 47 |
+
T: int,
|
| 48 |
+
window_arg: int,
|
| 49 |
+
device: torch.device,
|
| 50 |
+
sliding_window: int | None,
|
| 51 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 52 |
+
key = (
|
| 53 |
+
B,
|
| 54 |
+
Hq,
|
| 55 |
+
T,
|
| 56 |
+
BLOCK_SIZE,
|
| 57 |
+
window_arg,
|
| 58 |
+
sliding_window,
|
| 59 |
+
device.type,
|
| 60 |
+
getattr(device, "index", None),
|
| 61 |
+
)
|
| 62 |
+
hit = _CSR_CACHE.get(key)
|
| 63 |
+
if hit is not None:
|
| 64 |
+
_CSR_CACHE.move_to_end(key)
|
| 65 |
+
return hit
|
| 66 |
+
|
| 67 |
+
if window_arg == 0:
|
| 68 |
+
row_ptr, col_idx, seq_lens = build_dense_causal_csr(B, Hq, T, BLOCK_SIZE, device)
|
| 69 |
+
else:
|
| 70 |
+
row_ptr, col_idx, seq_lens = build_sliding_window_csr(
|
| 71 |
+
window_arg, T, BLOCK_SIZE, B, Hq, device
|
| 72 |
+
)
|
| 73 |
+
_CSR_CACHE[key] = (row_ptr, col_idx, seq_lens)
|
| 74 |
+
while len(_CSR_CACHE) > _CSR_CACHE_MAX:
|
| 75 |
+
_CSR_CACHE.popitem(last=False)
|
| 76 |
+
return row_ptr, col_idx, seq_lens
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def hydra(
|
| 80 |
+
q: torch.Tensor,
|
| 81 |
+
k: torch.Tensor,
|
| 82 |
+
v: torch.Tensor,
|
| 83 |
+
*,
|
| 84 |
+
is_causal: bool = True,
|
| 85 |
+
sliding_window: int | None = None,
|
| 86 |
+
policy_layer_idx: int | None = None,
|
| 87 |
+
precision: str = "high",
|
| 88 |
+
) -> torch.Tensor:
|
| 89 |
+
"""Blackwell-tuned causal FlashAttention with GQA and optional sliding window.
|
| 90 |
+
|
| 91 |
+
Same semantics as ``hydra.hydra``.
|
| 92 |
+
"""
|
| 93 |
+
if precision not in {"high", "fast"}:
|
| 94 |
+
raise ValueError(f"precision must be 'high' or 'fast', got {precision!r}")
|
| 95 |
+
if precision != "high":
|
| 96 |
+
raise NotImplementedError("precision='fast' is not wired in this extracted Hydra API yet")
|
| 97 |
+
if not is_causal:
|
| 98 |
+
raise NotImplementedError("non-causal attention is not supported in this version")
|
| 99 |
+
if q.dtype != torch.bfloat16 or k.dtype != torch.bfloat16 or v.dtype != torch.bfloat16:
|
| 100 |
+
raise ValueError(f"q/k/v must be bf16; got {q.dtype}, {k.dtype}, {v.dtype}")
|
| 101 |
+
if q.dim() != 4 or k.dim() != 4 or v.dim() != 4:
|
| 102 |
+
raise ValueError(f"q/k/v must be 4D (B, H, T, D); got {q.shape}, {k.shape}, {v.shape}")
|
| 103 |
+
|
| 104 |
+
B, Hq, T, D = q.shape
|
| 105 |
+
if D != HEAD_DIM:
|
| 106 |
+
raise ValueError(f"head_dim={D} must equal HEAD_DIM={HEAD_DIM}")
|
| 107 |
+
Hkv = k.shape[1]
|
| 108 |
+
if Hq % Hkv != 0:
|
| 109 |
+
raise ValueError(f"H_q={Hq} must be a multiple of H_kv={Hkv} (GQA constraint)")
|
| 110 |
+
|
| 111 |
+
decision = apply_runtime_policy(
|
| 112 |
+
q, k, v,
|
| 113 |
+
sliding_window=sliding_window,
|
| 114 |
+
block_size=BLOCK_SIZE,
|
| 115 |
+
head_dim=HEAD_DIM,
|
| 116 |
+
layer_idx=policy_layer_idx,
|
| 117 |
+
)
|
| 118 |
+
sliding_window = decision.effective_window
|
| 119 |
+
|
| 120 |
+
# Decode-step specialization: T_q == 1 with arbitrary T_kv. The decode
|
| 121 |
+
# kernel does not require T_kv % BLOCK_SIZE == 0 and has no Q-blocking,
|
| 122 |
+
# so it services HF generation's per-token call without the eager fallback.
|
| 123 |
+
if T == 1:
|
| 124 |
+
if sliding_window is not None and sliding_window <= 0:
|
| 125 |
+
raise ValueError(f"sliding_window must be positive, got {sliding_window}")
|
| 126 |
+
window_arg = 0 if sliding_window is None else sliding_window
|
| 127 |
+
return FlashAttnHydraDecodeFunction.apply(q, k, v, window_arg)
|
| 128 |
+
|
| 129 |
+
if T % BLOCK_SIZE != 0:
|
| 130 |
+
raise ValueError(f"T={T} must be a multiple of BLOCK_SIZE={BLOCK_SIZE}")
|
| 131 |
+
|
| 132 |
+
if sliding_window is None:
|
| 133 |
+
window_arg = 0
|
| 134 |
+
else:
|
| 135 |
+
if sliding_window <= 0:
|
| 136 |
+
raise ValueError(f"sliding_window must be positive, got {sliding_window}")
|
| 137 |
+
# When the window covers the full sequence, this degrades to dense causal;
|
| 138 |
+
# skip the kernel-side masking work in that case.
|
| 139 |
+
if sliding_window >= T:
|
| 140 |
+
window_arg = 0
|
| 141 |
+
else:
|
| 142 |
+
window_arg = sliding_window
|
| 143 |
+
|
| 144 |
+
row_ptr, col_idx, seq_lens = _get_csr(B, Hq, T, window_arg, q.device, sliding_window)
|
| 145 |
+
|
| 146 |
+
return FlashAttnHydraFunction.apply(q, k, v, row_ptr, col_idx, seq_lens, window_arg)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def csr_cache_clear() -> None:
|
| 150 |
+
"""Drop all cached (row_ptr, col_idx, seq_lens) entries."""
|
| 151 |
+
_CSR_CACHE.clear()
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def csr_cache_info() -> dict:
|
| 155 |
+
return {"size": len(_CSR_CACHE), "max": _CSR_CACHE_MAX}
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# Compatibility aliases while the extraction is still being consolidated.
|
| 159 |
+
hydra_attention = hydra
|
| 160 |
+
flash_attn_blackwell = hydra
|
torch-ext/hydra/csr.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CSR (Compressed Sparse Row) builders for the Blackwell FA kernel.
|
| 2 |
+
|
| 3 |
+
Convention
|
| 4 |
+
----------
|
| 5 |
+
The kernel iterates each Q-block's CSR row as ``ColIdx[ci_lo .. ci_hi)`` where
|
| 6 |
+
the **last entry** is the diagonal block (``col_idx[ci_hi - 1] == q_block_id``)
|
| 7 |
+
and the preceding entries are off-diagonal lower-triangular K-blocks (all
|
| 8 |
+
strictly less than ``q_block_id``, by the causal constraint).
|
| 9 |
+
|
| 10 |
+
The kernel applies causal masking + hi/lo precision split *within* the
|
| 11 |
+
diagonal block and a standard online-softmax merge for the off-diagonal
|
| 12 |
+
blocks. Builders MUST place the diagonal as the last entry per Q row;
|
| 13 |
+
otherwise the kernel reads the wrong block as "the one needing causal
|
| 14 |
+
masking" and produces silently-wrong attention outputs.
|
| 15 |
+
|
| 16 |
+
All builders return ``(row_ptr, col_idx, seq_lens)``:
|
| 17 |
+
row_ptr: (B, H, num_q_blocks + 1) int32 — CSR row pointers per (B, H).
|
| 18 |
+
col_idx: (B, H, total_nnz) int32 — CSR column indices.
|
| 19 |
+
seq_lens: (B,) int32 — per-batch sequence length.
|
| 20 |
+
|
| 21 |
+
The (B, H) broadcast is materialized as a contiguous expand so the kernel can
|
| 22 |
+
index without per-head pointer arithmetic.
|
| 23 |
+
"""
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import math
|
| 27 |
+
|
| 28 |
+
import torch
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _broadcast_csr(
|
| 32 |
+
row_ptr_row: list[int],
|
| 33 |
+
col_idx_row: list[int],
|
| 34 |
+
batch_size: int,
|
| 35 |
+
num_heads: int,
|
| 36 |
+
seq_len: int,
|
| 37 |
+
device: torch.device | str,
|
| 38 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 39 |
+
"""Lift per-row Python lists to per-(B,H) contiguous int32 tensors."""
|
| 40 |
+
row_ptr = (
|
| 41 |
+
torch.tensor(row_ptr_row, device=device, dtype=torch.int32)
|
| 42 |
+
.view(1, 1, -1)
|
| 43 |
+
.expand(batch_size, num_heads, -1)
|
| 44 |
+
.contiguous()
|
| 45 |
+
)
|
| 46 |
+
col_idx = (
|
| 47 |
+
torch.tensor(col_idx_row, device=device, dtype=torch.int32)
|
| 48 |
+
.view(1, 1, -1)
|
| 49 |
+
.expand(batch_size, num_heads, -1)
|
| 50 |
+
.contiguous()
|
| 51 |
+
)
|
| 52 |
+
seq_lens = torch.full((batch_size,), seq_len, device=device, dtype=torch.int32)
|
| 53 |
+
return row_ptr, col_idx, seq_lens
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def build_dense_causal_csr(
|
| 57 |
+
batch_size: int,
|
| 58 |
+
num_heads: int,
|
| 59 |
+
seq_len: int,
|
| 60 |
+
block_size: int,
|
| 61 |
+
device: torch.device | str,
|
| 62 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 63 |
+
"""Dense lower-triangular causal pattern.
|
| 64 |
+
|
| 65 |
+
For Q-block i: off-diagonal entries enumerate K-blocks 0..i-1, then the
|
| 66 |
+
diagonal placeholder i. nnz scales as O(num_q_blocks^2 / 2).
|
| 67 |
+
"""
|
| 68 |
+
num_q_blocks = math.ceil(seq_len / block_size)
|
| 69 |
+
row_ptr_row = [0]
|
| 70 |
+
col_idx_row: list[int] = []
|
| 71 |
+
for q_block in range(num_q_blocks):
|
| 72 |
+
col_idx_row.extend(range(q_block))
|
| 73 |
+
col_idx_row.append(q_block)
|
| 74 |
+
row_ptr_row.append(len(col_idx_row))
|
| 75 |
+
return _broadcast_csr(row_ptr_row, col_idx_row, batch_size, num_heads, seq_len, device)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def build_sliding_window_csr(
|
| 79 |
+
window: int,
|
| 80 |
+
seq_len: int,
|
| 81 |
+
block_size: int,
|
| 82 |
+
batch_size: int,
|
| 83 |
+
num_heads: int,
|
| 84 |
+
device: torch.device | str,
|
| 85 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 86 |
+
"""Causal sliding-window pattern, widened for per-Q-token masking.
|
| 87 |
+
|
| 88 |
+
``window`` is measured in tokens (transformers / Mistral convention):
|
| 89 |
+
Q-token at position p attends K-tokens [max(0, p - window + 1), p].
|
| 90 |
+
|
| 91 |
+
The CSR is built using the **"some Q-token attends"** criterion — a
|
| 92 |
+
K-block is included if ANY Q-token in the Q-block could attend ANY
|
| 93 |
+
K-token in it. The kernel applies a per-Q-token in-window mask inside
|
| 94 |
+
the off-diagonal-leftmost and diagonal blocks, so over-included
|
| 95 |
+
K-tokens at the leftmost edge get masked to -inf and contribute zero.
|
| 96 |
+
|
| 97 |
+
Concretely, for Q-block i, the leftmost-attended K-token across all
|
| 98 |
+
Q-tokens in the block is ``max(0, i*BS - window + 1)``. The K-block
|
| 99 |
+
containing that token is its position // BS.
|
| 100 |
+
|
| 101 |
+
Edge cases:
|
| 102 |
+
- window >= seq_len: degrades to dense_causal (full lower triangle).
|
| 103 |
+
- window == 1: each Q-token only attends itself; diagonal-only with
|
| 104 |
+
within-block causal mask.
|
| 105 |
+
|
| 106 |
+
nnz scales as O(num_q_blocks * ceil(window / BS)) — linear in T for
|
| 107 |
+
fixed window, which is the point of sliding window.
|
| 108 |
+
"""
|
| 109 |
+
if window <= 0:
|
| 110 |
+
raise ValueError(f"window must be positive, got {window}")
|
| 111 |
+
if block_size <= 0:
|
| 112 |
+
raise ValueError(f"block_size must be positive, got {block_size}")
|
| 113 |
+
num_q_blocks = math.ceil(seq_len / block_size)
|
| 114 |
+
row_ptr_row = [0]
|
| 115 |
+
col_idx_row: list[int] = []
|
| 116 |
+
for q_block in range(num_q_blocks):
|
| 117 |
+
# Smallest K-token position attended by any Q-token in this Q-block.
|
| 118 |
+
# The leftmost Q-token (position q_block * BS) has the most reach to
|
| 119 |
+
# the left; its window starts at q_block*BS - window + 1.
|
| 120 |
+
left_bound = q_block * block_size - window + 1
|
| 121 |
+
if left_bound <= 0:
|
| 122 |
+
k_min_block = 0
|
| 123 |
+
else:
|
| 124 |
+
# Smallest n with (n+1)*BS - 1 >= left_bound, i.e., n = left_bound // BS.
|
| 125 |
+
k_min_block = left_bound // block_size
|
| 126 |
+
# Off-diagonal entries: k_min_block .. q_block - 1
|
| 127 |
+
col_idx_row.extend(range(k_min_block, q_block))
|
| 128 |
+
# Diagonal placeholder
|
| 129 |
+
col_idx_row.append(q_block)
|
| 130 |
+
row_ptr_row.append(len(col_idx_row))
|
| 131 |
+
return _broadcast_csr(row_ptr_row, col_idx_row, batch_size, num_heads, seq_len, device)
|
torch-ext/hydra/function.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Autograd ``Function`` wrapper for the Blackwell FA kernel (perf-tuned variant).
|
| 2 |
+
|
| 3 |
+
Changes vs. upstream ``hydra.function``:
|
| 4 |
+
|
| 5 |
+
- The transposed-CSR is fetched through ``build_csrT_cached`` (LRU keyed
|
| 6 |
+
on pattern *content*, not ``data_ptr()``). The upstream cache misses
|
| 7 |
+
every call today because ``api.hydra`` re-allocates
|
| 8 |
+
``row_ptr`` / ``col_idx`` per invocation; the new key fixes that.
|
| 9 |
+
|
| 10 |
+
- ``rp_T`` / ``ci_T`` are saved through ``ctx.save_for_backward`` so the
|
| 11 |
+
backward never re-builds them. This matches the upstream behaviour but
|
| 12 |
+
is now meaningfully cheap on cache hit (~O(num_q_blocks) device-side
|
| 13 |
+
for the key check).
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from .kernel_fwd import BLOCK_SIZE, launch_attn_fwd
|
| 20 |
+
from .kernel_bwd import build_csrT_cached, launch_attn_bwd
|
| 21 |
+
from .kernel_decode import launch_attn_fwd_decode
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class FlashAttnHydraFunction(torch.autograd.Function):
|
| 25 |
+
"""Autograd-aware wrapper. See ``api.hydra`` for the public surface."""
|
| 26 |
+
|
| 27 |
+
@staticmethod
|
| 28 |
+
def forward(ctx, q, k, v, row_ptr, col_idx, seq_lens, window):
|
| 29 |
+
o, lse = launch_attn_fwd(q, k, v, row_ptr, col_idx, seq_lens, window=window)
|
| 30 |
+
num_q_blocks = q.shape[2] // BLOCK_SIZE
|
| 31 |
+
rp_T, ci_T = build_csrT_cached(row_ptr, col_idx, num_q_blocks)
|
| 32 |
+
ctx.save_for_backward(q, k, v, o, lse, row_ptr, col_idx, seq_lens, rp_T, ci_T)
|
| 33 |
+
ctx.window = window
|
| 34 |
+
return o
|
| 35 |
+
|
| 36 |
+
@staticmethod
|
| 37 |
+
def backward(ctx, do):
|
| 38 |
+
q, k, v, o, lse, row_ptr, col_idx, seq_lens, rp_T, ci_T = ctx.saved_tensors
|
| 39 |
+
# Both o and do must be contiguous for the delta kernel. The saved o
|
| 40 |
+
# comes from launch_attn_fwd (torch.empty_like(q) — contiguous if q is)
|
| 41 |
+
# but transformers can save a view of o into ctx, and `do` arriving
|
| 42 |
+
# from upstream is often a transpose view from the attention output
|
| 43 |
+
# reshape. Make both contiguous defensively.
|
| 44 |
+
dq, dk, dv = launch_attn_bwd(
|
| 45 |
+
q, k, v, o.contiguous(), do.contiguous(), lse,
|
| 46 |
+
row_ptr, col_idx, seq_lens,
|
| 47 |
+
row_ptr_T=rp_T, col_idx_T=ci_T,
|
| 48 |
+
window=ctx.window,
|
| 49 |
+
)
|
| 50 |
+
return dq, dk, dv, None, None, None, None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class FlashAttnHydraDecodeFunction(torch.autograd.Function):
|
| 54 |
+
"""Forward-only autograd wrapper for the T_q==1 decode kernel.
|
| 55 |
+
|
| 56 |
+
Generation runs under torch.no_grad so backward is intentionally
|
| 57 |
+
unimplemented; calling .backward() raises with a clear message.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def forward(ctx, q, k, v, window):
|
| 62 |
+
o, lse = launch_attn_fwd_decode(q, k, v, window=window)
|
| 63 |
+
ctx.save_for_backward(q, k, v, o, lse)
|
| 64 |
+
ctx.window = window
|
| 65 |
+
ctx.set_materialize_grads(False)
|
| 66 |
+
return o
|
| 67 |
+
|
| 68 |
+
@staticmethod
|
| 69 |
+
def backward(ctx, do):
|
| 70 |
+
raise NotImplementedError(
|
| 71 |
+
"FlashAttnHydraDecodeFunction has no backward. "
|
| 72 |
+
"Decode-step (T_q == 1) is forward-only — wrap your call in "
|
| 73 |
+
"torch.no_grad() (HF generation does this automatically). "
|
| 74 |
+
"For training, use the prefill kernel with T_q == T_kv."
|
| 75 |
+
)
|
torch-ext/hydra/kernel_bwd.py
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backward Triton kernels for Blackwell-tuned causal FlashAttention.
|
| 2 |
+
|
| 3 |
+
Perf-tuned variant of ``hydra.kernel_bwd`` that
|
| 4 |
+
|
| 5 |
+
- keeps the Triton kernels byte-for-byte identical to the upstream
|
| 6 |
+
versions (so the gradient-correctness story is unchanged),
|
| 7 |
+
- replaces ``build_csrT``'s CPU + numpy round-trip with a pure-torch /
|
| 8 |
+
GPU-resident equivalent (``build_csrT_gpu``) that produces the same
|
| 9 |
+
output and stays on the input device,
|
| 10 |
+
- exposes a module-level LRU cache (``build_csrT_cached``) keyed on
|
| 11 |
+
pattern shape rather than ``data_ptr()`` so per-call rebuilds of
|
| 12 |
+
``row_ptr`` / ``col_idx`` (as the api does today) still hit the
|
| 13 |
+
cache,
|
| 14 |
+
- clears the host-side ``delta = (do.float() * o.float()).sum(-1)``
|
| 15 |
+
cast spew: the existing launcher computes ``delta`` with two fp32
|
| 16 |
+
upcasts and a stride-collapsing ``.contiguous()`` every call; we keep
|
| 17 |
+
the math but drop the redundant ``.contiguous()`` (the result of
|
| 18 |
+
``.sum(-1)`` on a contiguous 4D tensor is already contiguous).
|
| 19 |
+
|
| 20 |
+
Public API is preserved: ``launch_attn_bwd`` has the same signature, and
|
| 21 |
+
``build_csrT`` is kept as a thin wrapper that dispatches to the pure-GPU
|
| 22 |
+
builder when possible (and falls back to the original CPU+numpy path
|
| 23 |
+
when the input is on CPU, so the CPU equivalence test exercises the
|
| 24 |
+
same code path).
|
| 25 |
+
"""
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import math
|
| 29 |
+
import os
|
| 30 |
+
from collections import OrderedDict
|
| 31 |
+
|
| 32 |
+
import torch
|
| 33 |
+
import triton
|
| 34 |
+
import triton.language as tl
|
| 35 |
+
|
| 36 |
+
from .kernel_fwd import BLOCK_SIZE, HEAD_DIM
|
| 37 |
+
from .kernel_delta import launch_compute_delta
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
_BWD_NUM_WARPS = int(os.environ.get("HYDRA_BWD_NUM_WARPS", "4"))
|
| 41 |
+
_BWD_NUM_STAGES = int(os.environ.get("HYDRA_BWD_NUM_STAGES", "1"))
|
| 42 |
+
_DISABLE_AUTOTUNE = int(os.environ.get("HYDRA_DISABLE_AUTOTUNE", "0"))
|
| 43 |
+
|
| 44 |
+
_CSRT_CACHE_MAX = int(os.environ.get("HYDRA_CSRT_CACHE_MAX", "32"))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _autotune_configs() -> list[triton.Config]:
|
| 48 |
+
configs: list[triton.Config] = []
|
| 49 |
+
for num_warps in (2, 4, 8):
|
| 50 |
+
for num_stages in (1, 2):
|
| 51 |
+
configs.append(triton.Config({}, num_warps=num_warps, num_stages=num_stages))
|
| 52 |
+
return configs
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _kernel_decorator(jit_kernel):
|
| 56 |
+
if _DISABLE_AUTOTUNE:
|
| 57 |
+
return jit_kernel
|
| 58 |
+
return triton.autotune(
|
| 59 |
+
configs=_autotune_configs(),
|
| 60 |
+
# Include WINDOW: sliding-window patterns have 2-4× fewer K-blocks per
|
| 61 |
+
# Q-block than dense, so optimal warps/stages plausibly differ. Keying
|
| 62 |
+
# them separately costs one extra autotune sweep per (T, window) pair.
|
| 63 |
+
key=["T_MAX", "D", "NUM_HEADS", "NUM_KV_HEADS", "WINDOW"],
|
| 64 |
+
)(jit_kernel)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ----------------------------- csrT builders -----------------------------
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _build_csrT_cpu_reference(row_ptr: torch.Tensor, col_idx: torch.Tensor, num_blocks: int):
|
| 71 |
+
"""Original CPU + numpy implementation, kept for reference / fallback.
|
| 72 |
+
|
| 73 |
+
Identical semantics to the upstream ``build_csrT`` in
|
| 74 |
+
``hydra.kernel_bwd``: per (B, H), enumerate which
|
| 75 |
+
Q-blocks reference each K-block as an OFF-diagonal attendee. The
|
| 76 |
+
diagonal placeholder at ``col_idx[ci_hi - 1]`` is excluded.
|
| 77 |
+
"""
|
| 78 |
+
import numpy as np
|
| 79 |
+
rp = row_ptr.detach().cpu().numpy()
|
| 80 |
+
ci = col_idx.detach().cpu().numpy()
|
| 81 |
+
B, H = rp.shape[0], rp.shape[1]
|
| 82 |
+
rp_flat = rp.reshape(B * H, -1)
|
| 83 |
+
ci_flat = ci.reshape(B * H, -1)
|
| 84 |
+
rpt_rows: list[list[int]] = []
|
| 85 |
+
cit_rows: list[list[int]] = []
|
| 86 |
+
for bh in range(B * H):
|
| 87 |
+
bucket: list[list[int]] = [[] for _ in range(num_blocks)]
|
| 88 |
+
rp_row = rp_flat[bh]
|
| 89 |
+
ci_row = ci_flat[bh]
|
| 90 |
+
for i in range(num_blocks):
|
| 91 |
+
lo = int(rp_row[i])
|
| 92 |
+
hi = int(rp_row[i + 1])
|
| 93 |
+
for p in range(lo, max(lo, hi - 1)):
|
| 94 |
+
bucket[int(ci_row[p])].append(i)
|
| 95 |
+
offs = [0]
|
| 96 |
+
vals: list[int] = []
|
| 97 |
+
for k in range(num_blocks):
|
| 98 |
+
vals.extend(bucket[k])
|
| 99 |
+
offs.append(len(vals))
|
| 100 |
+
rpt_rows.append(offs)
|
| 101 |
+
cit_rows.append(vals)
|
| 102 |
+
max_nnz = max(1, max(len(v) for v in cit_rows))
|
| 103 |
+
cit_pad = np.zeros((B * H, max_nnz), dtype=np.int32)
|
| 104 |
+
for bh, v in enumerate(cit_rows):
|
| 105 |
+
if v:
|
| 106 |
+
cit_pad[bh, : len(v)] = v
|
| 107 |
+
rpt_np = np.asarray(rpt_rows, dtype=np.int32)
|
| 108 |
+
rp_T = torch.from_numpy(rpt_np).to(row_ptr.device).reshape(B, H, num_blocks + 1).contiguous()
|
| 109 |
+
ci_T = torch.from_numpy(cit_pad).to(row_ptr.device).reshape(B, H, max_nnz).contiguous()
|
| 110 |
+
return rp_T, ci_T
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _is_broadcast_pattern(row_ptr: torch.Tensor, col_idx: torch.Tensor) -> bool:
|
| 114 |
+
"""Cheap check: does the (B, H) pattern collapse to a single shared row?
|
| 115 |
+
|
| 116 |
+
The CSR builders in ``hydra.csr`` always produce a
|
| 117 |
+
broadcast-then-contiguous pattern. We exploit that to compute the
|
| 118 |
+
transposed CSR once and ``.expand().contiguous()`` it back to
|
| 119 |
+
(B, H). For safety we verify by comparing row_ptr/col_idx against
|
| 120 |
+
the (0, 0) head.
|
| 121 |
+
"""
|
| 122 |
+
B, H = row_ptr.shape[0], row_ptr.shape[1]
|
| 123 |
+
if B == 1 and H == 1:
|
| 124 |
+
return True
|
| 125 |
+
rp0 = row_ptr[0, 0]
|
| 126 |
+
ci0 = col_idx[0, 0]
|
| 127 |
+
# equal_to expects same-shape comparand; use a single torch.equal per
|
| 128 |
+
# axis. This costs O(B*H*nnz) but is GPU-resident and ~1us for typical
|
| 129 |
+
# shapes — far cheaper than the .cpu()/.numpy() round-trip.
|
| 130 |
+
return bool(torch.all(row_ptr == rp0).item()) and bool(torch.all(col_idx == ci0).item())
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _build_csrT_single_row(rp_row: torch.Tensor, ci_row: torch.Tensor, num_blocks: int):
|
| 134 |
+
"""Pure-torch transposed-CSR for a single (B=H=1) row.
|
| 135 |
+
|
| 136 |
+
Produces ``(rpt_row, cit_row)`` of dtype int32 on the same device as
|
| 137 |
+
the inputs. The ordering of q_block_ids within each K-row matches
|
| 138 |
+
the CPU reference: ascending q_block_id.
|
| 139 |
+
"""
|
| 140 |
+
device = rp_row.device
|
| 141 |
+
rp_row = rp_row.to(torch.int64)
|
| 142 |
+
ci_row = ci_row.to(torch.int64)
|
| 143 |
+
|
| 144 |
+
# Off-diagonal "off" slices per Q-row: [lo, hi - 1). Total off-diag
|
| 145 |
+
# entries summed across Q-blocks equals nnz - num_blocks (one diag
|
| 146 |
+
# per Q-block). Build a flat (q_block_id, k_block_id) edge list.
|
| 147 |
+
counts = (rp_row[1:] - rp_row[:-1] - 1).clamp(min=0) # off-diag count per Q-block
|
| 148 |
+
num_offdiag = int(counts.sum().item())
|
| 149 |
+
if num_offdiag == 0:
|
| 150 |
+
rp_T_row = torch.zeros(num_blocks + 1, dtype=torch.int32, device=device)
|
| 151 |
+
ci_T_row = torch.zeros(1, dtype=torch.int32, device=device)
|
| 152 |
+
return rp_T_row, ci_T_row, 1
|
| 153 |
+
|
| 154 |
+
# q_idx[e] = which Q-block this edge belongs to.
|
| 155 |
+
q_idx = torch.repeat_interleave(
|
| 156 |
+
torch.arange(num_blocks, dtype=torch.int64, device=device),
|
| 157 |
+
counts,
|
| 158 |
+
)
|
| 159 |
+
# k_idx[e] = which K-block this edge points at. We need the slice
|
| 160 |
+
# ci_row[lo : hi - 1] for each Q-row, concatenated. Build a flat
|
| 161 |
+
# index into ci_row by starting at lo for each Q-block and adding the
|
| 162 |
+
# within-row offset (0, 1, 2, ...).
|
| 163 |
+
starts = rp_row[:-1] # lo per Q-block
|
| 164 |
+
edge_within = torch.arange(num_offdiag, dtype=torch.int64, device=device) - torch.repeat_interleave(
|
| 165 |
+
torch.cat([
|
| 166 |
+
torch.zeros(1, dtype=torch.int64, device=device),
|
| 167 |
+
counts.cumsum(0)[:-1],
|
| 168 |
+
]),
|
| 169 |
+
counts,
|
| 170 |
+
)
|
| 171 |
+
edge_pos = torch.repeat_interleave(starts, counts) + edge_within
|
| 172 |
+
k_idx = ci_row[edge_pos]
|
| 173 |
+
|
| 174 |
+
# Sort edges by k_idx (stable so q_idx within a bucket stays ascending).
|
| 175 |
+
sort_idx = torch.argsort(k_idx, stable=True)
|
| 176 |
+
k_idx_sorted = k_idx[sort_idx]
|
| 177 |
+
q_idx_sorted = q_idx[sort_idx]
|
| 178 |
+
|
| 179 |
+
# rp_T[k+1] = count of edges with k_idx <= k. Use bincount over [0, num_blocks).
|
| 180 |
+
per_k_count = torch.bincount(k_idx_sorted, minlength=num_blocks)
|
| 181 |
+
rp_T_row = torch.zeros(num_blocks + 1, dtype=torch.int64, device=device)
|
| 182 |
+
rp_T_row[1:] = per_k_count.cumsum(0)
|
| 183 |
+
|
| 184 |
+
max_nnz = max(1, num_offdiag)
|
| 185 |
+
ci_T_row = torch.zeros(max_nnz, dtype=torch.int32, device=device)
|
| 186 |
+
ci_T_row[:num_offdiag] = q_idx_sorted.to(torch.int32)
|
| 187 |
+
return rp_T_row.to(torch.int32), ci_T_row, max_nnz
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def build_csrT_gpu(row_ptr: torch.Tensor, col_idx: torch.Tensor, num_blocks: int):
|
| 191 |
+
"""Pure-device transposed-CSR builder.
|
| 192 |
+
|
| 193 |
+
Equivalent to ``build_csrT`` but never touches CPU. When the input
|
| 194 |
+
pattern is the broadcast-across-(B,H) form (the common case), only
|
| 195 |
+
one head's worth of work is done and the result is expanded.
|
| 196 |
+
|
| 197 |
+
Falls back to a per-(B,H) torch implementation for the non-broadcast
|
| 198 |
+
case (no current api path produces that, but keep it safe).
|
| 199 |
+
"""
|
| 200 |
+
if row_ptr.dim() != 3 or col_idx.dim() != 3:
|
| 201 |
+
raise ValueError(f"expected 3D row_ptr/col_idx; got {row_ptr.shape}, {col_idx.shape}")
|
| 202 |
+
B, H = row_ptr.shape[0], row_ptr.shape[1]
|
| 203 |
+
|
| 204 |
+
if _is_broadcast_pattern(row_ptr, col_idx):
|
| 205 |
+
rp_T_row, ci_T_row, max_nnz = _build_csrT_single_row(row_ptr[0, 0], col_idx[0, 0], num_blocks)
|
| 206 |
+
rp_T = rp_T_row.view(1, 1, num_blocks + 1).expand(B, H, num_blocks + 1).contiguous()
|
| 207 |
+
ci_T = ci_T_row.view(1, 1, max_nnz).expand(B, H, max_nnz).contiguous()
|
| 208 |
+
return rp_T, ci_T
|
| 209 |
+
|
| 210 |
+
# Non-broadcast path: per-(B,H) computation. We loop in Python over
|
| 211 |
+
# the B*H heads but each head's work stays on-device. For B*H up to
|
| 212 |
+
# a few thousand this is still much cheaper than .cpu().numpy().
|
| 213 |
+
out_rp_rows: list[torch.Tensor] = []
|
| 214 |
+
out_ci_rows: list[torch.Tensor] = []
|
| 215 |
+
max_nnz_seen = 1
|
| 216 |
+
for bh in range(B * H):
|
| 217 |
+
bi, hi = bh // H, bh % H
|
| 218 |
+
rp_T_row, ci_T_row, mn = _build_csrT_single_row(row_ptr[bi, hi], col_idx[bi, hi], num_blocks)
|
| 219 |
+
out_rp_rows.append(rp_T_row)
|
| 220 |
+
out_ci_rows.append(ci_T_row)
|
| 221 |
+
max_nnz_seen = max(max_nnz_seen, mn)
|
| 222 |
+
|
| 223 |
+
rp_T = torch.stack(out_rp_rows, dim=0).view(B, H, num_blocks + 1).contiguous()
|
| 224 |
+
# Right-pad to max_nnz_seen.
|
| 225 |
+
ci_T = torch.zeros(B * H, max_nnz_seen, dtype=torch.int32, device=row_ptr.device)
|
| 226 |
+
for bh, row in enumerate(out_ci_rows):
|
| 227 |
+
ci_T[bh, : row.shape[0]] = row
|
| 228 |
+
ci_T = ci_T.view(B, H, max_nnz_seen).contiguous()
|
| 229 |
+
return rp_T, ci_T
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def build_csrT(row_ptr: torch.Tensor, col_idx: torch.Tensor, num_blocks: int):
|
| 233 |
+
"""Drop-in replacement for the upstream ``build_csrT``.
|
| 234 |
+
|
| 235 |
+
Routes to the pure-GPU builder on CUDA inputs and to the
|
| 236 |
+
CPU+numpy reference on CPU inputs (so the equivalence test exercises
|
| 237 |
+
the same numerical path the kernel sees).
|
| 238 |
+
"""
|
| 239 |
+
if row_ptr.is_cuda:
|
| 240 |
+
return build_csrT_gpu(row_ptr, col_idx, num_blocks)
|
| 241 |
+
return _build_csrT_cpu_reference(row_ptr, col_idx, num_blocks)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
# ----------------------------- csrT cache --------------------------------
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# Two-level cache.
|
| 248 |
+
#
|
| 249 |
+
# - _CSRT_CACHE_FAST is keyed on (row_ptr.data_ptr(), col_idx.data_ptr(),
|
| 250 |
+
# shapes, device, num_blocks). When the public api caches the CSR tensors,
|
| 251 |
+
# the data_ptrs are stable across calls and this is an O(1) Python dict probe — no
|
| 252 |
+
# CUDA sync, no D2H copy.
|
| 253 |
+
#
|
| 254 |
+
# - _CSRT_CACHE_CONTENT is keyed on (num_blocks, shape, first-row
|
| 255 |
+
# content). Used as a fallback when the fast key misses (e.g. a user
|
| 256 |
+
# who builds CSRs fresh each call). The first-row content read costs
|
| 257 |
+
# one (num_blocks+1) + first-row-nnz int32 D2H sync — still vastly
|
| 258 |
+
# cheaper than the full CPU+numpy round-trip on the build side.
|
| 259 |
+
#
|
| 260 |
+
# On a content-cache hit we also promote into the fast cache so the next
|
| 261 |
+
# call with the same data_ptr is free.
|
| 262 |
+
_CSRT_CACHE_FAST: "OrderedDict[tuple, tuple[torch.Tensor, torch.Tensor]]" = OrderedDict()
|
| 263 |
+
_CSRT_CACHE_CONTENT: "OrderedDict[tuple, tuple[torch.Tensor, torch.Tensor]]" = OrderedDict()
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _fast_key(row_ptr: torch.Tensor, col_idx: torch.Tensor, num_blocks: int) -> tuple:
|
| 267 |
+
return (
|
| 268 |
+
row_ptr.data_ptr(),
|
| 269 |
+
col_idx.data_ptr(),
|
| 270 |
+
num_blocks,
|
| 271 |
+
tuple(row_ptr.shape),
|
| 272 |
+
tuple(col_idx.shape),
|
| 273 |
+
row_ptr.device.type,
|
| 274 |
+
getattr(row_ptr.device, "index", None),
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def _content_key(row_ptr: torch.Tensor, col_idx: torch.Tensor, num_blocks: int) -> tuple:
|
| 279 |
+
"""Content-addressed key. Reads the (0, 0) head's row to cover the
|
| 280 |
+
broadcast-CSR case (the only one the api emits today).
|
| 281 |
+
|
| 282 |
+
Invalidation rule
|
| 283 |
+
-----------------
|
| 284 |
+
A new entry is created whenever any of the following change:
|
| 285 |
+
- num_blocks
|
| 286 |
+
- row_ptr.shape / col_idx.shape
|
| 287 |
+
- device type / index
|
| 288 |
+
- the (0, 0) head's row_ptr / col_idx values
|
| 289 |
+
|
| 290 |
+
Non-broadcast inputs that share the (0, 0) row but differ on other
|
| 291 |
+
(B, H) entries would alias under this key. The api never produces
|
| 292 |
+
such inputs, but ``launch_attn_bwd`` users with hand-built CSRs
|
| 293 |
+
should call ``csrT_cache_clear()`` between distinct non-broadcast
|
| 294 |
+
patterns.
|
| 295 |
+
"""
|
| 296 |
+
rp0 = tuple(row_ptr[0, 0].to(torch.int64).tolist())
|
| 297 |
+
ci0 = tuple(col_idx[0, 0].to(torch.int64).tolist())
|
| 298 |
+
return (
|
| 299 |
+
num_blocks,
|
| 300 |
+
tuple(row_ptr.shape),
|
| 301 |
+
tuple(col_idx.shape),
|
| 302 |
+
row_ptr.device.type,
|
| 303 |
+
getattr(row_ptr.device, "index", None),
|
| 304 |
+
rp0,
|
| 305 |
+
ci0,
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def _bound(cache: "OrderedDict") -> None:
|
| 310 |
+
while len(cache) > _CSRT_CACHE_MAX:
|
| 311 |
+
cache.popitem(last=False)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def build_csrT_cached(row_ptr: torch.Tensor, col_idx: torch.Tensor, num_blocks: int):
|
| 315 |
+
"""Return a (rp_T, ci_T) pair for the given CSR, hitting the LRU cache when possible.
|
| 316 |
+
|
| 317 |
+
Thread-safety: this is not thread-safe across CUDA streams. Callers
|
| 318 |
+
that share the same CSR across streams should manually clone the
|
| 319 |
+
returned tensors. In practice the autograd backward is called on the
|
| 320 |
+
same stream as the forward that produced the CSR, so this is fine.
|
| 321 |
+
"""
|
| 322 |
+
fk = _fast_key(row_ptr, col_idx, num_blocks)
|
| 323 |
+
hit = _CSRT_CACHE_FAST.get(fk)
|
| 324 |
+
if hit is not None:
|
| 325 |
+
_CSRT_CACHE_FAST.move_to_end(fk)
|
| 326 |
+
return hit
|
| 327 |
+
ck = _content_key(row_ptr, col_idx, num_blocks)
|
| 328 |
+
hit = _CSRT_CACHE_CONTENT.get(ck)
|
| 329 |
+
if hit is not None:
|
| 330 |
+
_CSRT_CACHE_CONTENT.move_to_end(ck)
|
| 331 |
+
_CSRT_CACHE_FAST[fk] = hit
|
| 332 |
+
_bound(_CSRT_CACHE_FAST)
|
| 333 |
+
return hit
|
| 334 |
+
rp_T, ci_T = build_csrT(row_ptr, col_idx, num_blocks)
|
| 335 |
+
_CSRT_CACHE_FAST[fk] = (rp_T, ci_T)
|
| 336 |
+
_CSRT_CACHE_CONTENT[ck] = (rp_T, ci_T)
|
| 337 |
+
_bound(_CSRT_CACHE_FAST)
|
| 338 |
+
_bound(_CSRT_CACHE_CONTENT)
|
| 339 |
+
return rp_T, ci_T
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def csrT_cache_clear() -> None:
|
| 343 |
+
"""Drop all cached csrT entries. Useful for test isolation."""
|
| 344 |
+
_CSRT_CACHE_FAST.clear()
|
| 345 |
+
_CSRT_CACHE_CONTENT.clear()
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def csrT_cache_info() -> dict:
|
| 349 |
+
"""Return basic cache stats. ``size`` is the number of *distinct*
|
| 350 |
+
csrT patterns currently cached (the content-keyed dict)."""
|
| 351 |
+
return {
|
| 352 |
+
"size": len(_CSRT_CACHE_CONTENT),
|
| 353 |
+
"fast_size": len(_CSRT_CACHE_FAST),
|
| 354 |
+
"max": _CSRT_CACHE_MAX,
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
# ----------------------------- Triton kernels ----------------------------
|
| 359 |
+
# These are kept local so the extracted package is self-contained.
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
@triton.jit
|
| 363 |
+
def _hydra_bwd_dq_jit(
|
| 364 |
+
Q, K, V, LSE,
|
| 365 |
+
dO,
|
| 366 |
+
Di_in,
|
| 367 |
+
dQ,
|
| 368 |
+
RowPtr, ColIdx, SeqLens,
|
| 369 |
+
stride_cih,
|
| 370 |
+
T_MAX: tl.constexpr,
|
| 371 |
+
NUM_HEADS: tl.constexpr,
|
| 372 |
+
NUM_KV_HEADS: tl.constexpr,
|
| 373 |
+
SCALE: tl.constexpr,
|
| 374 |
+
BS: tl.constexpr,
|
| 375 |
+
D: tl.constexpr,
|
| 376 |
+
WINDOW: tl.constexpr,
|
| 377 |
+
CONTIGUOUS_OFFDIAG: tl.constexpr = 0,
|
| 378 |
+
):
|
| 379 |
+
stride_h: tl.constexpr = T_MAX * D
|
| 380 |
+
stride_t: tl.constexpr = D
|
| 381 |
+
stride_rph: tl.constexpr = (T_MAX // BS) + 1
|
| 382 |
+
stride_lh: tl.constexpr = T_MAX
|
| 383 |
+
NUM_Q_BLOCKS: tl.constexpr = T_MAX // BS
|
| 384 |
+
|
| 385 |
+
pid = tl.program_id(0)
|
| 386 |
+
bh_id = pid // NUM_Q_BLOCKS
|
| 387 |
+
q_block_id = pid % NUM_Q_BLOCKS
|
| 388 |
+
|
| 389 |
+
rep: tl.constexpr = NUM_HEADS // NUM_KV_HEADS
|
| 390 |
+
b_id = bh_id // NUM_HEADS
|
| 391 |
+
hq_id = bh_id % NUM_HEADS
|
| 392 |
+
hkv_id = hq_id // rep
|
| 393 |
+
kv_bh_id = b_id * NUM_KV_HEADS + hkv_id
|
| 394 |
+
|
| 395 |
+
seq_len = tl.load(SeqLens + b_id)
|
| 396 |
+
q_start = q_block_id * BS
|
| 397 |
+
if q_start >= seq_len:
|
| 398 |
+
return
|
| 399 |
+
|
| 400 |
+
offs_tok = q_start + tl.arange(0, BS)
|
| 401 |
+
offs_d = tl.arange(0, D)
|
| 402 |
+
q_mask = offs_tok < seq_len
|
| 403 |
+
|
| 404 |
+
Q_ptr = Q + bh_id * stride_h
|
| 405 |
+
q_bf16 = tl.load(Q_ptr + offs_tok[:, None] * stride_t + offs_d[None, :])
|
| 406 |
+
|
| 407 |
+
dO_ptr = dO + bh_id * stride_h
|
| 408 |
+
dO_i = tl.load(dO_ptr + offs_tok[:, None] * stride_t + offs_d[None, :])
|
| 409 |
+
|
| 410 |
+
LSE_ptr = LSE + bh_id * stride_lh
|
| 411 |
+
LSE_i = tl.load(LSE_ptr + offs_tok)
|
| 412 |
+
|
| 413 |
+
rp_base = RowPtr + bh_id * stride_rph + q_block_id
|
| 414 |
+
ci_lo = tl.load(rp_base)
|
| 415 |
+
ci_hi = tl.load(rp_base + 1)
|
| 416 |
+
|
| 417 |
+
K_ptr = K + kv_bh_id * stride_h
|
| 418 |
+
V_ptr = V + kv_bh_id * stride_h
|
| 419 |
+
CI_ptr = ColIdx + bh_id * stride_cih
|
| 420 |
+
|
| 421 |
+
offs_k_tile = tl.arange(0, BS)
|
| 422 |
+
boundary = (q_start + BS) > seq_len
|
| 423 |
+
|
| 424 |
+
Di = tl.load(Di_in + bh_id * stride_lh + offs_tok, mask=q_mask, other=0.0)
|
| 425 |
+
|
| 426 |
+
dQ_i = tl.zeros([BS, D], dtype=tl.float32)
|
| 427 |
+
|
| 428 |
+
if ci_hi > ci_lo:
|
| 429 |
+
k_start_d = q_block_id * BS
|
| 430 |
+
offs_k_d = k_start_d + offs_k_tile
|
| 431 |
+
if boundary:
|
| 432 |
+
k_mask_d = offs_k_d < seq_len
|
| 433 |
+
k_bf16_d = tl.load(K_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :], mask=k_mask_d[:, None], other=0.0)
|
| 434 |
+
v_bf16_d = tl.load(V_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :], mask=k_mask_d[:, None], other=0.0)
|
| 435 |
+
s_d = tl.dot(q_bf16, tl.trans(k_bf16_d), out_dtype=tl.float32) * SCALE
|
| 436 |
+
causal = offs_tok[:, None] >= offs_k_d[None, :]
|
| 437 |
+
allowed = causal & k_mask_d[None, :]
|
| 438 |
+
if WINDOW > 0:
|
| 439 |
+
in_window = (offs_tok[:, None] - offs_k_d[None, :]) < WINDOW
|
| 440 |
+
allowed = allowed & in_window
|
| 441 |
+
s_d = tl.where(allowed, s_d, float("-inf"))
|
| 442 |
+
else:
|
| 443 |
+
k_bf16_d = tl.load(K_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :])
|
| 444 |
+
v_bf16_d = tl.load(V_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :])
|
| 445 |
+
s_d = tl.dot(q_bf16, tl.trans(k_bf16_d), out_dtype=tl.float32) * SCALE
|
| 446 |
+
causal = offs_tok[:, None] >= offs_k_d[None, :]
|
| 447 |
+
if WINDOW > 0:
|
| 448 |
+
in_window = (offs_tok[:, None] - offs_k_d[None, :]) < WINDOW
|
| 449 |
+
s_d = tl.where(causal & in_window, s_d, float("-inf"))
|
| 450 |
+
else:
|
| 451 |
+
s_d = tl.where(causal, s_d, float("-inf"))
|
| 452 |
+
|
| 453 |
+
P_d = tl.exp(s_d - LSE_i[:, None])
|
| 454 |
+
dP_d = tl.dot(dO_i, tl.trans(v_bf16_d), out_dtype=tl.float32)
|
| 455 |
+
dS_d = (P_d * (dP_d - Di[:, None])) * SCALE
|
| 456 |
+
dQ_i += tl.dot(dS_d.to(tl.bfloat16), k_bf16_d, out_dtype=tl.float32)
|
| 457 |
+
|
| 458 |
+
# See kernel_fwd: when CONTIGUOUS_OFFDIAG, hoist the CSR load.
|
| 459 |
+
if CONTIGUOUS_OFFDIAG:
|
| 460 |
+
k_block_id_start = tl.load(CI_ptr + ci_lo)
|
| 461 |
+
for ci in range(ci_lo, ci_hi - 1):
|
| 462 |
+
k_block_id = k_block_id_start + (ci - ci_lo)
|
| 463 |
+
k_start = k_block_id * BS
|
| 464 |
+
offs_k = k_start + offs_k_tile
|
| 465 |
+
k_bf16 = tl.load(K_ptr + offs_k[:, None] * stride_t + offs_d[None, :])
|
| 466 |
+
v_bf16 = tl.load(V_ptr + offs_k[:, None] * stride_t + offs_d[None, :])
|
| 467 |
+
s = tl.dot(q_bf16, tl.trans(k_bf16), out_dtype=tl.float32) * SCALE
|
| 468 |
+
if WINDOW > 0:
|
| 469 |
+
in_window = (offs_tok[:, None] - offs_k[None, :]) < WINDOW
|
| 470 |
+
s = tl.where(in_window, s, float("-inf"))
|
| 471 |
+
P = tl.exp(s - LSE_i[:, None])
|
| 472 |
+
dP = tl.dot(dO_i, tl.trans(v_bf16), out_dtype=tl.float32)
|
| 473 |
+
dS = (P * (dP - Di[:, None])) * SCALE
|
| 474 |
+
dQ_i += tl.dot(dS.to(tl.bfloat16), k_bf16, out_dtype=tl.float32)
|
| 475 |
+
else:
|
| 476 |
+
for ci in range(ci_lo, ci_hi - 1):
|
| 477 |
+
k_block_id = tl.load(CI_ptr + ci)
|
| 478 |
+
k_start = k_block_id * BS
|
| 479 |
+
offs_k = k_start + offs_k_tile
|
| 480 |
+
k_bf16 = tl.load(K_ptr + offs_k[:, None] * stride_t + offs_d[None, :])
|
| 481 |
+
v_bf16 = tl.load(V_ptr + offs_k[:, None] * stride_t + offs_d[None, :])
|
| 482 |
+
s = tl.dot(q_bf16, tl.trans(k_bf16), out_dtype=tl.float32) * SCALE
|
| 483 |
+
if WINDOW > 0:
|
| 484 |
+
in_window = (offs_tok[:, None] - offs_k[None, :]) < WINDOW
|
| 485 |
+
s = tl.where(in_window, s, float("-inf"))
|
| 486 |
+
P = tl.exp(s - LSE_i[:, None])
|
| 487 |
+
dP = tl.dot(dO_i, tl.trans(v_bf16), out_dtype=tl.float32)
|
| 488 |
+
dS = (P * (dP - Di[:, None])) * SCALE
|
| 489 |
+
dQ_i += tl.dot(dS.to(tl.bfloat16), k_bf16, out_dtype=tl.float32)
|
| 490 |
+
|
| 491 |
+
dQ_ptr = dQ + bh_id * stride_h
|
| 492 |
+
tl.store(dQ_ptr + offs_tok[:, None] * stride_t + offs_d[None, :], dQ_i.to(tl.bfloat16), mask=q_mask[:, None])
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
@triton.jit
|
| 496 |
+
def _hydra_bwd_dkv_jit(
|
| 497 |
+
Q, K, V, LSE,
|
| 498 |
+
dO,
|
| 499 |
+
Di_in,
|
| 500 |
+
dK, dV,
|
| 501 |
+
RowPtrT, ColIdxT, SeqLens,
|
| 502 |
+
stride_cith,
|
| 503 |
+
T_MAX: tl.constexpr,
|
| 504 |
+
NUM_HEADS: tl.constexpr,
|
| 505 |
+
NUM_KV_HEADS: tl.constexpr,
|
| 506 |
+
SCALE: tl.constexpr,
|
| 507 |
+
BS: tl.constexpr,
|
| 508 |
+
D: tl.constexpr,
|
| 509 |
+
WINDOW: tl.constexpr,
|
| 510 |
+
CONTIGUOUS_OFFDIAG: tl.constexpr = 0,
|
| 511 |
+
):
|
| 512 |
+
stride_h: tl.constexpr = T_MAX * D
|
| 513 |
+
stride_t: tl.constexpr = D
|
| 514 |
+
stride_rpth: tl.constexpr = (T_MAX // BS) + 1
|
| 515 |
+
stride_lh: tl.constexpr = T_MAX
|
| 516 |
+
NUM_K_BLOCKS: tl.constexpr = T_MAX // BS
|
| 517 |
+
|
| 518 |
+
pid = tl.program_id(0)
|
| 519 |
+
kv_bh_id = pid // NUM_K_BLOCKS
|
| 520 |
+
k_block_id = pid % NUM_K_BLOCKS
|
| 521 |
+
|
| 522 |
+
rep: tl.constexpr = NUM_HEADS // NUM_KV_HEADS
|
| 523 |
+
b_id = kv_bh_id // NUM_KV_HEADS
|
| 524 |
+
hkv_id = kv_bh_id % NUM_KV_HEADS
|
| 525 |
+
q_head_start = hkv_id * rep
|
| 526 |
+
|
| 527 |
+
seq_len = tl.load(SeqLens + b_id)
|
| 528 |
+
k_start = k_block_id * BS
|
| 529 |
+
if k_start >= seq_len:
|
| 530 |
+
return
|
| 531 |
+
|
| 532 |
+
offs_d = tl.arange(0, D)
|
| 533 |
+
offs_tok_k = k_start + tl.arange(0, BS)
|
| 534 |
+
k_mask = offs_tok_k < seq_len
|
| 535 |
+
|
| 536 |
+
K_ptr = K + kv_bh_id * stride_h
|
| 537 |
+
V_ptr = V + kv_bh_id * stride_h
|
| 538 |
+
|
| 539 |
+
k_bf16 = tl.load(K_ptr + offs_tok_k[:, None] * stride_t + offs_d[None, :], mask=k_mask[:, None], other=0.0)
|
| 540 |
+
v_bf16 = tl.load(V_ptr + offs_tok_k[:, None] * stride_t + offs_d[None, :], mask=k_mask[:, None], other=0.0)
|
| 541 |
+
|
| 542 |
+
dK_acc = tl.zeros([BS, D], dtype=tl.float32)
|
| 543 |
+
dV_acc = tl.zeros([BS, D], dtype=tl.float32)
|
| 544 |
+
|
| 545 |
+
for rep_idx in range(rep):
|
| 546 |
+
q_head_id = q_head_start + rep_idx
|
| 547 |
+
q_bh_id = b_id * NUM_HEADS + q_head_id
|
| 548 |
+
|
| 549 |
+
Q_ptr = Q + q_bh_id * stride_h
|
| 550 |
+
dO_ptr = dO + q_bh_id * stride_h
|
| 551 |
+
LSE_ptr = LSE + q_bh_id * stride_lh
|
| 552 |
+
Di_ptr = Di_in + q_bh_id * stride_lh
|
| 553 |
+
|
| 554 |
+
q_start_diag = k_block_id * BS
|
| 555 |
+
offs_tok_q = q_start_diag + tl.arange(0, BS)
|
| 556 |
+
q_mask_diag = offs_tok_q < seq_len
|
| 557 |
+
q_bf16_d = tl.load(Q_ptr + offs_tok_q[:, None] * stride_t + offs_d[None, :], mask=q_mask_diag[:, None], other=0.0)
|
| 558 |
+
dO_d = tl.load(dO_ptr + offs_tok_q[:, None] * stride_t + offs_d[None, :], mask=q_mask_diag[:, None], other=0.0)
|
| 559 |
+
LSE_d = tl.load(LSE_ptr + offs_tok_q, mask=q_mask_diag, other=0.0)
|
| 560 |
+
Di_d = tl.load(Di_ptr + offs_tok_q, mask=q_mask_diag, other=0.0)
|
| 561 |
+
|
| 562 |
+
s_d = tl.dot(q_bf16_d, tl.trans(k_bf16), out_dtype=tl.float32) * SCALE
|
| 563 |
+
causal_d = offs_tok_q[:, None] >= offs_tok_k[None, :]
|
| 564 |
+
allowed_d = causal_d & k_mask[None, :] & q_mask_diag[:, None]
|
| 565 |
+
if WINDOW > 0:
|
| 566 |
+
in_window_d = (offs_tok_q[:, None] - offs_tok_k[None, :]) < WINDOW
|
| 567 |
+
allowed_d = allowed_d & in_window_d
|
| 568 |
+
s_d = tl.where(allowed_d, s_d, float("-inf"))
|
| 569 |
+
P_d = tl.exp(s_d - LSE_d[:, None])
|
| 570 |
+
dP_d = tl.dot(dO_d, tl.trans(v_bf16), out_dtype=tl.float32)
|
| 571 |
+
dS_d = (P_d * (dP_d - Di_d[:, None])) * SCALE
|
| 572 |
+
|
| 573 |
+
dK_acc += tl.dot(tl.trans(dS_d.to(tl.bfloat16)), q_bf16_d, out_dtype=tl.float32)
|
| 574 |
+
dV_acc += tl.dot(tl.trans(P_d.to(tl.bfloat16)), dO_d, out_dtype=tl.float32)
|
| 575 |
+
|
| 576 |
+
rpt_base = RowPtrT + q_bh_id * stride_rpth + k_block_id
|
| 577 |
+
cit_lo = tl.load(rpt_base)
|
| 578 |
+
cit_hi = tl.load(rpt_base + 1)
|
| 579 |
+
CIT_ptr = ColIdxT + q_bh_id * stride_cith
|
| 580 |
+
|
| 581 |
+
# See kernel_fwd: when CONTIGUOUS_OFFDIAG, hoist the transposed-CSR
|
| 582 |
+
# load. For dense, K-block k attends Q-blocks k+1..num_q_blocks-1
|
| 583 |
+
# (contiguous); for sliding-window, k attends a contiguous Q-range
|
| 584 |
+
# too. The transposed-CSR builder (build_csrT) preserves ascending
|
| 585 |
+
# q_block_id order within each K-row, so the run is contiguous.
|
| 586 |
+
if CONTIGUOUS_OFFDIAG:
|
| 587 |
+
q_block_id_start_o = tl.load(CIT_ptr + cit_lo)
|
| 588 |
+
for ci in range(cit_lo, cit_hi):
|
| 589 |
+
q_block_id = q_block_id_start_o + (ci - cit_lo)
|
| 590 |
+
q_start = q_block_id * BS
|
| 591 |
+
offs_tok_q2 = q_start + tl.arange(0, BS)
|
| 592 |
+
q_mask2 = offs_tok_q2 < seq_len
|
| 593 |
+
q_bf16_o = tl.load(Q_ptr + offs_tok_q2[:, None] * stride_t + offs_d[None, :], mask=q_mask2[:, None], other=0.0)
|
| 594 |
+
dO_o = tl.load(dO_ptr + offs_tok_q2[:, None] * stride_t + offs_d[None, :], mask=q_mask2[:, None], other=0.0)
|
| 595 |
+
LSE_o = tl.load(LSE_ptr + offs_tok_q2, mask=q_mask2, other=0.0)
|
| 596 |
+
Di_o = tl.load(Di_ptr + offs_tok_q2, mask=q_mask2, other=0.0)
|
| 597 |
+
|
| 598 |
+
s_o = tl.dot(q_bf16_o, tl.trans(k_bf16), out_dtype=tl.float32) * SCALE
|
| 599 |
+
allowed_o = q_mask2[:, None] & k_mask[None, :]
|
| 600 |
+
if WINDOW > 0:
|
| 601 |
+
in_window_o = (offs_tok_q2[:, None] - offs_tok_k[None, :]) < WINDOW
|
| 602 |
+
allowed_o = allowed_o & in_window_o
|
| 603 |
+
s_o = tl.where(allowed_o, s_o, float("-inf"))
|
| 604 |
+
P_o = tl.exp(s_o - LSE_o[:, None])
|
| 605 |
+
dP_o = tl.dot(dO_o, tl.trans(v_bf16), out_dtype=tl.float32)
|
| 606 |
+
dS_o = (P_o * (dP_o - Di_o[:, None])) * SCALE
|
| 607 |
+
|
| 608 |
+
dK_acc += tl.dot(tl.trans(dS_o.to(tl.bfloat16)), q_bf16_o, out_dtype=tl.float32)
|
| 609 |
+
dV_acc += tl.dot(tl.trans(P_o.to(tl.bfloat16)), dO_o, out_dtype=tl.float32)
|
| 610 |
+
else:
|
| 611 |
+
for ci in range(cit_lo, cit_hi):
|
| 612 |
+
q_block_id = tl.load(CIT_ptr + ci)
|
| 613 |
+
q_start = q_block_id * BS
|
| 614 |
+
offs_tok_q2 = q_start + tl.arange(0, BS)
|
| 615 |
+
q_mask2 = offs_tok_q2 < seq_len
|
| 616 |
+
q_bf16_o = tl.load(Q_ptr + offs_tok_q2[:, None] * stride_t + offs_d[None, :], mask=q_mask2[:, None], other=0.0)
|
| 617 |
+
dO_o = tl.load(dO_ptr + offs_tok_q2[:, None] * stride_t + offs_d[None, :], mask=q_mask2[:, None], other=0.0)
|
| 618 |
+
LSE_o = tl.load(LSE_ptr + offs_tok_q2, mask=q_mask2, other=0.0)
|
| 619 |
+
Di_o = tl.load(Di_ptr + offs_tok_q2, mask=q_mask2, other=0.0)
|
| 620 |
+
|
| 621 |
+
s_o = tl.dot(q_bf16_o, tl.trans(k_bf16), out_dtype=tl.float32) * SCALE
|
| 622 |
+
allowed_o = q_mask2[:, None] & k_mask[None, :]
|
| 623 |
+
if WINDOW > 0:
|
| 624 |
+
in_window_o = (offs_tok_q2[:, None] - offs_tok_k[None, :]) < WINDOW
|
| 625 |
+
allowed_o = allowed_o & in_window_o
|
| 626 |
+
s_o = tl.where(allowed_o, s_o, float("-inf"))
|
| 627 |
+
P_o = tl.exp(s_o - LSE_o[:, None])
|
| 628 |
+
dP_o = tl.dot(dO_o, tl.trans(v_bf16), out_dtype=tl.float32)
|
| 629 |
+
dS_o = (P_o * (dP_o - Di_o[:, None])) * SCALE
|
| 630 |
+
|
| 631 |
+
dK_acc += tl.dot(tl.trans(dS_o.to(tl.bfloat16)), q_bf16_o, out_dtype=tl.float32)
|
| 632 |
+
dV_acc += tl.dot(tl.trans(P_o.to(tl.bfloat16)), dO_o, out_dtype=tl.float32)
|
| 633 |
+
|
| 634 |
+
dK_ptr = dK + kv_bh_id * stride_h + offs_tok_k[:, None] * stride_t + offs_d[None, :]
|
| 635 |
+
dV_ptr = dV + kv_bh_id * stride_h + offs_tok_k[:, None] * stride_t + offs_d[None, :]
|
| 636 |
+
tl.store(dK_ptr, dK_acc.to(tl.bfloat16), mask=k_mask[:, None])
|
| 637 |
+
tl.store(dV_ptr, dV_acc.to(tl.bfloat16), mask=k_mask[:, None])
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
_hydra_bwd_dq_kernel = _kernel_decorator(_hydra_bwd_dq_jit)
|
| 641 |
+
_hydra_bwd_dkv_kernel = _kernel_decorator(_hydra_bwd_dkv_jit)
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
# ----------------------------- launcher ----------------------------------
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
def launch_attn_bwd(
|
| 648 |
+
q: torch.Tensor,
|
| 649 |
+
k: torch.Tensor,
|
| 650 |
+
v: torch.Tensor,
|
| 651 |
+
o: torch.Tensor,
|
| 652 |
+
do: torch.Tensor,
|
| 653 |
+
lse: torch.Tensor,
|
| 654 |
+
row_ptr: torch.Tensor,
|
| 655 |
+
col_idx: torch.Tensor,
|
| 656 |
+
seq_lens: torch.Tensor,
|
| 657 |
+
row_ptr_T: torch.Tensor | None = None,
|
| 658 |
+
col_idx_T: torch.Tensor | None = None,
|
| 659 |
+
window: int = 0,
|
| 660 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 661 |
+
"""Launch dQ then dK/dV kernels.
|
| 662 |
+
|
| 663 |
+
Identical signature / semantics to the upstream ``launch_attn_bwd``.
|
| 664 |
+
If ``row_ptr_T`` / ``col_idx_T`` are not provided, builds them via
|
| 665 |
+
the cached pure-GPU builder so repeated calls with the same pattern
|
| 666 |
+
pay a single one-time build cost.
|
| 667 |
+
"""
|
| 668 |
+
batch_size, num_heads, t_max, head_dim = q.shape
|
| 669 |
+
num_kv_heads = k.shape[1]
|
| 670 |
+
if t_max % BLOCK_SIZE != 0:
|
| 671 |
+
raise ValueError(f"T ({t_max}) must be a multiple of BLOCK_SIZE ({BLOCK_SIZE})")
|
| 672 |
+
if head_dim != HEAD_DIM:
|
| 673 |
+
raise ValueError(f"head_dim ({head_dim}) must equal HEAD_DIM ({HEAD_DIM})")
|
| 674 |
+
if num_heads % num_kv_heads != 0:
|
| 675 |
+
raise ValueError(f"num_heads ({num_heads}) must be divisible by num_kv_heads ({num_kv_heads})")
|
| 676 |
+
|
| 677 |
+
batch_heads_q = batch_size * num_heads
|
| 678 |
+
batch_heads_kv = batch_size * num_kv_heads
|
| 679 |
+
num_q_blocks = t_max // BLOCK_SIZE
|
| 680 |
+
|
| 681 |
+
dq = torch.empty_like(q)
|
| 682 |
+
dk = torch.empty_like(k)
|
| 683 |
+
dv = torch.empty_like(v)
|
| 684 |
+
|
| 685 |
+
# delta[b, h, t] = sum_d O[b, h, t, d] * dO[b, h, t, d]
|
| 686 |
+
# Single-pass Triton kernel avoids the ~3 fp32 transients (do.float(),
|
| 687 |
+
# o.float(), their product) the host pipeline allocates — ~256 MB
|
| 688 |
+
# steady-state HBM saved at Qwen3-8B T=8192. Bounded reorder error
|
| 689 |
+
# vs torch's .sum(-1) is ~1.5e-5, three orders below bf16 quantum.
|
| 690 |
+
delta = launch_compute_delta(o, do)
|
| 691 |
+
delta_2d = delta.view(batch_heads_q, t_max)
|
| 692 |
+
lse_2d = lse.view(batch_heads_q, t_max)
|
| 693 |
+
|
| 694 |
+
if row_ptr_T is None or col_idx_T is None:
|
| 695 |
+
row_ptr_T, col_idx_T = build_csrT_cached(row_ptr, col_idx, num_q_blocks)
|
| 696 |
+
|
| 697 |
+
stride_cih = col_idx.shape[2] if col_idx.ndim == 3 else col_idx.shape[1]
|
| 698 |
+
stride_cith = col_idx_T.shape[2] if col_idx_T.ndim == 3 else col_idx_T.shape[1]
|
| 699 |
+
scale = 1.0 / math.sqrt(head_dim)
|
| 700 |
+
|
| 701 |
+
grid_q = (num_q_blocks * batch_heads_q,)
|
| 702 |
+
grid_kv = (num_q_blocks * batch_heads_kv,)
|
| 703 |
+
|
| 704 |
+
common_kwargs = dict(
|
| 705 |
+
T_MAX=t_max,
|
| 706 |
+
NUM_HEADS=num_heads,
|
| 707 |
+
NUM_KV_HEADS=num_kv_heads,
|
| 708 |
+
SCALE=scale,
|
| 709 |
+
BS=BLOCK_SIZE,
|
| 710 |
+
D=head_dim,
|
| 711 |
+
WINDOW=int(window),
|
| 712 |
+
# Both build_dense_causal_csr and build_sliding_window_csr emit
|
| 713 |
+
# off-diag K-block IDs as a contiguous run; the transposed CSR
|
| 714 |
+
# builder preserves ascending q_block_id within each K-row, which
|
| 715 |
+
# is likewise contiguous for these patterns. Lets the kernels
|
| 716 |
+
# hoist the per-iteration CSR scalar GMEM load.
|
| 717 |
+
CONTIGUOUS_OFFDIAG=1,
|
| 718 |
+
)
|
| 719 |
+
if _DISABLE_AUTOTUNE:
|
| 720 |
+
common_kwargs["num_warps"] = _BWD_NUM_WARPS
|
| 721 |
+
common_kwargs["num_stages"] = _BWD_NUM_STAGES
|
| 722 |
+
|
| 723 |
+
_hydra_bwd_dq_kernel[grid_q](
|
| 724 |
+
q, k, v, lse_2d,
|
| 725 |
+
do,
|
| 726 |
+
delta_2d,
|
| 727 |
+
dq,
|
| 728 |
+
row_ptr, col_idx, seq_lens,
|
| 729 |
+
stride_cih,
|
| 730 |
+
**common_kwargs,
|
| 731 |
+
)
|
| 732 |
+
|
| 733 |
+
_hydra_bwd_dkv_kernel[grid_kv](
|
| 734 |
+
q, k, v, lse_2d,
|
| 735 |
+
do,
|
| 736 |
+
delta_2d,
|
| 737 |
+
dk, dv,
|
| 738 |
+
row_ptr_T, col_idx_T, seq_lens,
|
| 739 |
+
stride_cith,
|
| 740 |
+
**common_kwargs,
|
| 741 |
+
)
|
| 742 |
+
|
| 743 |
+
return dq, dk, dv
|
torch-ext/hydra/kernel_decode.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Forward Triton kernel for the decode step: Q.seq_len == 1.
|
| 2 |
+
|
| 3 |
+
Specialization
|
| 4 |
+
--------------
|
| 5 |
+
Generation hot path: one new Q token attends to a full KV cache of length
|
| 6 |
+
T_kv. The prefill kernel in ``kernel_fwd.py`` requires T % BLOCK_SIZE == 0
|
| 7 |
+
and pays for Q-blocking + causal CSR iteration that is meaningless when
|
| 8 |
+
there is exactly one query row. This kernel collapses to:
|
| 9 |
+
|
| 10 |
+
program grid = (B * H_q,) # one program per query head
|
| 11 |
+
Q broadcast-replicated to a [MM, D] tile (MM=16, the tcore M minimum)
|
| 12 |
+
K/V streamed in [BK, D] tiles across the visible K range
|
| 13 |
+
|
| 14 |
+
Online softmax with a single Q row reduces to scalar (m, l) and a [1, D]
|
| 15 |
+
accumulator (we lift it to [MM, D] with broadcast so Triton's tl.dot —
|
| 16 |
+
which requires M >= 16 on Blackwell — sees a tensor-core-sized matmul).
|
| 17 |
+
Only the first row of the [MM, D] output is actually distinct; we keep
|
| 18 |
+
the broadcast cost (~16× duplicate work) because the V matmul is the
|
| 19 |
+
heavy cost and dominates regardless. The alternative — manually emitting
|
| 20 |
+
fma loops — would not use tensor cores and would be far slower.
|
| 21 |
+
|
| 22 |
+
No causal mask is needed because the single Q token is at absolute
|
| 23 |
+
position T_kv - 1 and every visible K token is <= that position.
|
| 24 |
+
|
| 25 |
+
Sliding window
|
| 26 |
+
--------------
|
| 27 |
+
When ``WINDOW > 0`` and ``WINDOW < T_kv``, K is restricted to the last
|
| 28 |
+
WINDOW positions: ``k_start = T_kv - WINDOW``. The launcher computes
|
| 29 |
+
``k_start`` and the number of K tiles; the kernel iterates that range
|
| 30 |
+
unconditionally and masks the tail tile where ``offs_k >= T_kv``.
|
| 31 |
+
|
| 32 |
+
GQA
|
| 33 |
+
---
|
| 34 |
+
Each H_q head reads the same K/V slab indexed by
|
| 35 |
+
``hkv = hq // (NUM_HEADS // NUM_KV_HEADS)``.
|
| 36 |
+
|
| 37 |
+
Precision
|
| 38 |
+
---------
|
| 39 |
+
bf16 inputs, fp32 accumulator, bf16 output. The kernel casts ``p`` to bf16
|
| 40 |
+
once per K-tile before the ``p @ V`` matmul. No hi/lo precision split:
|
| 41 |
+
without a causal mask there is no single "diagonal" block where the cast
|
| 42 |
+
loss is concentrated; for T_kv up to ~32K this stays well inside bf16's
|
| 43 |
+
useful range and the test below confirms a max abs diff < 3e-2 against
|
| 44 |
+
SDPA.
|
| 45 |
+
"""
|
| 46 |
+
from __future__ import annotations
|
| 47 |
+
|
| 48 |
+
import math
|
| 49 |
+
import os
|
| 50 |
+
|
| 51 |
+
import torch
|
| 52 |
+
import triton
|
| 53 |
+
import triton.language as tl
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# K-tile size for the decode kernel. Independent of the prefill kernel's
|
| 57 |
+
# BLOCK_SIZE because the decode kernel never reads CSR; it just streams.
|
| 58 |
+
BLOCK_K = int(os.environ.get("HYDRA_DECODE_BLOCK_K", "64"))
|
| 59 |
+
HEAD_DIM = int(os.environ.get("HYDRA_HEAD_DIM", "128"))
|
| 60 |
+
|
| 61 |
+
_DEC_NUM_WARPS = int(os.environ.get("HYDRA_DECODE_NUM_WARPS", "4"))
|
| 62 |
+
_DEC_NUM_STAGES = int(os.environ.get("HYDRA_DECODE_NUM_STAGES", "2"))
|
| 63 |
+
_DISABLE_AUTOTUNE = int(os.environ.get("HYDRA_DISABLE_AUTOTUNE", "0"))
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _autotune_configs() -> list[triton.Config]:
|
| 67 |
+
"""Decode-friendly configs. Q is a single row so warp pressure is low;
|
| 68 |
+
favour stages=2-3 to overlap K/V loads with the small matmuls.
|
| 69 |
+
"""
|
| 70 |
+
configs: list[triton.Config] = []
|
| 71 |
+
for num_warps in (2, 4, 8):
|
| 72 |
+
for num_stages in (2, 3, 4):
|
| 73 |
+
configs.append(triton.Config({}, num_warps=num_warps, num_stages=num_stages))
|
| 74 |
+
return configs
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _fixed_config() -> triton.Config:
|
| 78 |
+
return triton.Config({}, num_warps=_DEC_NUM_WARPS, num_stages=_DEC_NUM_STAGES)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _kernel_decorator(jit_kernel):
|
| 82 |
+
if _DISABLE_AUTOTUNE:
|
| 83 |
+
return jit_kernel
|
| 84 |
+
return triton.autotune(
|
| 85 |
+
configs=_autotune_configs(),
|
| 86 |
+
# T_KV is a runtime arg (not constexpr) and is intentionally NOT in the
|
| 87 |
+
# key — every generation token changes T_kv by 1, and including it
|
| 88 |
+
# forced per-token recompiles (~5-10s each, 200x slowdown observed).
|
| 89 |
+
key=["D", "NUM_HEADS", "NUM_KV_HEADS"],
|
| 90 |
+
)(jit_kernel)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@triton.jit
|
| 94 |
+
def _hydra_decode_jit(
|
| 95 |
+
Q, # (B, H_q, 1, D) bf16
|
| 96 |
+
K, V, # (B, H_kv, T_KV, D) bf16
|
| 97 |
+
O, # (B, H_q, 1, D) bf16
|
| 98 |
+
LSE, # (B, H_q, 1) fp32 (kept for parity / future use)
|
| 99 |
+
T_KV, # runtime int — used only as the mask bound; NOT constexpr to avoid per-T_kv recompiles during generation
|
| 100 |
+
stride_kvbh_kv, # runtime stride for (B*Hkv) axis of K/V in elements (lets StaticCache pass non-T_KV*D strides)
|
| 101 |
+
stride_t_kv, # runtime stride for the T axis of K/V in elements
|
| 102 |
+
NUM_HEADS: tl.constexpr,
|
| 103 |
+
NUM_KV_HEADS: tl.constexpr,
|
| 104 |
+
SCALE: tl.constexpr,
|
| 105 |
+
BK: tl.constexpr,
|
| 106 |
+
D: tl.constexpr,
|
| 107 |
+
MM: tl.constexpr, # Q-replication dim (16 — tcore M-minimum).
|
| 108 |
+
K_START: tl.constexpr, # leftmost token attended (>=0). Sliding-window left edge.
|
| 109 |
+
NUM_K_TILES: tl.constexpr,
|
| 110 |
+
):
|
| 111 |
+
# Q strides: per-(B, H_q) slab has 1*D = D elements (Q is always contig (B,Hq,1,D)).
|
| 112 |
+
# K/V strides: passed in as runtime args so StaticCache (B, Hkv, T_max, D) works
|
| 113 |
+
# — kernel previously hardcoded stride_kvbh = T_KV * D which is wrong for non-T_KV strides.
|
| 114 |
+
stride_qbh: tl.constexpr = D
|
| 115 |
+
stride_kvbh = stride_kvbh_kv
|
| 116 |
+
stride_t = stride_t_kv
|
| 117 |
+
|
| 118 |
+
pid = tl.program_id(0)
|
| 119 |
+
rep: tl.constexpr = NUM_HEADS // NUM_KV_HEADS
|
| 120 |
+
b_id = pid // NUM_HEADS
|
| 121 |
+
hq_id = pid % NUM_HEADS
|
| 122 |
+
hkv_id = hq_id // rep
|
| 123 |
+
|
| 124 |
+
q_bh = b_id * NUM_HEADS + hq_id
|
| 125 |
+
kv_bh = b_id * NUM_KV_HEADS + hkv_id
|
| 126 |
+
|
| 127 |
+
offs_d = tl.arange(0, D)
|
| 128 |
+
offs_mm = tl.arange(0, MM)
|
| 129 |
+
offs_k_in_tile = tl.arange(0, BK)
|
| 130 |
+
|
| 131 |
+
# Fold SCALE * log2(e) into Q at load so the per-K-tile score does not
|
| 132 |
+
# pay one mul per element.
|
| 133 |
+
LOG2E: tl.constexpr = 1.4426950408889634
|
| 134 |
+
SCALE_2: tl.constexpr = SCALE * LOG2E
|
| 135 |
+
Q_ptr = Q + q_bh * stride_qbh
|
| 136 |
+
|
| 137 |
+
# Tensor cores need M >= 16 in tl.dot. Q has only one valid row, so we
|
| 138 |
+
# replicate it MM times along the M axis. All MM result rows of any
|
| 139 |
+
# downstream matmul are then identical, and we only keep row 0.
|
| 140 |
+
q_row = tl.load(Q_ptr + offs_d).to(tl.float32) * SCALE_2 # [D] fp32
|
| 141 |
+
q_bf16 = tl.broadcast_to(q_row[None, :], [MM, D]).to(tl.bfloat16) # [MM, D]
|
| 142 |
+
|
| 143 |
+
m_i = tl.full([MM], float("-inf"), dtype=tl.float32)
|
| 144 |
+
l_i = tl.zeros([MM], dtype=tl.float32)
|
| 145 |
+
acc = tl.zeros([MM, D], dtype=tl.float32)
|
| 146 |
+
|
| 147 |
+
K_ptr = K + kv_bh * stride_kvbh
|
| 148 |
+
V_ptr = V + kv_bh * stride_kvbh
|
| 149 |
+
|
| 150 |
+
# Iterate K tiles over the visible window [K_START, T_KV).
|
| 151 |
+
# Tile t covers tokens [K_START + t*BK, K_START + (t+1)*BK).
|
| 152 |
+
for t in range(0, NUM_K_TILES):
|
| 153 |
+
k_base = K_START + t * BK
|
| 154 |
+
offs_k = k_base + offs_k_in_tile # [BK]
|
| 155 |
+
k_mask = offs_k < T_KV # [BK]
|
| 156 |
+
|
| 157 |
+
# Masked load: OOB columns get 0; we still mask the score to -inf
|
| 158 |
+
# so its softmax weight is 0.
|
| 159 |
+
k_tile = tl.load(
|
| 160 |
+
K_ptr + offs_k[:, None] * stride_t + offs_d[None, :],
|
| 161 |
+
mask=k_mask[:, None], other=0.0,
|
| 162 |
+
) # [BK, D] bf16
|
| 163 |
+
v_tile = tl.load(
|
| 164 |
+
V_ptr + offs_k[:, None] * stride_t + offs_d[None, :],
|
| 165 |
+
mask=k_mask[:, None], other=0.0,
|
| 166 |
+
) # [BK, D] bf16
|
| 167 |
+
|
| 168 |
+
# Score: [MM, D] @ [D, BK] -> [MM, BK] in fp32. All MM rows identical.
|
| 169 |
+
s = tl.dot(q_bf16, tl.trans(k_tile), out_dtype=tl.float32) # [MM, BK]
|
| 170 |
+
s = tl.where(k_mask[None, :], s, float("-inf"))
|
| 171 |
+
|
| 172 |
+
# Online softmax merge. Identical per row, so the [MM]-vector state
|
| 173 |
+
# stays uniform; we keep the broadcast for matmul-shape consistency.
|
| 174 |
+
s_max = tl.max(s, axis=1) # [MM]
|
| 175 |
+
m_new = tl.maximum(m_i, s_max)
|
| 176 |
+
alpha = tl.exp2(m_i - m_new) # [MM]
|
| 177 |
+
p = tl.exp2(s - m_new[:, None]) # [MM, BK]
|
| 178 |
+
l_i = l_i * alpha + tl.sum(p, axis=1)
|
| 179 |
+
acc = tl.dot(p.to(tl.bfloat16), v_tile, acc=acc * alpha[:, None], out_dtype=tl.float32)
|
| 180 |
+
m_i = m_new
|
| 181 |
+
|
| 182 |
+
LN2: tl.constexpr = 0.6931471805599453
|
| 183 |
+
# All MM rows of acc / l_i / m_i are identical (they each saw the same
|
| 184 |
+
# broadcast Q row through the entire loop). Reduce to the row-0 values
|
| 185 |
+
# for the final store. ``tl.sum(..., axis=0) / MM`` extracts the common
|
| 186 |
+
# value as a side effect of averaging identical replicas — this is
|
| 187 |
+
# numerically equivalent to picking row 0 but stays inside Triton's
|
| 188 |
+
# supported reduction ops without needing a slice.
|
| 189 |
+
l_safe = tl.where(l_i > 0, l_i, 1.0) # [MM]
|
| 190 |
+
o_tile = acc / l_safe[:, None] # [MM, D] fp32, all rows equal
|
| 191 |
+
INV_MM: tl.constexpr = 1.0 / MM
|
| 192 |
+
# Average the MM identical replicas to extract the row-0 result.
|
| 193 |
+
# tl.sum(axis=0) over [MM, D] -> [D]; over [MM] -> scalar wrapped as [1]
|
| 194 |
+
# after a no-op broadcast through tl.arange masking.
|
| 195 |
+
o_row = tl.sum(o_tile, axis=0) * INV_MM # [D] fp32
|
| 196 |
+
o_row_bf16 = o_row.to(tl.bfloat16) # [D] bf16
|
| 197 |
+
lse_full = tl.where(l_i > 0, (m_i + tl.log2(l_safe)) * LN2, float("-inf")) # [MM]
|
| 198 |
+
# Build a [1]-shaped LSE: sum the [MM] vector then divide; the result is a
|
| 199 |
+
# Triton scalar — wrap via tl.full to get an explicit [1]-shape value.
|
| 200 |
+
lse_scalar_val = tl.sum(lse_full, axis=0) * INV_MM # scalar fp32
|
| 201 |
+
lse_out_vec = tl.full([1], 0.0, dtype=tl.float32) + lse_scalar_val # [1]
|
| 202 |
+
|
| 203 |
+
# Flat-store the single output row [D] at O_ptr + offs_d. No mask needed:
|
| 204 |
+
# offs_d covers exactly the D-element output buffer for this (B, Hq).
|
| 205 |
+
O_ptr = O + q_bh * stride_qbh
|
| 206 |
+
tl.store(O_ptr + offs_d, o_row_bf16)
|
| 207 |
+
|
| 208 |
+
# LSE: store the [1]-shaped value at offset q_bh.
|
| 209 |
+
LSE_ptr = LSE + q_bh
|
| 210 |
+
tl.store(LSE_ptr + tl.arange(0, 1), lse_out_vec)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
_hydra_decode_kernel = _kernel_decorator(_hydra_decode_jit)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def launch_attn_fwd_decode(
|
| 217 |
+
q: torch.Tensor, # (B, H_q, 1, D) bf16
|
| 218 |
+
k: torch.Tensor, # (B, H_kv, T_kv, D) bf16
|
| 219 |
+
v: torch.Tensor, # (B, H_kv, T_kv, D) bf16
|
| 220 |
+
window: int = 0,
|
| 221 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 222 |
+
"""Launch the decode-step forward kernel.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
q: (B, H_q, 1, D) bf16. T_q must be exactly 1.
|
| 226 |
+
k: (B, H_kv, T_kv, D) bf16.
|
| 227 |
+
v: (B, H_kv, T_kv, D) bf16.
|
| 228 |
+
window: 0 disables sliding window (attend all K). When > 0, only the
|
| 229 |
+
last ``window`` K tokens are attended: K range = [T_kv - window,
|
| 230 |
+
T_kv). The query is at position T_kv - 1, so this matches the
|
| 231 |
+
standard sliding-window-causal semantics used at prefill.
|
| 232 |
+
|
| 233 |
+
Returns:
|
| 234 |
+
o: (B, H_q, 1, D) bf16
|
| 235 |
+
lse: (B, H_q, 1) fp32
|
| 236 |
+
|
| 237 |
+
Notes:
|
| 238 |
+
Assumes K/V are contiguous in (B, H_kv, T_kv, D). T_kv does NOT need
|
| 239 |
+
to be a multiple of BLOCK_K; the last K-tile is masked.
|
| 240 |
+
"""
|
| 241 |
+
if q.dim() != 4 or k.dim() != 4 or v.dim() != 4:
|
| 242 |
+
raise ValueError(f"q/k/v must be 4D (B,H,T,D); got {q.shape} {k.shape} {v.shape}")
|
| 243 |
+
B, Hq, Tq, D = q.shape
|
| 244 |
+
if Tq != 1:
|
| 245 |
+
raise ValueError(f"decode kernel requires Tq == 1; got Tq={Tq}")
|
| 246 |
+
if k.shape[0] != B or v.shape[0] != B:
|
| 247 |
+
raise ValueError(f"batch mismatch q={q.shape} k={k.shape} v={v.shape}")
|
| 248 |
+
if k.shape != v.shape:
|
| 249 |
+
raise ValueError(f"k and v must have identical shapes; got {k.shape} vs {v.shape}")
|
| 250 |
+
if k.shape[3] != D:
|
| 251 |
+
raise ValueError(f"head_dim mismatch q={q.shape[-1]} k={k.shape[-1]}")
|
| 252 |
+
Hkv = k.shape[1]
|
| 253 |
+
Tkv = k.shape[2]
|
| 254 |
+
if Hq % Hkv != 0:
|
| 255 |
+
raise ValueError(f"H_q ({Hq}) must be a multiple of H_kv ({Hkv})")
|
| 256 |
+
if D != HEAD_DIM:
|
| 257 |
+
raise ValueError(f"head_dim ({D}) must equal HEAD_DIM ({HEAD_DIM})")
|
| 258 |
+
if Tkv <= 0:
|
| 259 |
+
raise ValueError(f"T_kv must be positive; got {Tkv}")
|
| 260 |
+
|
| 261 |
+
# Sliding window: clamp to [0, Tkv]. window <= 0 or window >= Tkv -> full.
|
| 262 |
+
if window is None or window <= 0 or window >= Tkv:
|
| 263 |
+
k_start = 0
|
| 264 |
+
else:
|
| 265 |
+
k_start = Tkv - window
|
| 266 |
+
num_k_visible = Tkv - k_start
|
| 267 |
+
num_k_tiles = (num_k_visible + BLOCK_K - 1) // BLOCK_K
|
| 268 |
+
|
| 269 |
+
o = torch.empty_like(q)
|
| 270 |
+
lse = torch.empty((B, Hq, 1), device=q.device, dtype=torch.float32)
|
| 271 |
+
|
| 272 |
+
grid = (B * Hq,)
|
| 273 |
+
# Pull actual strides from K (V must match shape). Lets StaticCache pass
|
| 274 |
+
# (B, Hkv, T_max, D) buffers — previously hardcoded stride_kvbh = Tkv*D
|
| 275 |
+
# would silently read garbage when T_max != Tkv.
|
| 276 |
+
common_kwargs = dict(
|
| 277 |
+
T_KV=Tkv,
|
| 278 |
+
stride_kvbh_kv=k.stride(1),
|
| 279 |
+
stride_t_kv=k.stride(2),
|
| 280 |
+
NUM_HEADS=Hq,
|
| 281 |
+
NUM_KV_HEADS=Hkv,
|
| 282 |
+
SCALE=1.0 / math.sqrt(D),
|
| 283 |
+
BK=BLOCK_K,
|
| 284 |
+
D=D,
|
| 285 |
+
MM=16, # tensor-core minimum M for tl.dot on Blackwell bf16.
|
| 286 |
+
K_START=int(k_start),
|
| 287 |
+
NUM_K_TILES=int(num_k_tiles),
|
| 288 |
+
)
|
| 289 |
+
if _DISABLE_AUTOTUNE:
|
| 290 |
+
common_kwargs["num_warps"] = _DEC_NUM_WARPS
|
| 291 |
+
common_kwargs["num_stages"] = _DEC_NUM_STAGES
|
| 292 |
+
|
| 293 |
+
_hydra_decode_kernel[grid](
|
| 294 |
+
q, k, v,
|
| 295 |
+
o, lse,
|
| 296 |
+
**common_kwargs,
|
| 297 |
+
)
|
| 298 |
+
return o, lse
|
torch-ext/hydra/kernel_delta.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single-pass Triton delta kernel.
|
| 2 |
+
|
| 3 |
+
Replaces the host-side ``(do.float() * o.float()).sum(-1)`` dance in
|
| 4 |
+
``hydra.kernel_bwd.launch_attn_bwd`` with a fused
|
| 5 |
+
GPU-resident kernel that reads ``o`` and ``do`` once (bf16, the same
|
| 6 |
+
loads the existing autograd chain already pays for) and writes a
|
| 7 |
+
single ``(B, Hq, T)`` fp32 ``delta`` tensor.
|
| 8 |
+
|
| 9 |
+
What this saves vs the host-side computation (Qwen3-8B-ish prefill,
|
| 10 |
+
B=1, H_q=32, T=8192, D=128):
|
| 11 |
+
|
| 12 |
+
- ``do.float()`` 128 MB fp32 transient: gone
|
| 13 |
+
- ``o.float()`` 128 MB fp32 transient: gone
|
| 14 |
+
- ``do.float() * o.float()`` 128 MB fp32 product: gone
|
| 15 |
+
- net delta tensor still allocated: 1 MB fp32 (B*Hq*T*4)
|
| 16 |
+
|
| 17 |
+
For an at-rest measurement the peak transient eliminated is ~384 MB;
|
| 18 |
+
in the steady-state allocator the saving conservatively measured by
|
| 19 |
+
the prior agent's accounting is ~256 MB (after one of the upcasts is
|
| 20 |
+
reaped). Either way the dedicated kernel is the right move — the
|
| 21 |
+
``o``/``do`` bytes are already on-chip in the autograd path, we just
|
| 22 |
+
fold the multiply+reduce into one pass.
|
| 23 |
+
|
| 24 |
+
Math:
|
| 25 |
+
|
| 26 |
+
delta[b, h, t] = sum_{d=0..D-1} o[b, h, t, d] * do[b, h, t, d]
|
| 27 |
+
|
| 28 |
+
bf16 inputs, fp32 accumulator, fp32 output. The reduction order is
|
| 29 |
+
Triton's parallel ``tl.sum`` tree across the D axis (per tile-row).
|
| 30 |
+
Compared to torch's left-to-right ``.sum(-1)`` the absolute error
|
| 31 |
+
bound is roughly ``D * eps_fp32 * max|o*do|`` which is ~1.5e-5 at
|
| 32 |
+
attention-typical scales — well within bf16's ~4e-3 quantum.
|
| 33 |
+
|
| 34 |
+
Layout:
|
| 35 |
+
|
| 36 |
+
- ``o``, ``do``: ``(B, Hq, T, D)`` bf16, contiguous.
|
| 37 |
+
- ``delta``: ``(B, Hq, T)`` fp32, contiguous.
|
| 38 |
+
- One program per ``(b*Hq + h, q_block_id)`` work item, computing
|
| 39 |
+
``BLOCK_SIZE`` rows of delta with full ``D`` in-tile reduction.
|
| 40 |
+
- Grid: ``(B * Hq * num_q_blocks,)`` flattened.
|
| 41 |
+
|
| 42 |
+
Arbitrary T: the kernel masks the tail tile (``offs_tok < T``); the
|
| 43 |
+
launcher does not require ``T % BLOCK_SIZE == 0``.
|
| 44 |
+
"""
|
| 45 |
+
from __future__ import annotations
|
| 46 |
+
|
| 47 |
+
import os
|
| 48 |
+
|
| 49 |
+
import torch
|
| 50 |
+
import triton
|
| 51 |
+
import triton.language as tl
|
| 52 |
+
|
| 53 |
+
from .kernel_fwd import BLOCK_SIZE, HEAD_DIM
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
_DELTA_NUM_WARPS = int(os.environ.get("HYDRA_DELTA_NUM_WARPS", "4"))
|
| 57 |
+
_DELTA_NUM_STAGES = int(os.environ.get("HYDRA_DELTA_NUM_STAGES", "1"))
|
| 58 |
+
_DISABLE_AUTOTUNE = int(os.environ.get("HYDRA_DISABLE_AUTOTUNE", "0"))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _autotune_configs() -> list[triton.Config]:
|
| 62 |
+
"""Same warps/stages sweep used by kernel_fwd; D and BS are fixed."""
|
| 63 |
+
configs: list[triton.Config] = []
|
| 64 |
+
for num_warps in (1, 2, 4):
|
| 65 |
+
for num_stages in (1, 2):
|
| 66 |
+
configs.append(triton.Config({}, num_warps=num_warps, num_stages=num_stages))
|
| 67 |
+
return configs
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _kernel_decorator(jit_kernel):
|
| 71 |
+
if _DISABLE_AUTOTUNE:
|
| 72 |
+
return jit_kernel
|
| 73 |
+
return triton.autotune(
|
| 74 |
+
configs=_autotune_configs(),
|
| 75 |
+
key=["T_MAX", "D"],
|
| 76 |
+
)(jit_kernel)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@triton.jit
|
| 80 |
+
def _compute_delta_jit(
|
| 81 |
+
O, # (B, Hq, T, D) bf16
|
| 82 |
+
dO, # (B, Hq, T, D) bf16
|
| 83 |
+
Delta, # (B, Hq, T) fp32
|
| 84 |
+
T_MAX: tl.constexpr,
|
| 85 |
+
BS: tl.constexpr,
|
| 86 |
+
D: tl.constexpr,
|
| 87 |
+
):
|
| 88 |
+
"""Compute ``Delta[b, h, t] = sum_d O[b, h, t, d] * dO[b, h, t, d]``.
|
| 89 |
+
|
| 90 |
+
One program handles a single ``(b*Hq + h, q_block_id)`` tile of
|
| 91 |
+
``BS`` rows, reducing the full ``D``-axis in fp32 in-register.
|
| 92 |
+
|
| 93 |
+
Grid: ``(batch_heads * num_q_blocks,)``.
|
| 94 |
+
"""
|
| 95 |
+
# Per-head row strides match the (B, Hq, T, D) / (B, Hq, T) layout
|
| 96 |
+
# the contiguous-tensor invariant gives us. Encoding them as
|
| 97 |
+
# constexpr lets the compiler fold the address arithmetic.
|
| 98 |
+
stride_h: tl.constexpr = T_MAX * D
|
| 99 |
+
stride_t: tl.constexpr = D
|
| 100 |
+
stride_lh: tl.constexpr = T_MAX
|
| 101 |
+
NUM_Q_BLOCKS: tl.constexpr = (T_MAX + BS - 1) // BS
|
| 102 |
+
|
| 103 |
+
pid = tl.program_id(0)
|
| 104 |
+
bh_id = pid // NUM_Q_BLOCKS
|
| 105 |
+
q_block_id = pid % NUM_Q_BLOCKS
|
| 106 |
+
|
| 107 |
+
q_start = q_block_id * BS
|
| 108 |
+
offs_tok = q_start + tl.arange(0, BS)
|
| 109 |
+
offs_d = tl.arange(0, D)
|
| 110 |
+
# Mask trailing tile when T is not a multiple of BS.
|
| 111 |
+
row_mask = offs_tok < T_MAX
|
| 112 |
+
|
| 113 |
+
# Load the (BS, D) o and do tiles in bf16. Out-of-bound rows are
|
| 114 |
+
# zero-filled so their contribution to the (masked-out) sum is
|
| 115 |
+
# exactly 0 and doesn't pollute neighbour rows under the tl.sum tree.
|
| 116 |
+
O_ptr = O + bh_id * stride_h
|
| 117 |
+
dO_ptr = dO + bh_id * stride_h
|
| 118 |
+
addr = offs_tok[:, None] * stride_t + offs_d[None, :]
|
| 119 |
+
o_bf16 = tl.load(O_ptr + addr, mask=row_mask[:, None], other=0.0)
|
| 120 |
+
do_bf16 = tl.load(dO_ptr + addr, mask=row_mask[:, None], other=0.0)
|
| 121 |
+
|
| 122 |
+
# bf16 -> fp32 -> elementwise mul -> reduce along D.
|
| 123 |
+
# The cast-then-mul order matches the host-side path
|
| 124 |
+
# ``do.float() * o.float()`` exactly (no intermediate bf16 product).
|
| 125 |
+
prod = o_bf16.to(tl.float32) * do_bf16.to(tl.float32)
|
| 126 |
+
di = tl.sum(prod, axis=-1) # (BS,) fp32
|
| 127 |
+
|
| 128 |
+
Delta_ptr = Delta + bh_id * stride_lh
|
| 129 |
+
tl.store(Delta_ptr + offs_tok, di, mask=row_mask)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
_compute_delta_kernel = _kernel_decorator(_compute_delta_jit)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def launch_compute_delta(o: torch.Tensor, do: torch.Tensor) -> torch.Tensor:
|
| 136 |
+
"""Compute ``delta = sum_d o * do`` as a fused single-pass GPU kernel.
|
| 137 |
+
|
| 138 |
+
Parameters
|
| 139 |
+
----------
|
| 140 |
+
o, do : ``(B, Hq, T, D)`` bf16 tensors. Must be contiguous and on the
|
| 141 |
+
same CUDA device. ``T`` is arbitrary (the tail tile is
|
| 142 |
+
masked); ``D`` must equal ``HEAD_DIM``.
|
| 143 |
+
|
| 144 |
+
Returns
|
| 145 |
+
-------
|
| 146 |
+
delta : ``(B, Hq, T)`` fp32 tensor, same device. Allocated fresh.
|
| 147 |
+
|
| 148 |
+
Numerical equivalence to ``(do.float() * o.float()).sum(-1)``:
|
| 149 |
+
|
| 150 |
+
Both compute ``sum_d (bf16_to_fp32(o[d]) * bf16_to_fp32(do[d]))``
|
| 151 |
+
with ``D == 128`` summands per (b, h, t) row. Triton's ``tl.sum``
|
| 152 |
+
uses a parallel reduction tree while torch's ``.sum(-1)`` is a
|
| 153 |
+
left-to-right scan. The reordering error is bounded by
|
| 154 |
+
``D * eps_fp32 * max|o*do|`` ~ ``128 * 1.2e-7 * O(1)`` ~ ``1.5e-5``,
|
| 155 |
+
well under bf16's ~4e-3 quantum.
|
| 156 |
+
"""
|
| 157 |
+
if o.shape != do.shape:
|
| 158 |
+
raise ValueError(f"o and do must have the same shape; got {o.shape} vs {do.shape}")
|
| 159 |
+
if o.dim() != 4:
|
| 160 |
+
raise ValueError(f"o must be 4D (B, Hq, T, D); got shape {o.shape}")
|
| 161 |
+
if o.dtype != torch.bfloat16 or do.dtype != torch.bfloat16:
|
| 162 |
+
raise ValueError(f"o, do must be bf16; got {o.dtype}, {do.dtype}")
|
| 163 |
+
if o.device != do.device:
|
| 164 |
+
raise ValueError(f"o and do must be on the same device; got {o.device} vs {do.device}")
|
| 165 |
+
if not o.is_contiguous() or not do.is_contiguous():
|
| 166 |
+
raise ValueError("o and do must be contiguous")
|
| 167 |
+
|
| 168 |
+
B, Hq, T, D = o.shape
|
| 169 |
+
if D != HEAD_DIM:
|
| 170 |
+
raise ValueError(f"head_dim ({D}) must equal HEAD_DIM ({HEAD_DIM})")
|
| 171 |
+
|
| 172 |
+
delta = torch.empty(B, Hq, T, dtype=torch.float32, device=o.device)
|
| 173 |
+
|
| 174 |
+
batch_heads = B * Hq
|
| 175 |
+
num_q_blocks = (T + BLOCK_SIZE - 1) // BLOCK_SIZE
|
| 176 |
+
grid = (batch_heads * num_q_blocks,)
|
| 177 |
+
|
| 178 |
+
kwargs = dict(
|
| 179 |
+
T_MAX=T,
|
| 180 |
+
BS=BLOCK_SIZE,
|
| 181 |
+
D=D,
|
| 182 |
+
)
|
| 183 |
+
if _DISABLE_AUTOTUNE:
|
| 184 |
+
kwargs["num_warps"] = _DELTA_NUM_WARPS
|
| 185 |
+
kwargs["num_stages"] = _DELTA_NUM_STAGES
|
| 186 |
+
|
| 187 |
+
_compute_delta_kernel[grid](o, do, delta, **kwargs)
|
| 188 |
+
return delta
|
torch-ext/hydra/kernel_fwd.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cross-arch-tuned forward kernel.
|
| 2 |
+
|
| 3 |
+
This module contains the active forward Triton kernel used by the extracted
|
| 4 |
+
Hydra package. The JIT kernel body and launcher descend from the iter3
|
| 5 |
+
research kernel, with the cross-architecture tuning hooks kept explicit near
|
| 6 |
+
the top of the file.
|
| 7 |
+
|
| 8 |
+
Why
|
| 9 |
+
---
|
| 10 |
+
The iter3 autotune sweep is a 9-cell cartesian
|
| 11 |
+
``(num_warps ∈ {2,4,8}) × (num_stages ∈ {1,2,3})`` over a fixed
|
| 12 |
+
``BLOCK_SIZE = 64``. Tuning at import time per
|
| 13 |
+
``torch.cuda.get_device_capability()`` lets us:
|
| 14 |
+
|
| 15 |
+
1. Lower default ``BLOCK_SIZE`` from 64 → 32 on every measured arch
|
| 16 |
+
(sm_86/89/120/121). Cross-arch sweep at T=4096 dense fwdbwd found
|
| 17 |
+
BS=32 winning on every architecture we measured. The smaller K/V
|
| 18 |
+
tile reduces shared-memory pressure and lets more CTAs run
|
| 19 |
+
concurrently per SM, which dominates on Ampere/Ada cards
|
| 20 |
+
(~100 KB smem/SM cap) and is neutral-to-positive on Blackwell.
|
| 21 |
+
|
| 22 |
+
2. Replace the 9-cell autotune sweep with a 1-3 cell list keyed on
|
| 23 |
+
compute capability. For arches we've measured we ship the
|
| 24 |
+
sweep-winning ``(num_warps, num_stages)`` plus one or two
|
| 25 |
+
safe-fallback configs (so the autotuner can't get stuck if a
|
| 26 |
+
particular load pattern at runtime regresses on the headline cell).
|
| 27 |
+
For arches we have NOT measured (anything not in our table) we
|
| 28 |
+
fall back to the original 9-cell sweep so the autotuner can find
|
| 29 |
+
a good config on first use.
|
| 30 |
+
|
| 31 |
+
Both changes preserve the launcher API and the kernel signature.
|
| 32 |
+
No correctness change: ``BS`` is still a constexpr; the kernel just
|
| 33 |
+
gets specialised at a different value.
|
| 34 |
+
|
| 35 |
+
Per-arch defaults are provisional tuning seeds, not benchmark claims. They
|
| 36 |
+
control autotune order and can be overridden with the environment variables
|
| 37 |
+
below. Submission-facing benchmark claims must come from checked-in artifacts,
|
| 38 |
+
not this header.
|
| 39 |
+
|
| 40 |
+
Do not cite the defaults below as evidence of speedup or hardware coverage.
|
| 41 |
+
|
| 42 |
+
Override hooks
|
| 43 |
+
--------------
|
| 44 |
+
Three env vars short-circuit everything:
|
| 45 |
+
- HYDRA_BLOCK_SIZE - wins over the per-arch default
|
| 46 |
+
- HYDRA_DISABLE_AUTOTUNE=1 + HYDRA_NUM_WARPS
|
| 47 |
+
+ HYDRA_NUM_STAGES — wins over autotune
|
| 48 |
+
- HYDRA_AUTOTUNE_MODE=full — forces the 9-cell sweep
|
| 49 |
+
even on a known arch (useful for re-tuning new wheels of triton).
|
| 50 |
+
"""
|
| 51 |
+
from __future__ import annotations
|
| 52 |
+
|
| 53 |
+
import math
|
| 54 |
+
import os
|
| 55 |
+
|
| 56 |
+
import torch
|
| 57 |
+
import triton
|
| 58 |
+
import triton.language as tl
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# ---------------- Per-arch defaults --------------------------------------
|
| 62 |
+
|
| 63 |
+
# Per-(major,minor) compute-capability -> (BLOCK_SIZE, [(num_warps, num_stages), ...])
|
| 64 |
+
#
|
| 65 |
+
# The autotune list is ordered with the SWEEP-WINNING config first so that
|
| 66 |
+
# the autotuner picks it on the first launch (which is also the only launch
|
| 67 |
+
# in many production deploys: the cache key includes T_MAX/D so a stable
|
| 68 |
+
# shape produces exactly one autotune cycle).
|
| 69 |
+
#
|
| 70 |
+
# Tail entries are conservative fallbacks: they exist so the autotuner has
|
| 71 |
+
# at least one safe config to fall back to if a specific runtime workload
|
| 72 |
+
# regresses on the headline cell (e.g. a different T that we did not sweep).
|
| 73 |
+
_PER_ARCH: dict[tuple[int, int], tuple[int, list[tuple[int, int]]]] = {
|
| 74 |
+
# GB10 (Blackwell sm_121).
|
| 75 |
+
(12, 1): (32, [(2, 3), (4, 2)]),
|
| 76 |
+
# Pro 6000 (Blackwell sm_120). NOT MEASURED in iter; default to the
|
| 77 |
+
# sm_121 pick — they share most of the relevant resources. Adjust
|
| 78 |
+
# after running the sweep on a free Pro 6000.
|
| 79 |
+
(12, 0): (32, [(2, 3), (4, 2)]),
|
| 80 |
+
# Ada (sm_89): 4080, 4070 Ti.
|
| 81 |
+
(8, 9): (32, [(4, 1), (4, 2)]),
|
| 82 |
+
# Ampere consumer (sm_86): 3090, 3080, 3060.
|
| 83 |
+
(8, 6): (32, [(2, 1), (4, 2)]),
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# The original 9-cell sweep — used when no per-arch entry exists, and also
|
| 87 |
+
# when the user opts in via HYDRA_AUTOTUNE_MODE=full.
|
| 88 |
+
_FULL_SWEEP: list[tuple[int, int]] = [
|
| 89 |
+
(W, S) for W in (2, 4, 8) for S in (1, 2, 3)
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _device_capability_or_none() -> tuple[int, int] | None:
|
| 94 |
+
"""Return ``(major, minor)`` for cuda:0, or ``None`` if no CUDA."""
|
| 95 |
+
try:
|
| 96 |
+
if torch.cuda.is_available():
|
| 97 |
+
return tuple(torch.cuda.get_device_capability(0)) # type: ignore[return-value]
|
| 98 |
+
except Exception:
|
| 99 |
+
pass
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _default_block_size_from_arch() -> int:
|
| 104 |
+
"""Pick BLOCK_SIZE based on the current CUDA device's compute capability.
|
| 105 |
+
|
| 106 |
+
Fall back to 64 (iter3's value) if we don't have a measured entry, so
|
| 107 |
+
no measured arch regresses below its iter3 baseline.
|
| 108 |
+
"""
|
| 109 |
+
cc = _device_capability_or_none()
|
| 110 |
+
if cc is not None and cc in _PER_ARCH:
|
| 111 |
+
return _PER_ARCH[cc][0]
|
| 112 |
+
return 64
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _arch_aware_autotune_configs() -> list[triton.Config]:
|
| 116 |
+
"""Per-arch shrunk autotune list, or the full 9-cell sweep if unknown."""
|
| 117 |
+
if os.environ.get("HYDRA_AUTOTUNE_MODE", "").lower() == "full":
|
| 118 |
+
ws_pairs = _FULL_SWEEP
|
| 119 |
+
else:
|
| 120 |
+
cc = _device_capability_or_none()
|
| 121 |
+
if cc is not None and cc in _PER_ARCH:
|
| 122 |
+
ws_pairs = _PER_ARCH[cc][1]
|
| 123 |
+
else:
|
| 124 |
+
ws_pairs = _FULL_SWEEP
|
| 125 |
+
return [triton.Config({}, num_warps=W, num_stages=S) for (W, S) in ws_pairs]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# Env var override wins over per-arch default (preserves the iter3 escape hatch).
|
| 129 |
+
BLOCK_SIZE = int(os.environ.get("HYDRA_BLOCK_SIZE", str(_default_block_size_from_arch())))
|
| 130 |
+
HEAD_DIM = int(os.environ.get("HYDRA_HEAD_DIM", "128"))
|
| 131 |
+
|
| 132 |
+
_FWD_NUM_WARPS = int(os.environ.get("HYDRA_NUM_WARPS", "4"))
|
| 133 |
+
_FWD_NUM_STAGES = int(os.environ.get("HYDRA_NUM_STAGES", "2"))
|
| 134 |
+
_DISABLE_AUTOTUNE = int(os.environ.get("HYDRA_DISABLE_AUTOTUNE", "0"))
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _autotune_configs() -> list[triton.Config]:
|
| 138 |
+
return _arch_aware_autotune_configs()
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _kernel_decorator(jit_kernel):
|
| 142 |
+
if _DISABLE_AUTOTUNE:
|
| 143 |
+
return jit_kernel
|
| 144 |
+
return triton.autotune(
|
| 145 |
+
configs=_autotune_configs(),
|
| 146 |
+
key=["T_MAX", "D", "NUM_HEADS", "NUM_KV_HEADS", "ASSUME_FULL"],
|
| 147 |
+
)(jit_kernel)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@triton.jit
|
| 151 |
+
def _hydra_fwd_jit(
|
| 152 |
+
Q, K, V,
|
| 153 |
+
O, LSE,
|
| 154 |
+
RowPtr, ColIdx, SeqLens,
|
| 155 |
+
stride_cih,
|
| 156 |
+
T_MAX: tl.constexpr,
|
| 157 |
+
NUM_HEADS: tl.constexpr,
|
| 158 |
+
NUM_KV_HEADS: tl.constexpr,
|
| 159 |
+
SCALE: tl.constexpr,
|
| 160 |
+
BS: tl.constexpr,
|
| 161 |
+
D: tl.constexpr,
|
| 162 |
+
WINDOW: tl.constexpr,
|
| 163 |
+
CONTIGUOUS_OFFDIAG: tl.constexpr = 0,
|
| 164 |
+
ASSUME_FULL: tl.constexpr = 0,
|
| 165 |
+
):
|
| 166 |
+
stride_h: tl.constexpr = T_MAX * D
|
| 167 |
+
stride_t: tl.constexpr = D
|
| 168 |
+
stride_lh: tl.constexpr = T_MAX
|
| 169 |
+
stride_rph: tl.constexpr = (T_MAX // BS) + 1
|
| 170 |
+
NUM_Q_BLOCKS: tl.constexpr = T_MAX // BS
|
| 171 |
+
|
| 172 |
+
pid = tl.program_id(0)
|
| 173 |
+
bh_id = pid // NUM_Q_BLOCKS
|
| 174 |
+
q_block_id = pid % NUM_Q_BLOCKS
|
| 175 |
+
|
| 176 |
+
rep: tl.constexpr = NUM_HEADS // NUM_KV_HEADS
|
| 177 |
+
b_id = bh_id // NUM_HEADS
|
| 178 |
+
hq_id = bh_id % NUM_HEADS
|
| 179 |
+
hkv_id = hq_id // rep
|
| 180 |
+
kv_bh_id = b_id * NUM_KV_HEADS + hkv_id
|
| 181 |
+
|
| 182 |
+
if ASSUME_FULL:
|
| 183 |
+
seq_len = T_MAX # constexpr-friendly: no SeqLens load, no early-return
|
| 184 |
+
else:
|
| 185 |
+
seq_len = tl.load(SeqLens + b_id)
|
| 186 |
+
|
| 187 |
+
q_start = q_block_id * BS
|
| 188 |
+
|
| 189 |
+
offs_tok = q_start + tl.arange(0, BS)
|
| 190 |
+
offs_d = tl.arange(0, D)
|
| 191 |
+
|
| 192 |
+
if ASSUME_FULL:
|
| 193 |
+
pass # no early-return path needed
|
| 194 |
+
else:
|
| 195 |
+
if q_start >= seq_len:
|
| 196 |
+
O_ptr_e = O + bh_id * stride_h
|
| 197 |
+
tl.store(O_ptr_e + offs_tok[:, None] * stride_t + offs_d[None, :],
|
| 198 |
+
tl.zeros([BS, D], dtype=tl.bfloat16))
|
| 199 |
+
LSE_ptr_e = LSE + bh_id * stride_lh
|
| 200 |
+
tl.store(LSE_ptr_e + offs_tok, tl.full([BS], float("-inf"), dtype=tl.float32))
|
| 201 |
+
return
|
| 202 |
+
|
| 203 |
+
LOG2E: tl.constexpr = 1.4426950408889634
|
| 204 |
+
SCALE_2: tl.constexpr = SCALE * LOG2E
|
| 205 |
+
Q_ptr = Q + bh_id * stride_h
|
| 206 |
+
q_bf16 = (tl.load(Q_ptr + offs_tok[:, None] * stride_t + offs_d[None, :]).to(tl.float32) * SCALE_2).to(tl.bfloat16)
|
| 207 |
+
|
| 208 |
+
m_i = tl.full([BS], float("-inf"), dtype=tl.float32)
|
| 209 |
+
l_i = tl.zeros([BS], dtype=tl.float32)
|
| 210 |
+
acc = tl.zeros([BS, D], dtype=tl.float32)
|
| 211 |
+
|
| 212 |
+
rp_base = RowPtr + bh_id * stride_rph + q_block_id
|
| 213 |
+
ci_lo = tl.load(rp_base)
|
| 214 |
+
ci_hi = tl.load(rp_base + 1)
|
| 215 |
+
|
| 216 |
+
K_ptr = K + kv_bh_id * stride_h
|
| 217 |
+
V_ptr = V + kv_bh_id * stride_h
|
| 218 |
+
CI_ptr = ColIdx + bh_id * stride_cih
|
| 219 |
+
|
| 220 |
+
offs_d_arange = tl.arange(0, BS)
|
| 221 |
+
|
| 222 |
+
if ci_hi > ci_lo:
|
| 223 |
+
k_start_d = q_block_id * BS
|
| 224 |
+
offs_k_d = k_start_d + offs_d_arange
|
| 225 |
+
|
| 226 |
+
if ASSUME_FULL:
|
| 227 |
+
# No boundary branch — full unmasked diagonal load + causal mask.
|
| 228 |
+
k_bf16_d = tl.load(K_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :])
|
| 229 |
+
v_bf16_d = tl.load(V_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :])
|
| 230 |
+
s_d = tl.dot(q_bf16, tl.trans(k_bf16_d), out_dtype=tl.float32)
|
| 231 |
+
causal = offs_tok[:, None] >= offs_k_d[None, :]
|
| 232 |
+
if WINDOW > 0:
|
| 233 |
+
in_window = (offs_tok[:, None] - offs_k_d[None, :]) < WINDOW
|
| 234 |
+
s_d = tl.where(causal & in_window, s_d, float("-inf"))
|
| 235 |
+
else:
|
| 236 |
+
s_d = tl.where(causal, s_d, float("-inf"))
|
| 237 |
+
else:
|
| 238 |
+
boundary = (q_start + BS) > seq_len
|
| 239 |
+
if boundary:
|
| 240 |
+
k_mask = offs_k_d < seq_len
|
| 241 |
+
k_bf16_d = tl.load(K_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :],
|
| 242 |
+
mask=k_mask[:, None], other=0.0)
|
| 243 |
+
v_bf16_d = tl.load(V_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :],
|
| 244 |
+
mask=k_mask[:, None], other=0.0)
|
| 245 |
+
s_d = tl.dot(q_bf16, tl.trans(k_bf16_d), out_dtype=tl.float32)
|
| 246 |
+
causal = offs_tok[:, None] >= offs_k_d[None, :]
|
| 247 |
+
allowed = causal & k_mask[None, :]
|
| 248 |
+
if WINDOW > 0:
|
| 249 |
+
in_window = (offs_tok[:, None] - offs_k_d[None, :]) < WINDOW
|
| 250 |
+
allowed = allowed & in_window
|
| 251 |
+
s_d = tl.where(allowed, s_d, float("-inf"))
|
| 252 |
+
else:
|
| 253 |
+
k_bf16_d = tl.load(K_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :])
|
| 254 |
+
v_bf16_d = tl.load(V_ptr + offs_k_d[:, None] * stride_t + offs_d[None, :])
|
| 255 |
+
s_d = tl.dot(q_bf16, tl.trans(k_bf16_d), out_dtype=tl.float32)
|
| 256 |
+
causal = offs_tok[:, None] >= offs_k_d[None, :]
|
| 257 |
+
if WINDOW > 0:
|
| 258 |
+
in_window = (offs_tok[:, None] - offs_k_d[None, :]) < WINDOW
|
| 259 |
+
s_d = tl.where(causal & in_window, s_d, float("-inf"))
|
| 260 |
+
else:
|
| 261 |
+
s_d = tl.where(causal, s_d, float("-inf"))
|
| 262 |
+
|
| 263 |
+
m_i = tl.max(s_d, axis=1)
|
| 264 |
+
p_d = tl.exp2(s_d - m_i[:, None])
|
| 265 |
+
l_i = tl.sum(p_d, axis=1)
|
| 266 |
+
acc = tl.dot(p_d.to(tl.bfloat16), v_bf16_d, out_dtype=tl.float32)
|
| 267 |
+
|
| 268 |
+
# Off-diag loop unchanged (no boundary semantics here in either mode).
|
| 269 |
+
if CONTIGUOUS_OFFDIAG:
|
| 270 |
+
k_block_id_start = tl.load(CI_ptr + ci_lo)
|
| 271 |
+
for ci in range(ci_lo, ci_hi - 1):
|
| 272 |
+
k_block_id = k_block_id_start + (ci - ci_lo)
|
| 273 |
+
k_start = k_block_id * BS
|
| 274 |
+
offs_k = k_start + offs_d_arange
|
| 275 |
+
k_bf16 = tl.load(K_ptr + offs_k[:, None] * stride_t + offs_d[None, :])
|
| 276 |
+
v_bf16 = tl.load(V_ptr + offs_k[:, None] * stride_t + offs_d[None, :])
|
| 277 |
+
s = tl.dot(q_bf16, tl.trans(k_bf16), out_dtype=tl.float32)
|
| 278 |
+
if WINDOW > 0:
|
| 279 |
+
in_window = (offs_tok[:, None] - offs_k[None, :]) < WINDOW
|
| 280 |
+
s = tl.where(in_window, s, float("-inf"))
|
| 281 |
+
m_new = tl.maximum(m_i, tl.max(s, axis=1))
|
| 282 |
+
alpha = tl.exp2(m_i - m_new)
|
| 283 |
+
p = tl.exp2(s - m_new[:, None])
|
| 284 |
+
l_i = l_i * alpha + tl.sum(p, axis=1)
|
| 285 |
+
acc = tl.dot(p.to(tl.bfloat16), v_bf16, acc=acc * alpha[:, None], out_dtype=tl.float32)
|
| 286 |
+
m_i = m_new
|
| 287 |
+
else:
|
| 288 |
+
for ci in range(ci_lo, ci_hi - 1):
|
| 289 |
+
k_block_id = tl.load(CI_ptr + ci)
|
| 290 |
+
k_start = k_block_id * BS
|
| 291 |
+
offs_k = k_start + offs_d_arange
|
| 292 |
+
k_bf16 = tl.load(K_ptr + offs_k[:, None] * stride_t + offs_d[None, :])
|
| 293 |
+
v_bf16 = tl.load(V_ptr + offs_k[:, None] * stride_t + offs_d[None, :])
|
| 294 |
+
s = tl.dot(q_bf16, tl.trans(k_bf16), out_dtype=tl.float32)
|
| 295 |
+
if WINDOW > 0:
|
| 296 |
+
in_window = (offs_tok[:, None] - offs_k[None, :]) < WINDOW
|
| 297 |
+
s = tl.where(in_window, s, float("-inf"))
|
| 298 |
+
m_new = tl.maximum(m_i, tl.max(s, axis=1))
|
| 299 |
+
alpha = tl.exp2(m_i - m_new)
|
| 300 |
+
p = tl.exp2(s - m_new[:, None])
|
| 301 |
+
l_i = l_i * alpha + tl.sum(p, axis=1)
|
| 302 |
+
acc = tl.dot(p.to(tl.bfloat16), v_bf16, acc=acc * alpha[:, None], out_dtype=tl.float32)
|
| 303 |
+
m_i = m_new
|
| 304 |
+
|
| 305 |
+
LN2: tl.constexpr = 0.6931471805599453
|
| 306 |
+
l_safe = tl.where(l_i > 0, l_i, 1.0)
|
| 307 |
+
if ASSUME_FULL:
|
| 308 |
+
o_tile = acc / l_safe[:, None]
|
| 309 |
+
lse_out = (m_i + tl.log2(l_safe)) * LN2
|
| 310 |
+
else:
|
| 311 |
+
q_mask = offs_tok < seq_len
|
| 312 |
+
o_tile = tl.where(q_mask[:, None], acc / l_safe[:, None], 0.0)
|
| 313 |
+
lse_out = tl.where(q_mask, (m_i + tl.log2(l_safe)) * LN2, float("-inf"))
|
| 314 |
+
|
| 315 |
+
O_ptr = O + bh_id * stride_h
|
| 316 |
+
tl.store(O_ptr + offs_tok[:, None] * stride_t + offs_d[None, :], o_tile.to(tl.bfloat16))
|
| 317 |
+
LSE_ptr = LSE + bh_id * stride_lh
|
| 318 |
+
tl.store(LSE_ptr + offs_tok, lse_out)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
_hydra_fwd_kernel = _kernel_decorator(_hydra_fwd_jit)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def launch_attn_fwd(
|
| 325 |
+
q: torch.Tensor,
|
| 326 |
+
k: torch.Tensor,
|
| 327 |
+
v: torch.Tensor,
|
| 328 |
+
row_ptr: torch.Tensor,
|
| 329 |
+
col_idx: torch.Tensor,
|
| 330 |
+
seq_lens: torch.Tensor,
|
| 331 |
+
window: int = 0,
|
| 332 |
+
contiguous_offdiag: bool = True,
|
| 333 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 334 |
+
if q.dim() != 4 or k.dim() != 4 or v.dim() != 4:
|
| 335 |
+
raise ValueError(f"q/k/v must be 4D; got {q.shape} {k.shape} {v.shape}")
|
| 336 |
+
batch_size, num_heads, t_max, head_dim = q.shape
|
| 337 |
+
num_kv_heads = k.shape[1]
|
| 338 |
+
if t_max % BLOCK_SIZE != 0:
|
| 339 |
+
raise ValueError(f"T ({t_max}) must be multiple of BLOCK_SIZE ({BLOCK_SIZE})")
|
| 340 |
+
if head_dim != HEAD_DIM:
|
| 341 |
+
raise ValueError(f"head_dim ({head_dim}) must equal HEAD_DIM ({HEAD_DIM})")
|
| 342 |
+
|
| 343 |
+
batch_heads = batch_size * num_heads
|
| 344 |
+
num_q_blocks = t_max // BLOCK_SIZE
|
| 345 |
+
o = torch.empty_like(q)
|
| 346 |
+
lse_3d = torch.empty((batch_size, num_heads, t_max), device=q.device, dtype=torch.float32)
|
| 347 |
+
lse_2d = lse_3d.view(batch_heads, t_max)
|
| 348 |
+
cih = col_idx.shape[2] if col_idx.ndim == 3 else col_idx.shape[1]
|
| 349 |
+
grid = (num_q_blocks * batch_heads,)
|
| 350 |
+
|
| 351 |
+
# ASSUME_FULL is safe iff EVERY seq_len equals t_max. Cheap GPU-resident
|
| 352 |
+
# check (no D2H sync if we just compare with a fused all-equal kernel,
|
| 353 |
+
# but for now use a tiny .item() — it's a single int and seq_lens lives
|
| 354 |
+
# in HBM near the launcher already).
|
| 355 |
+
if seq_lens.numel() == 1:
|
| 356 |
+
assume_full = int(seq_lens.item()) == t_max
|
| 357 |
+
else:
|
| 358 |
+
# Multi-batch: check min==max==t_max.
|
| 359 |
+
sl_min = int(seq_lens.min().item())
|
| 360 |
+
sl_max = int(seq_lens.max().item())
|
| 361 |
+
assume_full = (sl_min == t_max) and (sl_max == t_max)
|
| 362 |
+
|
| 363 |
+
common_kwargs = dict(
|
| 364 |
+
T_MAX=t_max, NUM_HEADS=num_heads, NUM_KV_HEADS=num_kv_heads,
|
| 365 |
+
SCALE=1.0 / math.sqrt(head_dim), BS=BLOCK_SIZE, D=head_dim,
|
| 366 |
+
WINDOW=int(window),
|
| 367 |
+
CONTIGUOUS_OFFDIAG=1 if contiguous_offdiag else 0,
|
| 368 |
+
ASSUME_FULL=1 if assume_full else 0,
|
| 369 |
+
)
|
| 370 |
+
if _DISABLE_AUTOTUNE:
|
| 371 |
+
common_kwargs["num_warps"] = _FWD_NUM_WARPS
|
| 372 |
+
common_kwargs["num_stages"] = _FWD_NUM_STAGES
|
| 373 |
+
|
| 374 |
+
_hydra_fwd_kernel[grid](
|
| 375 |
+
q, k, v, o, lse_2d,
|
| 376 |
+
row_ptr, col_idx, seq_lens, cih,
|
| 377 |
+
**common_kwargs,
|
| 378 |
+
)
|
| 379 |
+
return o, lse_3d
|
torch-ext/hydra/policy.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Runtime policy hooks for live attention throttling.
|
| 2 |
+
|
| 3 |
+
This is intentionally small and deterministic. The policy can clamp an
|
| 4 |
+
existing sliding-window request before CSR construction / kernel launch. It can
|
| 5 |
+
also opt into converting dense causal attention to sliding-window attention,
|
| 6 |
+
but that is disabled by default because it changes model semantics.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from collections import deque
|
| 11 |
+
from dataclasses import asdict, dataclass
|
| 12 |
+
import math
|
| 13 |
+
import os
|
| 14 |
+
from typing import Any, Mapping
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
_MIB = 1024 * 1024
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _parse_bool(value: object, default: bool = False) -> bool:
|
| 21 |
+
if value is None:
|
| 22 |
+
return default
|
| 23 |
+
if isinstance(value, bool):
|
| 24 |
+
return value
|
| 25 |
+
text = str(value).strip().lower()
|
| 26 |
+
if text in {"1", "true", "yes", "on", "enabled"}:
|
| 27 |
+
return True
|
| 28 |
+
if text in {"0", "false", "no", "off", "disabled"}:
|
| 29 |
+
return False
|
| 30 |
+
return default
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _parse_int(value: object, default: int | None = None) -> int | None:
|
| 34 |
+
if value is None or value == "":
|
| 35 |
+
return default
|
| 36 |
+
return int(value)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _parse_int_tuple(value: object) -> tuple[int, ...] | None:
|
| 40 |
+
if value is None or value == "":
|
| 41 |
+
return None
|
| 42 |
+
if isinstance(value, str):
|
| 43 |
+
parts = [part.strip() for part in value.replace(";", ",").split(",")]
|
| 44 |
+
items = [int(part) for part in parts if part]
|
| 45 |
+
else:
|
| 46 |
+
items = [int(part) for part in value] # type: ignore[arg-type]
|
| 47 |
+
if not items:
|
| 48 |
+
return None
|
| 49 |
+
if any(item <= 0 for item in items):
|
| 50 |
+
raise ValueError(f"layer windows must be positive integers, got {value!r}")
|
| 51 |
+
return tuple(items)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _parse_float(value: object, default: float) -> float:
|
| 55 |
+
if value is None or value == "":
|
| 56 |
+
return default
|
| 57 |
+
return float(value)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _parse_bytes(value: object, default: int) -> int:
|
| 61 |
+
if value is None or value == "":
|
| 62 |
+
return default
|
| 63 |
+
text = str(value).strip().lower()
|
| 64 |
+
scale = 1
|
| 65 |
+
for suffix, multiplier in (
|
| 66 |
+
("gib", 1024**3),
|
| 67 |
+
("gb", 1000**3),
|
| 68 |
+
("mib", 1024**2),
|
| 69 |
+
("mb", 1000**2),
|
| 70 |
+
("kib", 1024),
|
| 71 |
+
("kb", 1000),
|
| 72 |
+
):
|
| 73 |
+
if text.endswith(suffix):
|
| 74 |
+
scale = multiplier
|
| 75 |
+
text = text[: -len(suffix)]
|
| 76 |
+
break
|
| 77 |
+
return int(float(text) * scale)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@dataclass(frozen=True)
|
| 81 |
+
class RuntimePolicy:
|
| 82 |
+
"""Policy for request-time attention throttling."""
|
| 83 |
+
|
| 84 |
+
mode: str = "off"
|
| 85 |
+
max_window: int | None = None
|
| 86 |
+
min_window: int = 128
|
| 87 |
+
reserve_bytes: int = 512 * _MIB
|
| 88 |
+
utilization: float = 0.85
|
| 89 |
+
allow_dense_to_window: bool = False
|
| 90 |
+
fail_closed: bool = False
|
| 91 |
+
align_to_block: bool = True
|
| 92 |
+
layer_windows: tuple[int, ...] | None = None
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def enabled(self) -> bool:
|
| 96 |
+
return self.mode not in {"", "0", "off", "disabled", "none"}
|
| 97 |
+
|
| 98 |
+
@classmethod
|
| 99 |
+
def from_env(cls) -> "RuntimePolicy":
|
| 100 |
+
raw_mode = os.environ.get("HYDRA_POLICY", "off").strip().lower()
|
| 101 |
+
if raw_mode in {"1", "true", "yes", "on", "enabled"}:
|
| 102 |
+
raw_mode = "adaptive"
|
| 103 |
+
return cls(
|
| 104 |
+
mode=raw_mode,
|
| 105 |
+
max_window=_parse_int(os.environ.get("HYDRA_MAX_WINDOW")),
|
| 106 |
+
min_window=int(os.environ.get("HYDRA_MIN_WINDOW", "128")),
|
| 107 |
+
reserve_bytes=_parse_bytes(
|
| 108 |
+
os.environ.get("HYDRA_RESERVE_BYTES"),
|
| 109 |
+
int(float(os.environ.get("HYDRA_RESERVE_MB", "512")) * _MIB),
|
| 110 |
+
),
|
| 111 |
+
utilization=_parse_float(os.environ.get("HYDRA_UTILIZATION"), 0.85),
|
| 112 |
+
allow_dense_to_window=_parse_bool(
|
| 113 |
+
os.environ.get("HYDRA_ALLOW_DENSE_THROTTLE"), False
|
| 114 |
+
),
|
| 115 |
+
fail_closed=_parse_bool(os.environ.get("HYDRA_FAIL_CLOSED"), False),
|
| 116 |
+
align_to_block=_parse_bool(os.environ.get("HYDRA_ALIGN_WINDOW"), True),
|
| 117 |
+
layer_windows=_parse_int_tuple(
|
| 118 |
+
os.environ.get("HYDRA_LAYER_WINDOWS")
|
| 119 |
+
),
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
@classmethod
|
| 123 |
+
def from_mapping(cls, data: Mapping[str, Any]) -> "RuntimePolicy":
|
| 124 |
+
return cls(
|
| 125 |
+
mode=str(data.get("mode", "adaptive")).strip().lower(),
|
| 126 |
+
max_window=_parse_int(data.get("max_window")),
|
| 127 |
+
min_window=int(data.get("min_window", 128)),
|
| 128 |
+
reserve_bytes=_parse_bytes(data.get("reserve_bytes"), 512 * _MIB),
|
| 129 |
+
utilization=float(data.get("utilization", 0.85)),
|
| 130 |
+
allow_dense_to_window=_parse_bool(data.get("allow_dense_to_window"), False),
|
| 131 |
+
fail_closed=_parse_bool(data.get("fail_closed"), False),
|
| 132 |
+
align_to_block=_parse_bool(data.get("align_to_block"), True),
|
| 133 |
+
layer_windows=_parse_int_tuple(data.get("layer_windows")),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
def to_mapping(self) -> dict[str, Any]:
|
| 137 |
+
return asdict(self)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@dataclass(frozen=True)
|
| 141 |
+
class PolicyDecision:
|
| 142 |
+
enabled: bool
|
| 143 |
+
action: str
|
| 144 |
+
reason: str
|
| 145 |
+
requested_window: int | None
|
| 146 |
+
effective_window: int | None
|
| 147 |
+
estimated_bytes: int | None = None
|
| 148 |
+
budget_bytes: int | None = None
|
| 149 |
+
free_bytes: int | None = None
|
| 150 |
+
total_bytes: int | None = None
|
| 151 |
+
layer_idx: int | None = None
|
| 152 |
+
layer_window: int | None = None
|
| 153 |
+
|
| 154 |
+
def to_mapping(self) -> dict[str, Any]:
|
| 155 |
+
return asdict(self)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
_POLICY_OVERRIDE: RuntimePolicy | None = None
|
| 159 |
+
_HISTORY: deque[PolicyDecision] = deque(maxlen=64)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def set_runtime_policy(policy: RuntimePolicy | Mapping[str, Any] | None) -> None:
|
| 163 |
+
"""Install a process-local policy override.
|
| 164 |
+
|
| 165 |
+
Passing ``None`` returns policy selection to environment variables.
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
global _POLICY_OVERRIDE
|
| 169 |
+
if policy is None:
|
| 170 |
+
_POLICY_OVERRIDE = None
|
| 171 |
+
elif isinstance(policy, RuntimePolicy):
|
| 172 |
+
_POLICY_OVERRIDE = policy
|
| 173 |
+
else:
|
| 174 |
+
_POLICY_OVERRIDE = RuntimePolicy.from_mapping(policy)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def get_runtime_policy() -> RuntimePolicy:
|
| 178 |
+
return _POLICY_OVERRIDE or RuntimePolicy.from_env()
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def last_policy_decision() -> PolicyDecision | None:
|
| 182 |
+
return _HISTORY[-1] if _HISTORY else None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def policy_history() -> list[dict[str, Any]]:
|
| 186 |
+
return [item.to_mapping() for item in _HISTORY]
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _record(decision: PolicyDecision) -> PolicyDecision:
|
| 190 |
+
if decision.enabled:
|
| 191 |
+
_HISTORY.append(decision)
|
| 192 |
+
return decision
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def estimate_attention_bytes(
|
| 196 |
+
*,
|
| 197 |
+
batch_size: int,
|
| 198 |
+
query_heads: int,
|
| 199 |
+
query_tokens: int,
|
| 200 |
+
key_tokens: int,
|
| 201 |
+
head_dim: int,
|
| 202 |
+
dtype_bytes: int,
|
| 203 |
+
block_size: int,
|
| 204 |
+
sliding_window: int | None,
|
| 205 |
+
decode: bool = False,
|
| 206 |
+
) -> int:
|
| 207 |
+
"""Conservative estimate of extra bytes allocated by this attention call."""
|
| 208 |
+
|
| 209 |
+
output = batch_size * query_heads * query_tokens * head_dim * dtype_bytes
|
| 210 |
+
lse = batch_size * query_heads * query_tokens * 4
|
| 211 |
+
if decode:
|
| 212 |
+
return output + lse
|
| 213 |
+
|
| 214 |
+
q_blocks = math.ceil(query_tokens / block_size)
|
| 215 |
+
k_blocks = math.ceil(key_tokens / block_size)
|
| 216 |
+
row_ptr = batch_size * query_heads * (q_blocks + 1) * 4
|
| 217 |
+
seq_lens = batch_size * 4
|
| 218 |
+
|
| 219 |
+
if sliding_window is None or sliding_window >= key_tokens:
|
| 220 |
+
nnz_per_head = q_blocks * (q_blocks + 1) // 2
|
| 221 |
+
else:
|
| 222 |
+
blocks_per_row = min(k_blocks, math.ceil(sliding_window / block_size) + 1)
|
| 223 |
+
nnz_per_head = q_blocks * blocks_per_row
|
| 224 |
+
col_idx = batch_size * query_heads * nnz_per_head * 4
|
| 225 |
+
return output + lse + row_ptr + col_idx + seq_lens
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _cuda_mem_info(device) -> tuple[int | None, int | None]:
|
| 229 |
+
if getattr(device, "type", None) != "cuda":
|
| 230 |
+
return None, None
|
| 231 |
+
try:
|
| 232 |
+
import torch
|
| 233 |
+
|
| 234 |
+
free, total = torch.cuda.mem_get_info(device)
|
| 235 |
+
return int(free), int(total)
|
| 236 |
+
except Exception:
|
| 237 |
+
return None, None
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _budget(policy: RuntimePolicy, free_bytes: int | None) -> int | None:
|
| 241 |
+
if free_bytes is None:
|
| 242 |
+
return None
|
| 243 |
+
usable = max(0, free_bytes - policy.reserve_bytes)
|
| 244 |
+
return int(usable * policy.utilization)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _align_window(window: int, block_size: int, policy: RuntimePolicy) -> int:
|
| 248 |
+
window = max(policy.min_window, window)
|
| 249 |
+
if policy.align_to_block:
|
| 250 |
+
window = max(block_size, (window // block_size) * block_size)
|
| 251 |
+
return window
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def _layer_window_for(
|
| 255 |
+
policy: RuntimePolicy, layer_idx: int | None, block_size: int
|
| 256 |
+
) -> int | None:
|
| 257 |
+
if layer_idx is None or not policy.layer_windows:
|
| 258 |
+
return None
|
| 259 |
+
if layer_idx < 0:
|
| 260 |
+
return None
|
| 261 |
+
raw_window = policy.layer_windows[layer_idx % len(policy.layer_windows)]
|
| 262 |
+
return _align_window(raw_window, block_size, policy)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def _fit_window_for_budget(
|
| 266 |
+
*,
|
| 267 |
+
budget_bytes: int,
|
| 268 |
+
fixed_bytes: int,
|
| 269 |
+
batch_size: int,
|
| 270 |
+
query_heads: int,
|
| 271 |
+
query_tokens: int,
|
| 272 |
+
block_size: int,
|
| 273 |
+
policy: RuntimePolicy,
|
| 274 |
+
) -> int:
|
| 275 |
+
q_blocks = math.ceil(query_tokens / block_size)
|
| 276 |
+
bytes_per_block_per_row = batch_size * query_heads * q_blocks * 4
|
| 277 |
+
if bytes_per_block_per_row <= 0:
|
| 278 |
+
return policy.min_window
|
| 279 |
+
remaining = max(0, budget_bytes - fixed_bytes)
|
| 280 |
+
# ``fixed_bytes`` is estimated with a one-token window, which still maps to
|
| 281 |
+
# two CSR blocks per row: one reach block plus the diagonal block. Add any
|
| 282 |
+
# remaining block budget to that floor, then subtract the diagonal to get
|
| 283 |
+
# token-window reach.
|
| 284 |
+
blocks_per_row = 2 + max(0, remaining // bytes_per_block_per_row)
|
| 285 |
+
# CSR includes the diagonal block in addition to window reach.
|
| 286 |
+
window_blocks = max(1, int(blocks_per_row) - 1)
|
| 287 |
+
return _align_window(window_blocks * block_size, block_size, policy)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def apply_runtime_policy(
|
| 291 |
+
q,
|
| 292 |
+
k,
|
| 293 |
+
v,
|
| 294 |
+
*,
|
| 295 |
+
sliding_window: int | None,
|
| 296 |
+
block_size: int,
|
| 297 |
+
head_dim: int,
|
| 298 |
+
layer_idx: int | None = None,
|
| 299 |
+
) -> PolicyDecision:
|
| 300 |
+
"""Apply the active policy and return the effective sliding window."""
|
| 301 |
+
|
| 302 |
+
policy = get_runtime_policy()
|
| 303 |
+
requested = sliding_window
|
| 304 |
+
if not policy.enabled:
|
| 305 |
+
return PolicyDecision(False, "off", "policy disabled", requested, requested)
|
| 306 |
+
|
| 307 |
+
batch_size, query_heads, query_tokens, dim = q.shape
|
| 308 |
+
key_tokens = k.shape[2]
|
| 309 |
+
decode = query_tokens == 1
|
| 310 |
+
dtype_bytes = q.element_size()
|
| 311 |
+
free, total = _cuda_mem_info(q.device)
|
| 312 |
+
budget = _budget(policy, free)
|
| 313 |
+
effective = requested
|
| 314 |
+
layer_window = _layer_window_for(policy, layer_idx, block_size)
|
| 315 |
+
action = "pass"
|
| 316 |
+
reason = "within policy"
|
| 317 |
+
|
| 318 |
+
if policy.max_window is not None:
|
| 319 |
+
max_window = _align_window(policy.max_window, block_size, policy)
|
| 320 |
+
if effective is None and policy.allow_dense_to_window:
|
| 321 |
+
effective = max_window
|
| 322 |
+
action = "dense_to_window"
|
| 323 |
+
reason = f"dense attention converted to sw={effective}"
|
| 324 |
+
elif effective is not None and effective > max_window:
|
| 325 |
+
effective = max_window
|
| 326 |
+
action = "clamp_max_window"
|
| 327 |
+
reason = f"sliding_window clamped to max_window={effective}"
|
| 328 |
+
|
| 329 |
+
if layer_window is not None:
|
| 330 |
+
if effective is None and policy.allow_dense_to_window:
|
| 331 |
+
effective = layer_window
|
| 332 |
+
action = "layer_window"
|
| 333 |
+
reason = f"dense attention converted to layer_window={effective}"
|
| 334 |
+
elif effective is not None and effective > layer_window:
|
| 335 |
+
effective = layer_window
|
| 336 |
+
action = "layer_window"
|
| 337 |
+
reason = f"sliding_window clamped to layer_window={effective}"
|
| 338 |
+
|
| 339 |
+
estimate = estimate_attention_bytes(
|
| 340 |
+
batch_size=batch_size,
|
| 341 |
+
query_heads=query_heads,
|
| 342 |
+
query_tokens=query_tokens,
|
| 343 |
+
key_tokens=key_tokens,
|
| 344 |
+
head_dim=dim,
|
| 345 |
+
dtype_bytes=dtype_bytes,
|
| 346 |
+
block_size=block_size,
|
| 347 |
+
sliding_window=effective,
|
| 348 |
+
decode=decode,
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
if policy.mode == "adaptive" and budget is not None and estimate > budget and not decode:
|
| 352 |
+
fixed = estimate_attention_bytes(
|
| 353 |
+
batch_size=batch_size,
|
| 354 |
+
query_heads=query_heads,
|
| 355 |
+
query_tokens=query_tokens,
|
| 356 |
+
key_tokens=key_tokens,
|
| 357 |
+
head_dim=dim,
|
| 358 |
+
dtype_bytes=dtype_bytes,
|
| 359 |
+
block_size=block_size,
|
| 360 |
+
sliding_window=1,
|
| 361 |
+
decode=False,
|
| 362 |
+
)
|
| 363 |
+
if effective is None and policy.allow_dense_to_window:
|
| 364 |
+
effective = policy.max_window or key_tokens
|
| 365 |
+
if effective is not None:
|
| 366 |
+
fit_window = _fit_window_for_budget(
|
| 367 |
+
budget_bytes=budget,
|
| 368 |
+
fixed_bytes=fixed,
|
| 369 |
+
batch_size=batch_size,
|
| 370 |
+
query_heads=query_heads,
|
| 371 |
+
query_tokens=query_tokens,
|
| 372 |
+
block_size=block_size,
|
| 373 |
+
policy=policy,
|
| 374 |
+
)
|
| 375 |
+
if fit_window < effective:
|
| 376 |
+
effective = fit_window
|
| 377 |
+
action = "budget_throttle"
|
| 378 |
+
reason = f"estimated attention bytes exceeded budget; sw={effective}"
|
| 379 |
+
estimate = estimate_attention_bytes(
|
| 380 |
+
batch_size=batch_size,
|
| 381 |
+
query_heads=query_heads,
|
| 382 |
+
query_tokens=query_tokens,
|
| 383 |
+
key_tokens=key_tokens,
|
| 384 |
+
head_dim=dim,
|
| 385 |
+
dtype_bytes=dtype_bytes,
|
| 386 |
+
block_size=block_size,
|
| 387 |
+
sliding_window=effective,
|
| 388 |
+
decode=False,
|
| 389 |
+
)
|
| 390 |
+
elif policy.fail_closed:
|
| 391 |
+
decision = PolicyDecision(
|
| 392 |
+
True,
|
| 393 |
+
"reject",
|
| 394 |
+
"dense attention exceeds policy budget and dense throttling is disabled",
|
| 395 |
+
requested,
|
| 396 |
+
effective,
|
| 397 |
+
estimate,
|
| 398 |
+
budget,
|
| 399 |
+
free,
|
| 400 |
+
total,
|
| 401 |
+
layer_idx,
|
| 402 |
+
layer_window,
|
| 403 |
+
)
|
| 404 |
+
_record(decision)
|
| 405 |
+
raise MemoryError(decision.reason)
|
| 406 |
+
|
| 407 |
+
if policy.fail_closed and budget is not None and estimate > budget:
|
| 408 |
+
decision = PolicyDecision(
|
| 409 |
+
True,
|
| 410 |
+
"reject",
|
| 411 |
+
"attention estimate still exceeds policy budget after throttling",
|
| 412 |
+
requested,
|
| 413 |
+
effective,
|
| 414 |
+
estimate,
|
| 415 |
+
budget,
|
| 416 |
+
free,
|
| 417 |
+
total,
|
| 418 |
+
layer_idx,
|
| 419 |
+
layer_window,
|
| 420 |
+
)
|
| 421 |
+
_record(decision)
|
| 422 |
+
raise MemoryError(decision.reason)
|
| 423 |
+
|
| 424 |
+
return _record(
|
| 425 |
+
PolicyDecision(
|
| 426 |
+
True,
|
| 427 |
+
action,
|
| 428 |
+
reason,
|
| 429 |
+
requested,
|
| 430 |
+
effective,
|
| 431 |
+
estimate,
|
| 432 |
+
budget,
|
| 433 |
+
free,
|
| 434 |
+
total,
|
| 435 |
+
layer_idx,
|
| 436 |
+
layer_window,
|
| 437 |
+
)
|
| 438 |
+
)
|
torch-ext/hydra/sw_sinks_csr.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sliding-window + attention-sinks CSR builder.
|
| 2 |
+
|
| 3 |
+
StreamingLLM (Xiao et al. 2023, arXiv:2309.17453) showed that always keeping
|
| 4 |
+
the first N tokens ("attention sinks") visible recovers semantic quality
|
| 5 |
+
when a non-SW-trained LM is forced into a sliding-window attention regime.
|
| 6 |
+
Without sinks, the softmax distribution loses the anchor it implicitly
|
| 7 |
+
deposits on the first tokens during pretraining; with sinks, models like
|
| 8 |
+
Qwen2.5-Coder / Qwen3 stay coherent at long context under SW=4096.
|
| 9 |
+
|
| 10 |
+
This builder produces the same ``(row_ptr, col_idx, seq_lens)`` triple as
|
| 11 |
+
``build_sliding_window_csr`` but for each Q-block additionally includes the
|
| 12 |
+
first ``ceil(sinks / BLOCK_K)`` K-blocks. Deduplicated when the SW range
|
| 13 |
+
already covers the sinks. Per-row col_idx is sorted ascending with the
|
| 14 |
+
diagonal block as the last entry (kernel convention — see ``csr.py``).
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import math
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from .csr import _broadcast_csr
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def build_sw_sinks_csr(
|
| 26 |
+
window: int,
|
| 27 |
+
seq_len: int,
|
| 28 |
+
block_size: int,
|
| 29 |
+
batch_size: int,
|
| 30 |
+
num_heads: int,
|
| 31 |
+
device: torch.device | str,
|
| 32 |
+
sinks: int = 4,
|
| 33 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 34 |
+
"""Sliding-window + first-N-sinks causal CSR.
|
| 35 |
+
|
| 36 |
+
Same shape contract as ``build_sliding_window_csr``: returns
|
| 37 |
+
``(row_ptr, col_idx, seq_lens)`` broadcast across (B, H).
|
| 38 |
+
|
| 39 |
+
For Q-block ``qb``:
|
| 40 |
+
- Off-diagonal SW range (block ids): ``[k_min_sw, qb)`` where
|
| 41 |
+
``k_min_sw = max(0, (qb*BS - window + 1)) // BS`` if positive
|
| 42 |
+
else 0.
|
| 43 |
+
- PLUS sink K-blocks: ``[0, sinks_blocks)`` with
|
| 44 |
+
``sinks_blocks = ceil(sinks / BS)``.
|
| 45 |
+
- PLUS diagonal placeholder ``qb`` (always the last entry).
|
| 46 |
+
|
| 47 |
+
The union is deduplicated and sorted ascending; the diagonal is
|
| 48 |
+
appended last per the kernel's row-walker contract.
|
| 49 |
+
|
| 50 |
+
Edge cases:
|
| 51 |
+
- sinks <= 0 OR sinks_blocks == 0: degrades to plain SW.
|
| 52 |
+
- sinks_blocks >= num_q_blocks: degrades to dense causal.
|
| 53 |
+
- window covers the sinks (qb*BS < window): sinks already
|
| 54 |
+
included; dedup makes this a no-op.
|
| 55 |
+
"""
|
| 56 |
+
if window <= 0:
|
| 57 |
+
raise ValueError(f"window must be positive, got {window}")
|
| 58 |
+
if block_size <= 0:
|
| 59 |
+
raise ValueError(f"block_size must be positive, got {block_size}")
|
| 60 |
+
if sinks < 0:
|
| 61 |
+
raise ValueError(f"sinks must be non-negative, got {sinks}")
|
| 62 |
+
|
| 63 |
+
num_q_blocks = math.ceil(seq_len / block_size)
|
| 64 |
+
sinks_blocks = math.ceil(sinks / block_size) if sinks > 0 else 0
|
| 65 |
+
# Clip so we never claim a sink block past the sequence end.
|
| 66 |
+
sinks_blocks = min(sinks_blocks, num_q_blocks)
|
| 67 |
+
|
| 68 |
+
row_ptr_row = [0]
|
| 69 |
+
col_idx_row: list[int] = []
|
| 70 |
+
for q_block in range(num_q_blocks):
|
| 71 |
+
# SW left-edge in K-block space (same logic as build_sliding_window_csr).
|
| 72 |
+
left_bound = q_block * block_size - window + 1
|
| 73 |
+
if left_bound <= 0:
|
| 74 |
+
k_min_sw = 0
|
| 75 |
+
else:
|
| 76 |
+
k_min_sw = left_bound // block_size
|
| 77 |
+
|
| 78 |
+
sw_range_end = q_block # exclusive (diagonal excluded)
|
| 79 |
+
sw_lo = min(k_min_sw, sw_range_end)
|
| 80 |
+
|
| 81 |
+
# Sink block ids that fall strictly before the diagonal AND strictly
|
| 82 |
+
# before the SW range start (anything inside SW is already covered).
|
| 83 |
+
sink_lo = 0
|
| 84 |
+
sink_hi = min(sinks_blocks, sw_lo) # exclusive
|
| 85 |
+
|
| 86 |
+
if sink_hi > sink_lo:
|
| 87 |
+
col_idx_row.extend(range(sink_lo, sink_hi))
|
| 88 |
+
col_idx_row.extend(range(sw_lo, sw_range_end))
|
| 89 |
+
# Diagonal placeholder must be the last entry per kernel contract.
|
| 90 |
+
col_idx_row.append(q_block)
|
| 91 |
+
row_ptr_row.append(len(col_idx_row))
|
| 92 |
+
|
| 93 |
+
return _broadcast_csr(row_ptr_row, col_idx_row, batch_size, num_heads, seq_len, device)
|