phase 0 baseline
Browse files- .gitignore +7 -0
- README.md +159 -7
- app.py +633 -0
- h3_split_blocks.py +51 -0
- packages.txt +1 -0
- requirements.txt +13 -0
- vsa_h3.py +444 -0
- vsa_kernel/__init__.py +16 -0
- vsa_kernel/block_sparse_attn_triton.py +879 -0
- vsa_kernel/index.py +250 -0
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# local research + bench environment, never shipped or committed
|
| 2 |
+
.research/
|
| 3 |
+
.venv-bench/
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.pyc
|
| 6 |
+
# benchmark artifacts produced per experiment (scripts stay committed)
|
| 7 |
+
.auto/last_run/
|
README.md
CHANGED
|
@@ -1,13 +1,165 @@
|
|
| 1 |
---
|
| 2 |
-
title: FastH3
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.26.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FastH3 4-step Preview (VSA)
|
| 3 |
+
emoji: 🎬
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.26.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: 4-step MiniMax-H3 sparse attention — video + audio
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 1h
|
| 12 |
+
suggested_hardware: zero-a10g
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# FastH3 4-step Preview (VSA, data-free) — MiniMax-H3 in four forward passes
|
| 16 |
+
|
| 17 |
+
[`FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree`](https://huggingface.co/FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree)
|
| 18 |
+
is a **data-free DMD2 distillation** of [`MiniMaxAI/MiniMax-H3`](https://huggingface.co/MiniMaxAI/MiniMax-H3), the
|
| 19 |
+
33B dual-modality DiT that emits a video *and* its synchronized soundtrack from one denoising pass. The student
|
| 20 |
+
keeps the teacher's architecture exactly and only replaces `transformer/`, so it is a drop-in for the same
|
| 21 |
+
`diffusers` modular pipeline — but it needs **four** DiT forwards instead of thirty, and it was distilled **with
|
| 22 |
+
Video Sparse Attention on**.
|
| 23 |
+
|
| 24 |
+
Everything runs unquantized at **bfloat16**.
|
| 25 |
+
|
| 26 |
+
## The sampling contract: five grid points, four forwards
|
| 27 |
+
|
| 28 |
+
`MiniMaxH3Scheduler.set_timesteps(n)` builds `linspace(1, 0, n)`, applies the shift
|
| 29 |
+
`σ' = s·σ / (1 + (s−1)·σ)`, and then drops the trailing zero when it forms the timesteps — so **`n` sigma grid
|
| 30 |
+
points drive `n − 1` model evaluations**. The distilled ladder is `t = 999, 749, 500, 250 → 0`: five points, four
|
| 31 |
+
forwards.
|
| 32 |
+
|
| 33 |
+
This Space therefore fixes `num_inference_steps = 5`, matching the checkpoint's own
|
| 34 |
+
`fastvideo_inference.json` (`num_inference_steps: 5`, `transformer_forwards: 4`,
|
| 35 |
+
`dmd_denoising_steps: [999, 749, 500, 250]`, `guidance_scale: 1.0`). It is not a knob. There is no CFG and no
|
| 36 |
+
negative prompt — the teacher is guidance-distilled and the student inherits that.
|
| 37 |
+
|
| 38 |
+
## Video Sparse Attention is not optional here
|
| 39 |
+
|
| 40 |
+
This checkpoint is the **VSA** variant. Its `fastvideo_inference.json` pins
|
| 41 |
+
`attention_backend: VIDEO_SPARSE_ATTN_H3`, `vsa_sparsity: 0.9`, `vsa_tile_size: 64`, and the transformer ships 50
|
| 42 |
+
trained `attn.to_gate_compress` tensors (~3.6 GiB) that only the sparse path consumes. FastVideo publishes a
|
| 43 |
+
separate `…-Dense-DataFree` checkpoint for people who want dense — running *this* one dense is running it
|
| 44 |
+
off-distribution.
|
| 45 |
+
|
| 46 |
+
The published kernel is `vsa_kernel: sm100a`, which is GB200-only, and the ZeroGPU pool is **sm120** (RTX PRO 6000
|
| 47 |
+
Blackwell). FastVideo's other officially supported route is `--vsa-kernel triton`, which is pure Triton and
|
| 48 |
+
architecture-agnostic — so this Space vendors those two files verbatim from
|
| 49 |
+
[FastVideo](https://github.com/hao-ai-lab/FastVideo) (Apache-2.0) into `vsa_kernel/` and ports the H3 backend on top
|
| 50 |
+
of them:
|
| 51 |
+
|
| 52 |
+
| File | What it is |
|
| 53 |
+
|---|---|
|
| 54 |
+
| `vsa_kernel/block_sparse_attn_triton.py` | FastVideo's Triton block-sparse attention, verbatim except that the autotune sweep is collapsed to the single config it lands on for Blackwell (re-enable it with `H3_VSA_AUTOTUNE=1`). |
|
| 55 |
+
| `vsa_kernel/index.py` | FastVideo's `map_to_index` / `topk_index_to_map`, verbatim. |
|
| 56 |
+
| `vsa_h3.py` | The port: `MiniMaxH3VSAAttnProcessor`, a `diffusers` attention processor reproducing `MiniMaxH3VSABackend`. |
|
| 57 |
+
|
| 58 |
+
`vsa_h3.py` follows FastVideo's `video_sparse_attn_h3.py` step for step: 64-token `(4, 4, 4)` tiles over the
|
| 59 |
+
post-patchify video grid, segment-pure prefix tiles, per-head fp32 pooled tile scores,
|
| 60 |
+
`topk = max(1, min(⌈(1 − sparsity)·n_video_tiles⌉, n_video_tiles))`, prefix keys exempt (always selected) and prefix
|
| 61 |
+
queries always dense, plus the compression branch `softmax(scores) @ pool(v)` broadcast back over each tile row and
|
| 62 |
+
scaled by the trained gate with **no** activation. The tile geometry is derived per-forward from the pipeline's own
|
| 63 |
+
`token_tags` / `position_ids`, so the `[text | cond | audio | video]` packing stays authoritative.
|
| 64 |
+
|
| 65 |
+
`diffusers` 0.40.0 has no `to_gate_compress`, so `vsa_h3.add_gate_compress_modules()` patches
|
| 66 |
+
`MiniMaxH3TransformerBlock.__init__` **before** the pipeline loads; otherwise the 50 gate tensors load as
|
| 67 |
+
"unexpected keys" and are silently dropped. Blocks whose gate is all-zero have it removed again after load, exactly
|
| 68 |
+
as FastVideo does.
|
| 69 |
+
|
| 70 |
+
A hidden `/selftest` API endpoint runs the ported kernel at sparsity 0 against `F.scaled_dot_product_attention` on
|
| 71 |
+
a real packed layout, so a wrong tile order or transpose is caught without spending a generation.
|
| 72 |
+
|
| 73 |
+
## Split across two Spaces
|
| 74 |
+
|
| 75 |
+
MiniMax-H3 is ~196 GiB in bfloat16 and a ZeroGPU Space is evicted at **150 GB of storage**, so no single
|
| 76 |
+
unquantized Space can hold it. Cutting `MiniMaxH3Blocks` at its `text_encoder` step splits it in two, and both
|
| 77 |
+
halves fit:
|
| 78 |
+
|
| 79 |
+
| Space | Subfolders | Download |
|
| 80 |
+
|---|---|---|
|
| 81 |
+
| [`multimodalart/qwen3vl-conditioner`](https://huggingface.co/spaces/multimodalart/qwen3vl-conditioner) | `text_encoder/` + `tokenizer/` + `processor/` | 66.7 GB → 62.15 GiB bf16 |
|
| 82 |
+
| this one | `transformer/` + `vae/` + `audio_vae/` | ~85 GB → 75.7 GiB resident |
|
| 83 |
+
|
| 84 |
+
The conditioner is a **public** Space and is unchanged by the distillation — the student's
|
| 85 |
+
`modular_model_index.json` points its `text_encoder` at the same Qwen3-VL weights — so this Space reuses it as-is
|
| 86 |
+
and calls it over the gradio API for every request.
|
| 87 |
+
|
| 88 |
+
`h3_split_blocks.py` subclasses the pipeline's blocks with the `text_encoder` step removed. Dropping the step drops
|
| 89 |
+
the components it declares, so `load_components` resolves only `transformer` / `vae` / `audio_vae` / the two
|
| 90 |
+
schedulers, and `prompt_embeds` + `text_token_tags` become ordinary required pipeline inputs. The wire format is
|
| 91 |
+
those two tensors — `(1, num_text_tokens, 5120)` bfloat16 and `(num_text_tokens,)` int64 — in one safetensors file
|
| 92 |
+
with the resolved `height` / `width` / `num_frames` in its metadata header.
|
| 93 |
+
|
| 94 |
+
## Text-to-video+audio only
|
| 95 |
+
|
| 96 |
+
The preview distills **the T2VA path only**, so this Space exposes no keyframe / reference inputs — the student's
|
| 97 |
+
`transformer_ref` tower is not packaged with the checkpoint. Image conditioning is what
|
| 98 |
+
[`multimodalart/minimax-h3`](https://huggingface.co/spaces/multimodalart/minimax-h3) (the undistilled 30-step
|
| 99 |
+
teacher) is for.
|
| 100 |
+
|
| 101 |
+
## Prompt format
|
| 102 |
+
|
| 103 |
+
MiniMax-H3 is trained on a structured multimodal caption, not a bare sentence:
|
| 104 |
+
|
| 105 |
+
```
|
| 106 |
+
integrated_multimodal_description: <shots, camera, subjects, action, lighting>
|
| 107 |
+
overall_soundscape: <diegetic sound>
|
| 108 |
+
non_diegetic_music: <score>
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
**Expand prompt** (on by default) sends a short prompt through the conditioner's Qwen3-VL prompt rewriter, which
|
| 112 |
+
writes that structure for you and returns it. Turn it off when you have already written a full-format prompt — the
|
| 113 |
+
examples that carry MiniMax's own official prompts do exactly that.
|
| 114 |
+
|
| 115 |
+
## Generation constraints
|
| 116 |
+
|
| 117 |
+
Fixed by the checkpoint: 24 fps, a 768 pixel short edge at the training canvas, `num_frames` snapped up to the next
|
| 118 |
+
`17·n + 5`. The distillation's operating point is **1344×768 × 124 frames (≈5 s)** — that is the default. The 8 s
|
| 119 |
+
slider ceiling and the smaller canvases sit outside the distilled operating point, so quality degrades gracefully
|
| 120 |
+
rather than being guaranteed. The 8 s ceiling is a VRAM limit, not a model limit: 75.7 GiB of weights sit resident
|
| 121 |
+
on a 95.0 GiB card and the sparse working set grows with sequence length.
|
| 122 |
+
|
| 123 |
+
The checkpoint itself is a **preview**.
|
| 124 |
+
|
| 125 |
+
## Latency instrumentation
|
| 126 |
+
|
| 127 |
+
Every response carries `gen_s` — the server-side wall time of the pipeline call (four transformer forwards plus the
|
| 128 |
+
two decoders), measured with `time.perf_counter()` inside the `@spaces.GPU` function — along with the per-forward
|
| 129 |
+
times and the peak CUDA allocation in the report line, and a per-request id proving each artifact is freshly
|
| 130 |
+
generated. Historical numbers below were measured on the reference deployment of this exact configuration, over
|
| 131 |
+
`gradio_client`; this Space's own numbers are in its `.auto/log.jsonl`.
|
| 132 |
+
|
| 133 |
+
| Canvas × frames | Packed rows | Video tiles kept | Denoise + decode | Peak allocated |
|
| 134 |
+
|---|---|---|---|---|
|
| 135 |
+
| 544×544 × 56 (2.3 s) | 4 913 | 10/100 | 6 s | 81.05 GiB |
|
| 136 |
+
| 1344×768 × 124 (5.2 s) | 37 296 | 66/660 | 59–72 s | 83.70 GiB |
|
| 137 |
+
| 1344×768 × 192 (8.0 s) | 57 456 | 102/1020 | 87 s | 88.00 GiB |
|
| 138 |
+
|
| 139 |
+
A cold worker pays a one-time **~68 s** (75.7 GiB across PCIe ~11 s + Triton JIT of the vendored kernels ~57 s),
|
| 140 |
+
booked only on the first request a process serves.
|
| 141 |
+
|
| 142 |
+
## Space variables
|
| 143 |
+
|
| 144 |
+
| Variable | Default | Meaning |
|
| 145 |
+
|---|---|---|
|
| 146 |
+
| `H3_MODEL_REPO` | `FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree` | The distilled diffusers-layout checkpoint. Public. |
|
| 147 |
+
| `H3_CONDITIONER` | `multimodalart/qwen3vl-conditioner` | The public Space this one asks for embeddings. |
|
| 148 |
+
| `H3_ATTENTION` | `vsa` | The ported VSA-H3 backend. Any other value is passed to `set_attention_backend` as a dense escape hatch (e.g. `_native_cudnn`). |
|
| 149 |
+
| `H3_VSA_SPARSITY` | `0.9` | The checkpoint's trained sparsity. |
|
| 150 |
+
| `H3_VSA_AUTOTUNE` | unset | Set to `1` to restore FastVideo's full Triton autotune sweep instead of the pinned Blackwell config. |
|
| 151 |
+
| `H3_VSA_WARPS` | `4` | `num_warps` of the pinned Triton block-sparse config. |
|
| 152 |
+
| `H3_PLACEMENT` | `lazy` | `lazy` moves all weights onto the card on the first GPU call; `offload` hands placement to `ComponentsManager.enable_auto_cpu_offload`. |
|
| 153 |
+
| `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` (48 GB) does not fit 75 GiB of weights. |
|
| 154 |
+
|
| 155 |
+
## Secrets
|
| 156 |
+
|
| 157 |
+
None. Every weight this Space downloads is public, and the conditioner is a public Space called on the requesting
|
| 158 |
+
user's own ZeroGPU token.
|
| 159 |
+
|
| 160 |
+
## License
|
| 161 |
+
|
| 162 |
+
The weights are under the **MiniMax H3 Community License**, inherited from the base model — it carries territory and
|
| 163 |
+
acceptable-use restrictions. Read
|
| 164 |
+
[the base model's license](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/main/LICENSE) before using outputs. The
|
| 165 |
+
vendored kernels in `vsa_kernel/` are Apache-2.0, from [hao-ai-lab/FastVideo](https://github.com/hao-ai-lab/FastVideo).
|
app.py
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastH3 v1 (VSA) — the 4-step DMD2 distillation of MiniMax-H3, text to video + synchronized audio.
|
| 2 |
+
|
| 3 |
+
`FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree` replaces only the `transformer/` of the MiniMax-H3 release
|
| 4 |
+
with a data-free DMD2 student. Everything else in the repo (Qwen3-VL conditioner, both autoencoders, both schedulers)
|
| 5 |
+
is an unmodified copy of the base checkpoint, so it runs on the released `diffusers` modular pipeline — the two
|
| 6 |
+
differences at inference time are the step count and the **attention backend**.
|
| 7 |
+
|
| 8 |
+
**The sampling contract.** `num_inference_steps` counts sigma *grid points*, and `N` points drive `N - 1` transformer
|
| 9 |
+
forwards. The checkpoint's own `fastvideo_inference.json` states it exactly: `num_inference_steps: 5`,
|
| 10 |
+
`transformer_forwards: 4`, `dmd_denoising_steps: [999, 749, 500, 250]`, `guidance_scale: 1.0`. It is fixed here.
|
| 11 |
+
|
| 12 |
+
**VSA is not optional.** That same file records `attention_backend: VIDEO_SPARSE_ATTN_H3`, `vsa_tile_size: 64`,
|
| 13 |
+
`vsa_sparsity: 0.9`. This student was distilled *under* block-sparse attention and ships 50 trained
|
| 14 |
+
`attn.to_gate_compress` tensors that only the sparse path reads, so `vsa_h3.py` ports FastVideo's VSA-H3 backend onto
|
| 15 |
+
`MiniMaxH3Attention` and runs it, on FastVideo's own Triton kernels (vendored under `vsa_kernel/`). The checkpoint's
|
| 16 |
+
`vsa_kernel: sm100a` is the GB200-only fast path for the same mask semantics; this pool is sm120.
|
| 17 |
+
|
| 18 |
+
**Why the Space is split.** MiniMax-H3 is ~196 GiB in bfloat16 and a ZeroGPU Space is evicted at 150 GB of storage.
|
| 19 |
+
This half holds the distilled transformer and the two autoencoders (81 GB); the 62.15 GiB Qwen3-VL conditioner runs in
|
| 20 |
+
`multimodalart/qwen3vl-conditioner`, which this Space calls over the gradio API for every request. FastH3 ships the
|
| 21 |
+
base release's conditioner verbatim, so that Space encodes this checkpoint exactly. Nothing is quantized anywhere.
|
| 22 |
+
|
| 23 |
+
**Latency instrumentation.** The `@spaces.GPU` function times the pipeline call alone with `time.perf_counter()` and
|
| 24 |
+
returns `gen_s` next to the video, together with per-forward wall times and the peak CUDA allocation, so the
|
| 25 |
+
autoresearch loop can see where a request spends its seconds. A per-request id in the report proves each artifact is
|
| 26 |
+
freshly generated (a cached replay would repeat the id).
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
import os
|
| 32 |
+
import tempfile
|
| 33 |
+
import time
|
| 34 |
+
import traceback
|
| 35 |
+
import uuid
|
| 36 |
+
from functools import cache
|
| 37 |
+
|
| 38 |
+
# Before anything that could initialize CUDA: `import spaces` patches `torch.cuda` so the 75 GiB load can happen at
|
| 39 |
+
# startup rather than on GPU time.
|
| 40 |
+
import spaces
|
| 41 |
+
import gradio as gr
|
| 42 |
+
|
| 43 |
+
MODEL_REPO = os.environ.get("H3_MODEL_REPO", "FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree")
|
| 44 |
+
BASE_REPO = "MiniMaxAI/MiniMax-H3"
|
| 45 |
+
CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
|
| 46 |
+
# `lazy` moves all weights onto the card on the first GPU call and leaves them there; `offload` hands placement to
|
| 47 |
+
# `ComponentsManager.enable_auto_cpu_offload`. Packing at startup is not an option here: `spaces` writes every
|
| 48 |
+
# startup-resident CUDA tensor to a second on-disk copy, and this checkpoint's transformer (70.1 GB on disk) would
|
| 49 |
+
# bust the 150 GB quota.
|
| 50 |
+
PLACEMENT = os.environ.get("H3_PLACEMENT", "lazy").lower()
|
| 51 |
+
# `vsa` is the trained route (see the module docstring). `dense` is the escape hatch: it runs the released dense
|
| 52 |
+
# `diffusers` path with cuDNN's fused kernel, which is off-distribution for this student but useful to bisect against.
|
| 53 |
+
ATTENTION = os.environ.get("H3_ATTENTION", "vsa").lower()
|
| 54 |
+
# The checkpoint's own `vsa_sparsity`. Only read when `H3_ATTENTION=vsa`.
|
| 55 |
+
VSA_SPARSITY = float(os.environ.get("H3_VSA_SPARSITY", "0.9"))
|
| 56 |
+
# 75.7 GiB of weights plus activations does not fit a `large` (48 GiB) allocation.
|
| 57 |
+
GPU_SIZE = os.environ.get("H3_GPU_SIZE", "xlarge")
|
| 58 |
+
|
| 59 |
+
# The distilled ladder, as sigma grid points. 5 points -> 4 transformer forwards at t = 1000, 750, 500, 250.
|
| 60 |
+
SIGMA_GRID_POINTS = 5
|
| 61 |
+
NUM_FORWARDS = SIGMA_GRID_POINTS - 1
|
| 62 |
+
|
| 63 |
+
# Must stay identical to the conditioner's table: the *label* goes over the wire, so a canvas that half does not know
|
| 64 |
+
# is rejected there and surfaces as a failure here.
|
| 65 |
+
CANVASES = {
|
| 66 |
+
# 16:9
|
| 67 |
+
"960x544 · 16:9 fast": (544, 960),
|
| 68 |
+
"1024x576 · 16:9 fast": (576, 1024),
|
| 69 |
+
"1152x640 · 16:9": (640, 1152),
|
| 70 |
+
"1280x704 · 16:9": (704, 1280),
|
| 71 |
+
"1344x768 · 16:9 full": (768, 1344),
|
| 72 |
+
# 9:16
|
| 73 |
+
"544x960 · 9:16 fast": (960, 544),
|
| 74 |
+
"640x1152 · 9:16": (1152, 640),
|
| 75 |
+
"768x1344 · 9:16 full": (1344, 768),
|
| 76 |
+
# 1:1
|
| 77 |
+
"544x544 · 1:1 fast": (544, 544),
|
| 78 |
+
"768x768 · 1:1 full": (768, 768),
|
| 79 |
+
"1024x1024 · 1:1 max": (1024, 1024),
|
| 80 |
+
# 4:3 / 3:4
|
| 81 |
+
"768x576 · 4:3 fast": (576, 768),
|
| 82 |
+
"1024x768 · 4:3 full": (768, 1024),
|
| 83 |
+
"576x768 · 3:4 fast": (768, 576),
|
| 84 |
+
"768x1024 · 3:4 full": (1024, 768),
|
| 85 |
+
# 21:9
|
| 86 |
+
"1152x512 · 21:9 fast": (512, 1152),
|
| 87 |
+
"1536x672 · 21:9 full": (672, 1536),
|
| 88 |
+
}
|
| 89 |
+
# The distillation's own operating point: 768x1344, 124 frames, 24 fps.
|
| 90 |
+
DEFAULT_CANVAS = "1344x768 · 16:9 full"
|
| 91 |
+
DEFAULT_DURATION = 5
|
| 92 |
+
FPS, FRAMES_PER_CHUNK, LATENTS_PER_CHUNK = 24, 17, 5
|
| 93 |
+
# The ceiling holds for the *snapped* frame count. 75.74 GiB of weights are resident on a 95.0 GiB card, and the
|
| 94 |
+
# sparse path's own working set grows with the packed sequence, so this is a memory ceiling, not a policy one.
|
| 95 |
+
MIN_UI_DURATION, MAX_UI_DURATION = 2, 8
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# The frozen benchmark request. One fixed prompt, one fixed seed, the trained 4-forward schedule, the distilled
|
| 99 |
+
# operating point (1344x768, 124 frames, 24 fps), audio on (the pipeline always emits it). `bench/bench_client.py`
|
| 100 |
+
# duplicates this table; keep the two in sync.
|
| 101 |
+
BENCH_PROMPT = (
|
| 102 |
+
"integrated_multimodal_description: [Shot 1] Live-action, cinematic, a medium-wide shot frames a baker opening "
|
| 103 |
+
"the shutters of a small street bakery before sunrise. The camera pushes in with small amplitude at slow speed "
|
| 104 |
+
"as the middle-aged baker with a calm, slightly raspy voice (S1) places a fresh loaf on the wooden counter and "
|
| 105 |
+
"says: <d>[English] First batch of the morning.</d> [Shot 2] At 00:05.000, the camera cuts to a close-up of "
|
| 106 |
+
"steam rising from the sliced bread while the baker's final words carry over from the previous shot.\n\n"
|
| 107 |
+
"overall_soundscape: Wooden shutters scrape open over a quiet street as trays clink softly inside the bakery. "
|
| 108 |
+
"The doorbell rings once, followed by light footsteps and the crisp sound of bread being sliced.\n\n"
|
| 109 |
+
"non_diegetic_music: A soft acoustic-guitar pattern at a moderate tempo, joined by sparse upright-bass notes and "
|
| 110 |
+
"a gentle fade at the end."
|
| 111 |
+
)
|
| 112 |
+
BENCH_CANVAS = "1344x768 · 16:9 full"
|
| 113 |
+
BENCH_DURATION = 5
|
| 114 |
+
BENCH_SEED = 42
|
| 115 |
+
BENCH_UPSAMPLE = False # the prompt is already in the trained format
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def snap_frames(seconds: float) -> int:
|
| 119 |
+
"""The frame count MiniMax-H3's video VAE can decode: the next `17 * n + 5` at 24 fps."""
|
| 120 |
+
frames = max(1, round(float(seconds) * FPS))
|
| 121 |
+
while frames % FRAMES_PER_CHUNK != LATENTS_PER_CHUNK:
|
| 122 |
+
frames += 1
|
| 123 |
+
return frames
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def lower_duration_floor(seconds: float = MIN_UI_DURATION) -> None:
|
| 127 |
+
"""Let the pipeline generate below its 5 s floor. 56 frames (2.33 s) is fine on the released checkpoint."""
|
| 128 |
+
from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline
|
| 129 |
+
|
| 130 |
+
MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(seconds))
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
PIPE = None
|
| 134 |
+
MANAGER = None
|
| 135 |
+
LOAD_ERROR: str | None = None
|
| 136 |
+
LOADED_IN: float | None = None
|
| 137 |
+
VSA_BLOCKS = 0
|
| 138 |
+
VSA_GATES = 0
|
| 139 |
+
# Wall seconds of the last request's transformer forwards (reset by every `_generate` call).
|
| 140 |
+
FORWARD_TIMES: list[float] = []
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def status() -> str:
|
| 144 |
+
if LOAD_ERROR:
|
| 145 |
+
return LOAD_ERROR
|
| 146 |
+
if PIPE is None:
|
| 147 |
+
return f"Loading `{MODEL_REPO}` (transformer + VAEs, 81 GB). Watch the Space logs."
|
| 148 |
+
if ATTENTION == "vsa":
|
| 149 |
+
attention = (
|
| 150 |
+
f"**VSA-H3** block-sparse, tile 64 / sparsity {VSA_SPARSITY:g} on {VSA_BLOCKS} blocks "
|
| 151 |
+
f"({VSA_GATES} trained compression gates live)"
|
| 152 |
+
)
|
| 153 |
+
else:
|
| 154 |
+
attention = f"dense `{ATTENTION}` (off-distribution for this student)"
|
| 155 |
+
return (
|
| 156 |
+
f"Ready · distilled transformer + VAEs **bfloat16, unquantized** · {NUM_FORWARDS} transformer forwards "
|
| 157 |
+
f"({SIGMA_GRID_POINTS}-point sigma grid) · attention {attention} · placement `{PLACEMENT}` · "
|
| 158 |
+
f"loaded in {LOADED_IN:.0f}s · conditioner `{CONDITIONER_SPACE}`"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def load_models() -> str | None:
|
| 163 |
+
"""Load the denoising half at startup.
|
| 164 |
+
|
| 165 |
+
`MiniMaxH3GeneratorBlocks` declares `transformer`, `vae`, `audio_vae`, the two schedulers and `video_processor`,
|
| 166 |
+
so `load_components` fetches exactly those subfolders — `text_encoder/` and `transformer_ref/` are never touched.
|
| 167 |
+
Both autoencoders carry `_keep_in_fp32_modules` over every module and stay float32: a bfloat16 audio VAE decodes
|
| 168 |
+
the soundtrack roughly 20 dB too quiet.
|
| 169 |
+
|
| 170 |
+
`add_gate_compress_modules()` has to run *before* the transformer is instantiated. The checkpoint carries 50
|
| 171 |
+
`transformer_blocks.*.attn.to_gate_compress.weight` tensors — the trained VSA compression gate — and stock
|
| 172 |
+
`MiniMaxH3Attention` does not declare the module, so `from_pretrained` would report them as unexpected and drop
|
| 173 |
+
them. Declaring it first is what makes them load.
|
| 174 |
+
"""
|
| 175 |
+
global PIPE, MANAGER, LOAD_ERROR, LOADED_IN, VSA_BLOCKS, VSA_GATES
|
| 176 |
+
|
| 177 |
+
if PIPE is not None or LOAD_ERROR is not None:
|
| 178 |
+
return LOAD_ERROR
|
| 179 |
+
|
| 180 |
+
started = time.time()
|
| 181 |
+
try:
|
| 182 |
+
import torch
|
| 183 |
+
from diffusers import ComponentsManager
|
| 184 |
+
|
| 185 |
+
from h3_split_blocks import MiniMaxH3GeneratorBlocks
|
| 186 |
+
|
| 187 |
+
lower_duration_floor()
|
| 188 |
+
if ATTENTION == "vsa":
|
| 189 |
+
import vsa_h3
|
| 190 |
+
|
| 191 |
+
vsa_h3.add_gate_compress_modules()
|
| 192 |
+
|
| 193 |
+
manager = ComponentsManager()
|
| 194 |
+
blocks = MiniMaxH3GeneratorBlocks()
|
| 195 |
+
print(f"[gen] loading {[c.name for c in blocks.expected_components]} from {MODEL_REPO} ...", flush=True)
|
| 196 |
+
pipe = blocks.init_pipeline(MODEL_REPO, components_manager=manager, collection="fasth3")
|
| 197 |
+
pipe.load_components(dtype=torch.bfloat16)
|
| 198 |
+
|
| 199 |
+
if ATTENTION == "vsa":
|
| 200 |
+
VSA_BLOCKS, VSA_GATES = vsa_h3.install(pipe.transformer, sparsity=VSA_SPARSITY)
|
| 201 |
+
print(f"[gen] VSA-H3 on {VSA_BLOCKS} blocks, {VSA_GATES} trained gates", flush=True)
|
| 202 |
+
else:
|
| 203 |
+
pipe.transformer.set_attention_backend(ATTENTION)
|
| 204 |
+
|
| 205 |
+
_install_forward_timing(pipe)
|
| 206 |
+
|
| 207 |
+
if PLACEMENT == "offload":
|
| 208 |
+
manager.enable_auto_cpu_offload(device="cuda")
|
| 209 |
+
_arm_decode_hooks(pipe)
|
| 210 |
+
|
| 211 |
+
PIPE, MANAGER = pipe, manager
|
| 212 |
+
LOADED_IN = time.time() - started
|
| 213 |
+
print(f"[gen] ready in {LOADED_IN:.0f}s", flush=True)
|
| 214 |
+
except Exception as error:
|
| 215 |
+
traceback.print_exc()
|
| 216 |
+
LOAD_ERROR = (
|
| 217 |
+
f"**Loading `{MODEL_REPO}` failed** after {time.time() - started:.0f}s: "
|
| 218 |
+
f"`{type(error).__name__}: {error}`"
|
| 219 |
+
)
|
| 220 |
+
return LOAD_ERROR
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _install_forward_timing(pipe) -> None:
|
| 224 |
+
"""Record the wall time of every transformer forward, so each request can report where its seconds went.
|
| 225 |
+
|
| 226 |
+
Runs *after* `vsa_h3.install`, so the timing wrapper sits outside the layout-publishing wrapper.
|
| 227 |
+
"""
|
| 228 |
+
original_forward = pipe.transformer.forward
|
| 229 |
+
|
| 230 |
+
def timed_forward(*args, **kwargs):
|
| 231 |
+
started = time.perf_counter()
|
| 232 |
+
try:
|
| 233 |
+
return original_forward(*args, **kwargs)
|
| 234 |
+
finally:
|
| 235 |
+
FORWARD_TIMES.append(time.perf_counter() - started)
|
| 236 |
+
|
| 237 |
+
pipe.transformer.forward = timed_forward
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _arm_decode_hooks(pipe):
|
| 241 |
+
"""Make the offload hooks fire for the two VAEs.
|
| 242 |
+
|
| 243 |
+
`enable_auto_cpu_offload` wraps `forward`, and the decode blocks call `vae.decode(...)` directly, so the hook
|
| 244 |
+
never runs and the VAE is still on the host when the latents arrive on the card.
|
| 245 |
+
"""
|
| 246 |
+
for name in ("vae", "audio_vae"):
|
| 247 |
+
module = getattr(pipe, name)
|
| 248 |
+
inner = module.decode
|
| 249 |
+
|
| 250 |
+
def armed(*args, _module=module, _decode=inner, **kwargs):
|
| 251 |
+
hook = getattr(_module, "_hf_hook", None)
|
| 252 |
+
if hook is not None:
|
| 253 |
+
hook.pre_forward(_module)
|
| 254 |
+
return _decode(*args, **kwargs)
|
| 255 |
+
|
| 256 |
+
module.decode = armed
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
@cache
|
| 260 |
+
def conditioner():
|
| 261 |
+
"""The other half, over the gradio API. `gradio_client` attaches the caller's own ZeroGPU token per call, so the
|
| 262 |
+
conditioner's booking is billed to whoever asked for the video."""
|
| 263 |
+
from gradio_client import Client
|
| 264 |
+
|
| 265 |
+
return Client(CONDITIONER_SPACE)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def encode_remote(prompt: str, canvas: str, num_frames: int, rewrite_prompt: bool = False):
|
| 269 |
+
"""`/encode` on the conditioner Space: a safetensors file holding `prompt_embeds` + `text_token_tags`, with the
|
| 270 |
+
resolved `height` / `width` / `num_frames` in its metadata, plus the plan. `canvas` is the label."""
|
| 271 |
+
from safetensors import safe_open
|
| 272 |
+
|
| 273 |
+
path, plan = conditioner().predict(
|
| 274 |
+
prompt=prompt,
|
| 275 |
+
image_path=None,
|
| 276 |
+
last_image_path=None,
|
| 277 |
+
canvas=canvas,
|
| 278 |
+
num_frames=num_frames,
|
| 279 |
+
rewrite_prompt=bool(rewrite_prompt),
|
| 280 |
+
api_name="/encode",
|
| 281 |
+
)
|
| 282 |
+
with safe_open(path, framework="pt") as handle:
|
| 283 |
+
return handle.get_tensor("prompt_embeds"), handle.get_tensor("text_token_tags"), handle.metadata(), plan
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# Seconds of GPU one request needs, from the packed rows it is about to denoise. Block-sparse attention keeps a fixed
|
| 287 |
+
# 10% of the tiles, so the cost is linear in the rows and the quadratic term a dense path needs is gone. Measured
|
| 288 |
+
# warm on this Space: 37 296 rows in 59-72 s and 57 456 rows in 87 s, i.e. 1.51e-3 to 1.93e-3 s/row depending on how
|
| 289 |
+
# fast a slice of the pool the request lands on. Fitted to the *slow* end so a slow slice is not aborted mid-video.
|
| 290 |
+
_DUR_A, _DUR_BASE = 1.95e-3, 3.0
|
| 291 |
+
# The one-time costs a cold worker pays inside its first request: 75.7 GiB across PCIe (~11 s) plus the Triton JIT of
|
| 292 |
+
# the vendored block-sparse kernels (~57 s), measured at 74 s against 6 s warm for the same request. Booking that on
|
| 293 |
+
# *every* request would burn 70 s of each visitor's quota for nothing, so it is only booked while this process has
|
| 294 |
+
# not yet seen a request come back.
|
| 295 |
+
_COLD_ALLOWANCE, _WARM_ALLOWANCE = 75, 8
|
| 296 |
+
_WARM = False
|
| 297 |
+
# Booked over the estimate. Keep it small: an inflated duration burns the visitor's quota and drops queue priority.
|
| 298 |
+
_MARGIN = 1.15
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def get_duration(prompt_embeds, text_token_tags, height, width, num_frames, seed, *a, **k):
|
| 302 |
+
height, width, num_frames = int(height), int(width), int(num_frames)
|
| 303 |
+
latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2
|
| 304 |
+
rows = latent_frames * (height // 32) * (width // 32)
|
| 305 |
+
allowance = _WARM_ALLOWANCE if _WARM else _COLD_ALLOWANCE
|
| 306 |
+
return max(60, int((_DUR_A * rows + _DUR_BASE + allowance) * _MARGIN) + 2)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@spaces.GPU(duration=get_duration, size=GPU_SIZE)
|
| 310 |
+
def _generate(prompt_embeds, text_token_tags, height: int, width: int, num_frames: int, seed: int):
|
| 311 |
+
"""The only thing on GPU time: the four-forward packed-sequence denoise loop and the two decoders.
|
| 312 |
+
|
| 313 |
+
`gen_s` wraps the pipeline call alone — the four transformer forwards plus the two decoders — measured inside the
|
| 314 |
+
GPU worker with `time.perf_counter()`, and travels back to the client next to the video. The per-forward wall
|
| 315 |
+
times and the peak CUDA allocation ride along for profiling.
|
| 316 |
+
|
| 317 |
+
Only the generated outputs come back — a `@spaces.GPU` return crosses a process boundary by pickling, and the
|
| 318 |
+
full `PipelineState` still holds the packed latents, the rotary grid and the row indices on the card.
|
| 319 |
+
"""
|
| 320 |
+
import torch
|
| 321 |
+
|
| 322 |
+
if PLACEMENT == "lazy":
|
| 323 |
+
PIPE.to("cuda")
|
| 324 |
+
|
| 325 |
+
torch.cuda.reset_peak_memory_stats()
|
| 326 |
+
FORWARD_TIMES.clear()
|
| 327 |
+
started = time.perf_counter()
|
| 328 |
+
state = PIPE(
|
| 329 |
+
prompt_embeds=prompt_embeds.to("cuda"),
|
| 330 |
+
text_token_tags=text_token_tags,
|
| 331 |
+
height=height,
|
| 332 |
+
width=width,
|
| 333 |
+
num_frames=num_frames,
|
| 334 |
+
num_inference_steps=SIGMA_GRID_POINTS,
|
| 335 |
+
generator=torch.Generator("cpu").manual_seed(int(seed)),
|
| 336 |
+
)
|
| 337 |
+
gen_s = time.perf_counter() - started
|
| 338 |
+
peak_gib = torch.cuda.max_memory_allocated() / 1024**3
|
| 339 |
+
forwards = [round(t, 2) for t in FORWARD_TIMES]
|
| 340 |
+
print(f"[gen] pipeline {gen_s:.2f}s · forwards {forwards} · peak {peak_gib:.2f} GiB", flush=True)
|
| 341 |
+
return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate"), gen_s, peak_gib, forwards
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
@spaces.GPU(duration=120, size=GPU_SIZE)
|
| 345 |
+
def selftest() -> str:
|
| 346 |
+
"""Check the vendored VSA-H3 kernels against dense attention on this GPU.
|
| 347 |
+
|
| 348 |
+
At `sparsity = 0` the block map is all-true, so VSA-H3 has to reproduce full attention exactly (up to the tile
|
| 349 |
+
padding and the fp32 pooled selection, which cannot change an all-true mask). That is the one assertion that
|
| 350 |
+
catches a wrong tile order, a wrong `variable_block_sizes`, or a mis-transposed buffer — all of which would
|
| 351 |
+
otherwise show up only as a subtly wrong video. Runs on random tensors; no weights are touched.
|
| 352 |
+
|
| 353 |
+
Returns:
|
| 354 |
+
A markdown report: the dense-equivalence error, and how much of the dense output the 90%-sparse path keeps.
|
| 355 |
+
"""
|
| 356 |
+
import torch
|
| 357 |
+
import torch.nn.functional as F
|
| 358 |
+
|
| 359 |
+
from diffusers.modular_pipelines.minimax_h3.before_denoise import MiniMaxH3PrepareLayoutStep
|
| 360 |
+
|
| 361 |
+
import vsa_h3
|
| 362 |
+
|
| 363 |
+
device = torch.device("cuda")
|
| 364 |
+
heads, dim = 8, 128
|
| 365 |
+
# A real packed layout, just a small one: 300 text rows, 5 latent frames of 8x12 video, its soundtrack.
|
| 366 |
+
_, token_tags, *_ = MiniMaxH3PrepareLayoutStep.build_packed_sequence(
|
| 367 |
+
torch.ones(300, dtype=torch.long), 5, 16, 24, 50, (1, 2, 2), 2, 2, 0, ()
|
| 368 |
+
)
|
| 369 |
+
position_ids = torch.zeros(token_tags.numel(), 3, dtype=torch.float64)
|
| 370 |
+
video_start = int((token_tags == 0).nonzero()[0])
|
| 371 |
+
frame = torch.cartesian_prod(torch.arange(8.0), torch.arange(12.0))
|
| 372 |
+
position_ids[video_start:, 0] = torch.arange(5.0).repeat_interleave(96)
|
| 373 |
+
position_ids[video_start:, 1:] = frame.repeat(5, 1)
|
| 374 |
+
token_tags, position_ids = token_tags.to(device), position_ids.to(device)
|
| 375 |
+
|
| 376 |
+
lines = []
|
| 377 |
+
generator = torch.Generator(device=device).manual_seed(0)
|
| 378 |
+
shape = (1, token_tags.numel(), heads, dim)
|
| 379 |
+
query, key, value = (
|
| 380 |
+
torch.randn(shape, generator=generator, device=device, dtype=torch.bfloat16) for _ in range(3)
|
| 381 |
+
)
|
| 382 |
+
reference = F.scaled_dot_product_attention(
|
| 383 |
+
query.transpose(1, 2), key.transpose(1, 2), value.transpose(1, 2)
|
| 384 |
+
).transpose(1, 2)
|
| 385 |
+
|
| 386 |
+
for sparsity in (0.0, 0.9):
|
| 387 |
+
vsa_h3.reset_tile_buffers()
|
| 388 |
+
geometry = vsa_h3.geometry_from_layout(token_tags, position_ids, sparsity)
|
| 389 |
+
if geometry is None:
|
| 390 |
+
return "**FAILED**: `geometry_from_layout` did not recognize the standard packed layout."
|
| 391 |
+
out = vsa_h3.sparse_attention(query, key, value, None, geometry)
|
| 392 |
+
error = (out.float() - reference.float()).abs().max().item()
|
| 393 |
+
scale = reference.float().abs().max().item()
|
| 394 |
+
similarity = F.cosine_similarity(out.float().flatten(), reference.float().flatten(), dim=0).item()
|
| 395 |
+
lines.append(
|
| 396 |
+
f"| {sparsity:g} | {geometry.topk}/{geometry.num_video_tiles} | {error:.4f} | "
|
| 397 |
+
f"{error / scale:.2e} | {similarity:.6f} |"
|
| 398 |
+
)
|
| 399 |
+
if sparsity == 0.0 and error / scale > 0.02:
|
| 400 |
+
lines.append(f"\n**FAILED**: dense-equivalent VSA differs from SDPA by {error / scale:.3f} relative.")
|
| 401 |
+
|
| 402 |
+
return (
|
| 403 |
+
f"VSA-H3 on `{torch.cuda.get_device_name()}`, {token_tags.numel()} packed rows, "
|
| 404 |
+
f"{heads} heads x {dim}.\n\n"
|
| 405 |
+
"| sparsity | tiles kept | max abs err | relative | cosine |\n|---|---|---|---|---|\n" + "\n".join(lines)
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def generate(
|
| 410 |
+
prompt: str,
|
| 411 |
+
canvas: str = DEFAULT_CANVAS,
|
| 412 |
+
duration: float = DEFAULT_DURATION,
|
| 413 |
+
upsample: bool = True,
|
| 414 |
+
seed: int = 42,
|
| 415 |
+
progress=gr.Progress(track_tqdm=True),
|
| 416 |
+
):
|
| 417 |
+
"""Generate a video with a synchronized soundtrack from a text prompt, in four transformer forwards.
|
| 418 |
+
|
| 419 |
+
Args:
|
| 420 |
+
prompt: The request. MiniMax-H3 was trained on a structured format
|
| 421 |
+
(`integrated_multimodal_description: ... overall_soundscape: ... non_diegetic_music: ...`); leave
|
| 422 |
+
`upsample` on to have the conditioner rewrite a plain sentence into it first.
|
| 423 |
+
canvas: One of the released canvases, as a `WIDTHxHEIGHT · ratio` label. The distillation's own operating
|
| 424 |
+
point is `1344x768 · 16:9 full`.
|
| 425 |
+
duration: Length in seconds, rounded up to the next frame count the video VAE can decode (`17 * n + 5`).
|
| 426 |
+
upsample: Rewrite the prompt into MiniMax-H3's trained format before encoding it.
|
| 427 |
+
seed: Random seed.
|
| 428 |
+
|
| 429 |
+
Returns:
|
| 430 |
+
The path of an mp4 holding h264 video and AAC audio, a one-line report (with a per-request id proving the
|
| 431 |
+
artifact is fresh), the measured `gen_s` of the pipeline call, and the rewritten prompt when there was one.
|
| 432 |
+
"""
|
| 433 |
+
if LOAD_ERROR:
|
| 434 |
+
raise gr.Error(LOAD_ERROR)
|
| 435 |
+
if PIPE is None:
|
| 436 |
+
raise gr.Error("The denoiser is still loading.")
|
| 437 |
+
if not prompt or not prompt.strip():
|
| 438 |
+
raise gr.Error("MiniMax-H3 always takes a prompt.")
|
| 439 |
+
|
| 440 |
+
from diffusers.utils import encode_video
|
| 441 |
+
|
| 442 |
+
num_frames = snap_frames(duration)
|
| 443 |
+
|
| 444 |
+
progress(0.0, desc=f"{'Rewriting the prompt' if upsample else 'Conditioning'} on {CONDITIONER_SPACE} ...")
|
| 445 |
+
conditioned = time.time()
|
| 446 |
+
prompt_embeds, text_token_tags, metadata, plan = encode_remote(
|
| 447 |
+
prompt, canvas, num_frames, rewrite_prompt=bool(upsample)
|
| 448 |
+
)
|
| 449 |
+
condition_seconds = time.time() - conditioned
|
| 450 |
+
height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames"))
|
| 451 |
+
refined = plan.get("refined_prompt") or ""
|
| 452 |
+
|
| 453 |
+
progress(0.2, desc=f"{NUM_FORWARDS} transformer forwards at {width}x{height}, {num_frames} frames ...")
|
| 454 |
+
frames, audio, sampling_rate, gen_s, peak_gib, forwards = _generate(
|
| 455 |
+
prompt_embeds, text_token_tags, height, width, num_frames, seed
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
directory = os.path.join(tempfile.gettempdir(), "fasth3-outputs")
|
| 459 |
+
os.makedirs(directory, exist_ok=True)
|
| 460 |
+
request_id = uuid.uuid4().hex[:12]
|
| 461 |
+
path = os.path.join(directory, f"fasth3-{request_id}.mp4")
|
| 462 |
+
encode_video(frames, fps=FPS, output_path=path, audio=audio, audio_sample_rate=sampling_rate)
|
| 463 |
+
|
| 464 |
+
forward_total = sum(forwards) if forwards else float("nan")
|
| 465 |
+
forward_detail = "+".join(f"{t:.1f}" for t in forwards) if forwards else "n/a"
|
| 466 |
+
report = (
|
| 467 |
+
f"`{width}x{height}`, {num_frames} frames @ {FPS} fps, {NUM_FORWARDS} transformer forwards · "
|
| 468 |
+
f"pipeline **{gen_s:.1f}s** (forwards {forward_detail} = {forward_total:.1f}s, decode+other "
|
| 469 |
+
f"{gen_s - forward_total:.1f}s) · peak {peak_gib:.1f} GiB · "
|
| 470 |
+
f"conditioner {condition_seconds:.0f}s ({plan['num_text_tokens']} tokens"
|
| 471 |
+
f"{', rewritten' if refined else ''}) · seed {int(seed)} · req {request_id}"
|
| 472 |
+
)
|
| 473 |
+
print(f"[gen] {report}", flush=True)
|
| 474 |
+
return path, report, round(gen_s, 2), refined
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
load_models()
|
| 478 |
+
|
| 479 |
+
INTRO = f"""# FastH3 v1 (VSA) — MiniMax-H3 in 4 steps, sparse
|
| 480 |
+
|
| 481 |
+
<div>
|
| 482 |
+
<a href="https://huggingface.co/{MODEL_REPO}" target="_blank" rel="noopener"><strong>[ model ]</strong></a>
|
| 483 |
+
<a href="https://github.com/hao-ai-lab/FastVideo" target="_blank" rel="noopener"><strong>[ FastVideo ]</strong></a>
|
| 484 |
+
<a href="https://huggingface.co/{BASE_REPO}" target="_blank" rel="noopener"><strong>[ base model ]</strong></a>
|
| 485 |
+
</div>
|
| 486 |
+
|
| 487 |
+
[`{MODEL_REPO}`](https://huggingface.co/{MODEL_REPO}) is a **data-free DMD2 distillation** of
|
| 488 |
+
[MiniMax-H3](https://huggingface.co/{BASE_REPO}), the 33B dual-modality transformer that generates video **and** a
|
| 489 |
+
fully synchronized soundtrack (ambience, foley, speech) in one denoising pass. The base model samples in 50 steps;
|
| 490 |
+
this student walks a trained 4-step ladder — `t = 999, 749, 500, 250` — for **{NUM_FORWARDS} transformer forwards**
|
| 491 |
+
per video.
|
| 492 |
+
|
| 493 |
+
It is also distilled **under Video Sparse Attention**: 64-token tiles at 90% sparsity, with a trained per-head
|
| 494 |
+
compression gate. This Space runs that sparse path, on FastVideo's own Triton kernels — not a dense substitute.
|
| 495 |
+
"""
|
| 496 |
+
|
| 497 |
+
FORMAT_NOTE = """MiniMax-H3 was trained on a structured prompt, not a caption:
|
| 498 |
+
|
| 499 |
+
```text
|
| 500 |
+
integrated_multimodal_description: [Shot 1] ... <d>[English] spoken line.</d> [Shot 2] At 00:04.500, ...
|
| 501 |
+
overall_soundscape: ...
|
| 502 |
+
non_diegetic_music: ...
|
| 503 |
+
```
|
| 504 |
+
|
| 505 |
+
**Expand prompt** (on by default) sends a plain sentence through the Qwen3-VL conditioner's own language-model head
|
| 506 |
+
first, which writes that format with the same weights that are about to encode it. Turn it off when the prompt is
|
| 507 |
+
already written out — as the last two examples below are. See the base model's
|
| 508 |
+
[prompt writing guide](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/main/docs/VIDEO_PROMPT_WRITING_GUIDE_base_en.md).
|
| 509 |
+
"""
|
| 510 |
+
|
| 511 |
+
# Both long examples are the MiniMax-H3 authors' own published T2VA prompts: the first is Case 1 of the prompt writing
|
| 512 |
+
# guide, the second is the reproducible 768p T2VA case from the model card
|
| 513 |
+
# (`scripts/readme/reproducible-768p-t2va-request.sh`). Both are documentation of the Apache-2.0 base repo.
|
| 514 |
+
GUIDE_CASE_1 = BENCH_PROMPT
|
| 515 |
+
OFFICIAL_T2VA = (
|
| 516 |
+
"integrated_multimodal_description: [Shot 1] Cinematic, medium wide shot, pushing in slowly. In the cavernous, "
|
| 517 |
+
"dimly lit bridge of a starship, sleek metallic consoles with glowing amber displays flank a massive, curved "
|
| 518 |
+
"observation window. A female captain, in her late 40s with an athletic build and short silver-streaked black "
|
| 519 |
+
"hair, stands in the center midground. She wears a structured, high-collared dark navy military tunic with "
|
| 520 |
+
"silver chest insignias. Her back is to the camera, silhouetted against the cool, ambient starlight pouring "
|
| 521 |
+
"through the thick glass. She stands perfectly still with her hands clasped tightly behind her back. Outside the "
|
| 522 |
+
"window, a massive armada of jagged, dark grey dreadnoughts hovers in tight formation against a deep purple "
|
| 523 |
+
"space nebula. The fleet's massive rear thrusters begin to glow with an intense, escalating bright blue light. "
|
| 524 |
+
"[Shot 2] At 00:04.500, the camera cuts to a close-up of the captain's face and shakes strongly. The brilliant "
|
| 525 |
+
"blue-white light from the fleet's gathering energy reflects vividly in her dark eyes. Suddenly, a blinding "
|
| 526 |
+
"white flash floods through the window, completely washing out the background as the fleet jumps to hyperspace. "
|
| 527 |
+
"The sheer spatial force violently jolts the bridge, causing the captain from Shot 1 to stagger slightly "
|
| 528 |
+
"forward, her shoulders tensing as she visibly braces herself against the physical tremors. As the intense "
|
| 529 |
+
"white light fades abruptly, leaving only the dim, empty expanse of the purple nebula reflected in her starkly "
|
| 530 |
+
"lit skin, her jaw clenches, and she slowly closes her eyes in the newly emptied space.\n"
|
| 531 |
+
"overall_soundscape: A low, resonant hum of the ship's ambient life support systems serves as the baseline, soon "
|
| 532 |
+
"drowned out by an audible, escalating, high-pitched electronic whine as the fleet outside charges its "
|
| 533 |
+
"hyperdrives. A massive, deafening, bass-heavy boom and sharp crackle erupts during the blinding flash, "
|
| 534 |
+
"accompanied by the loud metallic creaking, rattling, and deep thuds of the bridge's bulkheads vibrating under "
|
| 535 |
+
"immense physical stress. The intense roaring impact then cuts abruptly back to a hollow, echoing room tone, "
|
| 536 |
+
"leaving only the faint, steady hum of the isolated bridge.\n"
|
| 537 |
+
"non_diegetic_music: Cinematic space-opera orchestral score, slow tempo, featuring a solitary, mournful French "
|
| 538 |
+
"horn melody over deep, sustained string dissonances that build rapidly in volume and intensity, swelling to a "
|
| 539 |
+
"massive orchestral peak before snapping immediately into silence right after the jump."
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
CSS = """
|
| 543 |
+
.main.fillable {max-width: 1250px !important}
|
| 544 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 545 |
+
"""
|
| 546 |
+
|
| 547 |
+
with gr.Blocks(title="FastH3 v1 (VSA)") as demo:
|
| 548 |
+
gr.Markdown(INTRO)
|
| 549 |
+
|
| 550 |
+
with gr.Row():
|
| 551 |
+
with gr.Column():
|
| 552 |
+
prompt = gr.Textbox(
|
| 553 |
+
label="Prompt",
|
| 554 |
+
lines=5,
|
| 555 |
+
placeholder="A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot",
|
| 556 |
+
value="A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot",
|
| 557 |
+
)
|
| 558 |
+
upsample = gr.Checkbox(
|
| 559 |
+
label="Expand prompt into MiniMax-H3's trained format",
|
| 560 |
+
value=True,
|
| 561 |
+
info="Runs on the conditioner Space before encoding. Turn off for a prompt already in that format.",
|
| 562 |
+
)
|
| 563 |
+
run = gr.Button("Generate", variant="primary")
|
| 564 |
+
with gr.Accordion("Advanced options", open=False):
|
| 565 |
+
canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
|
| 566 |
+
duration = gr.Slider(
|
| 567 |
+
label="Duration (s)",
|
| 568 |
+
minimum=MIN_UI_DURATION,
|
| 569 |
+
maximum=MAX_UI_DURATION,
|
| 570 |
+
step=1,
|
| 571 |
+
value=DEFAULT_DURATION,
|
| 572 |
+
)
|
| 573 |
+
seed = gr.Number(label="Seed", value=42, precision=0)
|
| 574 |
+
gr.Markdown(
|
| 575 |
+
f"Steps are fixed at the trained ladder — a {SIGMA_GRID_POINTS}-point sigma grid, "
|
| 576 |
+
f"{NUM_FORWARDS} transformer forwards, exactly what the checkpoint's own "
|
| 577 |
+
"`fastvideo_inference.json` specifies. There is no guidance scale and no negative prompt: the "
|
| 578 |
+
"base model is guidance-distilled."
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
with gr.Column():
|
| 582 |
+
video = gr.Video(label="Video + soundtrack")
|
| 583 |
+
report = gr.Markdown()
|
| 584 |
+
gen_s = gr.Number(label="gen_s — server-side pipeline time (s)", precision=2, interactive=False)
|
| 585 |
+
with gr.Accordion("Expanded prompt", open=False):
|
| 586 |
+
upsampled = gr.Textbox(show_label=False, lines=10, interactive=False)
|
| 587 |
+
|
| 588 |
+
with gr.Accordion("Prompt format", open=False):
|
| 589 |
+
gr.Markdown(FORMAT_NOTE)
|
| 590 |
+
banner = gr.Markdown()
|
| 591 |
+
|
| 592 |
+
gr.Examples(
|
| 593 |
+
examples=[
|
| 594 |
+
[
|
| 595 |
+
"A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot",
|
| 596 |
+
"960x544 · 16:9 fast",
|
| 597 |
+
5,
|
| 598 |
+
True,
|
| 599 |
+
],
|
| 600 |
+
[
|
| 601 |
+
"A cellist playing a slow, low melody alone in an empty concert hall",
|
| 602 |
+
"1344x768 · 16:9 full",
|
| 603 |
+
5,
|
| 604 |
+
True,
|
| 605 |
+
],
|
| 606 |
+
[GUIDE_CASE_1, "1344x768 · 16:9 full", 5, False],
|
| 607 |
+
[OFFICIAL_T2VA, "1344x768 · 16:9 full", 5, False],
|
| 608 |
+
],
|
| 609 |
+
inputs=[prompt, canvas, duration, upsample],
|
| 610 |
+
outputs=[video, report, gen_s, upsampled],
|
| 611 |
+
fn=generate,
|
| 612 |
+
cache_examples=True,
|
| 613 |
+
cache_mode="lazy",
|
| 614 |
+
label="Examples — the last two are the MiniMax-H3 authors' own published T2VA prompts",
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
run.click(
|
| 618 |
+
generate,
|
| 619 |
+
[prompt, canvas, duration, upsample, seed],
|
| 620 |
+
[video, report, gen_s, upsampled],
|
| 621 |
+
api_name="generate",
|
| 622 |
+
)
|
| 623 |
+
demo.load(status, None, banner, api_name="status")
|
| 624 |
+
|
| 625 |
+
# No UI, API only: the sparse-attention equivalence check, so the kernel can be verified on this pool without
|
| 626 |
+
# spending a full generation.
|
| 627 |
+
diagnose = gr.Button(visible=False)
|
| 628 |
+
diagnose.click(selftest, None, gr.Markdown(visible=False), api_name="selftest")
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
if __name__ == "__main__":
|
| 632 |
+
# Gradio 6 moved `theme` and `css` off the `Blocks` constructor onto `launch`.
|
| 633 |
+
demo.launch(theme=gr.themes.Citrus(), css=CSS, show_error=True, max_threads=1000, mcp_server=True)
|
h3_split_blocks.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The denoising half of a **split** MiniMax-H3 deployment.
|
| 2 |
+
|
| 3 |
+
MiniMax-H3 is ~196 GiB in bfloat16 and a ZeroGPU Space is evicted at 150 GB of storage, so `MiniMaxH3Blocks` is cut at
|
| 4 |
+
its `text_encoder` step: the 62.14 GiB Qwen3-VL conditioner runs in its own Space and everything else — the distilled
|
| 5 |
+
transformer and the two autoencoders — runs here. `prompt_embeds` + `text_token_tags` is the whole wire format between
|
| 6 |
+
the two halves.
|
| 7 |
+
|
| 8 |
+
For a text-only (`t2va`) request the full `MiniMaxH3Blocks` sequence is `before_encode -> text_encoder -> vae_encoder
|
| 9 |
+
-> denoise -> decode`, and the two `Auto*` encoder steps select nothing without a keyframe or a reference. What is
|
| 10 |
+
left once the text encoder goes is exactly `MiniMaxH3CoreDenoiseStep -> MiniMaxH3DecodeStep`, and those two declare
|
| 11 |
+
only `transformer`, the two schedulers, both autoencoders and `video_processor` — so `load_components` resolves those
|
| 12 |
+
out of the checkpoint's `modular_model_index.json` and never fetches the conditioner or the `ref2va` partition.
|
| 13 |
+
|
| 14 |
+
Adapted from `multimodalart/minimax-h3`, trimmed to the `t2va` (text -> video + audio) branch this Space serves.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
from diffusers.modular_pipelines.minimax_h3.modular_blocks_minimax_h3 import (
|
| 19 |
+
MiniMaxH3CoreDenoiseStep,
|
| 20 |
+
MiniMaxH3DecodeStep,
|
| 21 |
+
)
|
| 22 |
+
from diffusers.modular_pipelines.modular_pipeline import SequentialPipelineBlocks
|
| 23 |
+
from diffusers.modular_pipelines.modular_pipeline_utils import OutputParam
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class MiniMaxH3GeneratorBlocks(SequentialPipelineBlocks):
|
| 27 |
+
"""The denoising half of a split MiniMax-H3: the `t2va` branch of `MiniMaxH3Blocks` without its text encoder."""
|
| 28 |
+
|
| 29 |
+
model_name = "minimax-h3"
|
| 30 |
+
block_classes = [MiniMaxH3CoreDenoiseStep, MiniMaxH3DecodeStep]
|
| 31 |
+
block_names = ["denoise", "decode"]
|
| 32 |
+
|
| 33 |
+
@property
|
| 34 |
+
def description(self):
|
| 35 |
+
return (
|
| 36 |
+
"The denoising half of a split MiniMax-H3 deployment: the `t2va` branch of `MiniMaxH3Blocks` without its "
|
| 37 |
+
"text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the 62.14 GiB Qwen3-VL "
|
| 38 |
+
"conditioner is never loaded here."
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def outputs(self):
|
| 43 |
+
return [
|
| 44 |
+
OutputParam.template("videos", description="The generated video."),
|
| 45 |
+
OutputParam(
|
| 46 |
+
"audio",
|
| 47 |
+
type_hint=torch.Tensor,
|
| 48 |
+
description="The generated soundtrack, of shape `(1, 2, num_samples)`.",
|
| 49 |
+
),
|
| 50 |
+
OutputParam("sampling_rate", type_hint=int, description="Sample rate of the generated soundtrack in Hz."),
|
| 51 |
+
]
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ffmpeg
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MiniMax-H3 landed in diffusers 0.40.0 (modular pipelines only). Pinned because `h3_split_blocks.py` subclasses its
|
| 2 |
+
# block classes and `vsa_h3.py` patches `MiniMaxH3TransformerBlock` / `MiniMaxH3Attention` by name.
|
| 3 |
+
diffusers==0.40.0
|
| 4 |
+
# torch is deliberately unpinned: the ZeroGPU runtime preinstalls a supported build, and its bundled `pytorch-triton`
|
| 5 |
+
# is what compiles the vendored VSA block-sparse kernels. torchvision is not needed anywhere in this Space.
|
| 6 |
+
transformers
|
| 7 |
+
accelerate
|
| 8 |
+
# PyAV muxes the generated soundtrack onto the frames (`diffusers.utils.encode_video`).
|
| 9 |
+
av
|
| 10 |
+
pillow
|
| 11 |
+
numpy
|
| 12 |
+
safetensors>=0.8.0
|
| 13 |
+
# huggingface-hub is deliberately absent: it is platform-managed, and diffusers already constrains it to <2.
|
vsa_h3.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""VSA-H3 — MiniMax-H3's Video Sparse Attention, as a `diffusers` attention processor.
|
| 2 |
+
|
| 3 |
+
`FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree` is distilled **under** block-sparse attention
|
| 4 |
+
(`attention_backend: VIDEO_SPARSE_ATTN_H3`, `vsa_tile_size: 64`, `vsa_sparsity: 0.9` in the checkpoint's own
|
| 5 |
+
`fastvideo_inference.json`), and FastVideo's release notes are explicit that dense attention is *not* a drop-in
|
| 6 |
+
substitute for a VSA-trained student: the student learned to attend to the top-10% tiles its selector picks, and it
|
| 7 |
+
also ships 50 trained `attn.to_gate_compress` tensors that only the sparse path reads. So this Space runs the sparse
|
| 8 |
+
path.
|
| 9 |
+
|
| 10 |
+
`diffusers` has no VSA backend, so this module ports FastVideo's `MiniMaxH3VSABackend`
|
| 11 |
+
(`fastvideo/attention/backends/video_sparse_attn_h3.py`) onto `MiniMaxH3Attention`:
|
| 12 |
+
|
| 13 |
+
1. **Tiling.** The packed sequence `[text | audio | video]` is cut into 64-token tiles: segment-pure prefix chunks
|
| 14 |
+
first, then `(4, 4, 4)` cubes of the post-patchify `(t, h, w)` video grid. Tiles are zero-padded to 64 and
|
| 15 |
+
`variable_block_sizes` carries each tile's true occupancy.
|
| 16 |
+
2. **Selection.** Per head, tiles are mean-pooled in fp32, `scores = q_pooled @ k_pooledᵀ / √d`, and each query tile
|
| 17 |
+
keeps the top `ceil((1 - 0.9) * num_video_tiles)` video tiles. Prefix (text/audio) keys are *exempt* — always
|
| 18 |
+
selected — and prefix queries are always dense, which is FastVideo's default `vsa_mode`.
|
| 19 |
+
3. **Kernel.** The resulting bool block map is compacted with FastVideo's `map_to_index` Triton kernel and consumed by
|
| 20 |
+
its `triton_block_sparse_attn_forward`, both vendored verbatim under `vsa_kernel/`. This is FastVideo's own
|
| 21 |
+
`--vsa-kernel triton` route; the checkpoint's `vsa_kernel: sm100a` is the GB200-only fast path for the *same* mask
|
| 22 |
+
semantics, and this Space's Blackwell RTX PRO 6000 is sm120.
|
| 23 |
+
4. **Compression branch.** `out_c = softmax(scores) @ v_pooled` broadcast back over each tile's rows and scaled by the
|
| 24 |
+
trained per-row `to_gate_compress` gate, added to the sparse output. The base MiniMax-H3 release zero-initializes
|
| 25 |
+
this gate (branch inert); this student ships it trained.
|
| 26 |
+
|
| 27 |
+
The one deliberate deviation from the reference is memory layout, not math: buffers are tiled straight into the
|
| 28 |
+
kernel's `[B, H, S_pad, D]` layout and reused across the 50 blocks, and the gate is applied *after* untiling, so the
|
| 29 |
+
sparse path costs one persistent tile buffer per projection instead of four plus three transposed copies.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import functools
|
| 35 |
+
import math
|
| 36 |
+
from dataclasses import dataclass
|
| 37 |
+
|
| 38 |
+
import torch
|
| 39 |
+
|
| 40 |
+
# 64-token tiles: `(4, 4, 4)` over the post-patchify video grid. This is the checkpoint's `vsa_tile_size`, and it is
|
| 41 |
+
# the Triton kernels' native block size, so the block map needs no expansion.
|
| 42 |
+
TILE_ELEMS = 64
|
| 43 |
+
TILE_SHAPE = (4, 4, 4)
|
| 44 |
+
DEFAULT_SPARSITY = 0.9
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# --------------------------------------------------------------------------------------------------------------
|
| 48 |
+
# Geometry — ported from `fastvideo.attention.backends.video_sparse_attn{,_h3}`
|
| 49 |
+
# --------------------------------------------------------------------------------------------------------------
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _tile_partition_indices(dit_seq_shape: tuple[int, int, int], device: torch.device) -> torch.Tensor:
|
| 53 |
+
"""Row indices of the video grid in `(4, 4, 4)` tile order."""
|
| 54 |
+
grid_t, grid_h, grid_w = dit_seq_shape
|
| 55 |
+
ts, hs, ws = TILE_SHAPE
|
| 56 |
+
indices = torch.arange(grid_t * grid_h * grid_w, device=device, dtype=torch.long).reshape(grid_t, grid_h, grid_w)
|
| 57 |
+
chunks = []
|
| 58 |
+
for t in range(math.ceil(grid_t / ts)):
|
| 59 |
+
for h in range(math.ceil(grid_h / hs)):
|
| 60 |
+
for w in range(math.ceil(grid_w / ws)):
|
| 61 |
+
chunks.append(
|
| 62 |
+
indices[t * ts : min(t * ts + ts, grid_t), h * hs : min(h * hs + hs, grid_h), w * ws : min(w * ws + ws, grid_w)].flatten()
|
| 63 |
+
)
|
| 64 |
+
return torch.cat(chunks, dim=0)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _video_block_sizes(dit_seq_shape: tuple[int, int, int], device: torch.device) -> torch.Tensor:
|
| 68 |
+
"""Valid (non-padded) token count of every video tile, in the same tile order."""
|
| 69 |
+
ts, hs, ws = TILE_SHAPE
|
| 70 |
+
|
| 71 |
+
def sizes(length: int, tile: int) -> torch.Tensor:
|
| 72 |
+
count = math.ceil(length / tile)
|
| 73 |
+
out = torch.full((count,), tile, dtype=torch.long, device=device)
|
| 74 |
+
remainder = length - (count - 1) * tile
|
| 75 |
+
out[-1] = remainder if remainder > 0 else tile
|
| 76 |
+
return out
|
| 77 |
+
|
| 78 |
+
t_sizes, h_sizes, w_sizes = (sizes(length, tile) for length, tile in zip(dit_seq_shape, TILE_SHAPE))
|
| 79 |
+
return (t_sizes[:, None, None] * h_sizes[None, :, None] * w_sizes[None, None, :]).reshape(-1)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _non_pad_index(variable_block_sizes: torch.Tensor) -> torch.Tensor:
|
| 83 |
+
"""Padded-buffer slot of every real token, in tile order."""
|
| 84 |
+
device = variable_block_sizes.device
|
| 85 |
+
starts = torch.arange(variable_block_sizes.shape[0], device=device) * TILE_ELEMS
|
| 86 |
+
slots = starts[:, None] + torch.arange(TILE_ELEMS, device=device)[None, :]
|
| 87 |
+
keep = torch.arange(TILE_ELEMS, device=device)[None, :] < variable_block_sizes[:, None]
|
| 88 |
+
return slots[keep]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@dataclass(frozen=True, eq=False)
|
| 92 |
+
class VSAGeometry:
|
| 93 |
+
"""Everything the sparse path needs about one packed layout. Cached per `(prefix_segments, grid)`."""
|
| 94 |
+
|
| 95 |
+
seq_len: int
|
| 96 |
+
padded_len: int
|
| 97 |
+
n_tiles: int
|
| 98 |
+
num_prefix_tiles: int
|
| 99 |
+
num_video_tiles: int
|
| 100 |
+
topk: int
|
| 101 |
+
# int32 tile occupancies, as the Triton kernels take them.
|
| 102 |
+
variable_block_sizes: torch.Tensor
|
| 103 |
+
# fp32 tile occupancies, for the pooled mean.
|
| 104 |
+
tile_divisor: torch.Tensor
|
| 105 |
+
# packed row -> padded tile-buffer slot (scatter on the way in, gather on the way out)
|
| 106 |
+
untile_index: torch.Tensor
|
| 107 |
+
# packed row -> its tile, so the compression branch can be applied after untiling
|
| 108 |
+
row_tile_index: torch.Tensor
|
| 109 |
+
|
| 110 |
+
@property
|
| 111 |
+
def dense(self) -> bool:
|
| 112 |
+
return self.topk >= self.num_video_tiles
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@functools.lru_cache(maxsize=8)
|
| 116 |
+
def build_geometry(
|
| 117 |
+
prefix_segments: tuple[int, ...],
|
| 118 |
+
dit_seq_shape: tuple[int, int, int],
|
| 119 |
+
device: torch.device,
|
| 120 |
+
sparsity: float = DEFAULT_SPARSITY,
|
| 121 |
+
) -> VSAGeometry:
|
| 122 |
+
prefix_len = sum(prefix_segments)
|
| 123 |
+
|
| 124 |
+
prefix_sizes: list[int] = []
|
| 125 |
+
for segment in prefix_segments:
|
| 126 |
+
full, remainder = divmod(segment, TILE_ELEMS)
|
| 127 |
+
prefix_sizes.extend([TILE_ELEMS] * full)
|
| 128 |
+
if remainder:
|
| 129 |
+
prefix_sizes.append(remainder)
|
| 130 |
+
|
| 131 |
+
video_sizes = _video_block_sizes(dit_seq_shape, device)
|
| 132 |
+
variable_block_sizes = torch.cat(
|
| 133 |
+
[torch.tensor(prefix_sizes, dtype=torch.long, device=device), video_sizes]
|
| 134 |
+
)
|
| 135 |
+
partition = torch.cat(
|
| 136 |
+
[
|
| 137 |
+
torch.arange(prefix_len, device=device, dtype=torch.long),
|
| 138 |
+
_tile_partition_indices(dit_seq_shape, device) + prefix_len,
|
| 139 |
+
]
|
| 140 |
+
)
|
| 141 |
+
untile_index = _non_pad_index(variable_block_sizes)[torch.argsort(partition)]
|
| 142 |
+
|
| 143 |
+
n_tiles = int(variable_block_sizes.numel())
|
| 144 |
+
num_video_tiles = int(video_sizes.numel())
|
| 145 |
+
return VSAGeometry(
|
| 146 |
+
seq_len=int(partition.numel()),
|
| 147 |
+
padded_len=n_tiles * TILE_ELEMS,
|
| 148 |
+
n_tiles=n_tiles,
|
| 149 |
+
num_prefix_tiles=len(prefix_sizes),
|
| 150 |
+
num_video_tiles=num_video_tiles,
|
| 151 |
+
# FastVideo's `compute_topk`, clamped to [1, num_video_tiles].
|
| 152 |
+
topk=max(1, min(math.ceil((1.0 - sparsity) * num_video_tiles), num_video_tiles)),
|
| 153 |
+
variable_block_sizes=variable_block_sizes.to(torch.int32).contiguous(),
|
| 154 |
+
tile_divisor=variable_block_sizes.to(torch.float32).view(1, 1, -1, 1),
|
| 155 |
+
untile_index=untile_index,
|
| 156 |
+
row_tile_index=untile_index // TILE_ELEMS,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def geometry_from_layout(
|
| 161 |
+
token_tags: torch.Tensor,
|
| 162 |
+
position_ids: torch.Tensor,
|
| 163 |
+
sparsity: float = DEFAULT_SPARSITY,
|
| 164 |
+
) -> VSAGeometry | None:
|
| 165 |
+
"""Recover the VSA geometry from what the transformer is actually given.
|
| 166 |
+
|
| 167 |
+
The packed sequence a `t2va` request builds is `[text | audio | video]`, but nothing downstream is told that, so
|
| 168 |
+
the layout is read back off the two per-row descriptions the transformer already takes: `token_tags` (0 video,
|
| 169 |
+
1 text, 2 audio) gives the segment boundaries, and the `(t, h, w)` rotary grid of the video rows gives the shape
|
| 170 |
+
of the video block. Returns `None` for any layout the sparse path does not cover, so the caller can stay dense.
|
| 171 |
+
"""
|
| 172 |
+
tags = token_tags.tolist()
|
| 173 |
+
seq_len = len(tags)
|
| 174 |
+
if seq_len == 0 or tags[-1] != 0:
|
| 175 |
+
return None
|
| 176 |
+
|
| 177 |
+
video_start = seq_len
|
| 178 |
+
while video_start > 0 and tags[video_start - 1] == 0:
|
| 179 |
+
video_start -= 1
|
| 180 |
+
if video_start == 0:
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
prefix_segments: list[int] = []
|
| 184 |
+
previous = None
|
| 185 |
+
for tag in tags[:video_start]:
|
| 186 |
+
if tag == previous:
|
| 187 |
+
prefix_segments[-1] += 1
|
| 188 |
+
else:
|
| 189 |
+
prefix_segments.append(1)
|
| 190 |
+
previous = tag
|
| 191 |
+
|
| 192 |
+
# The video rows are `torch.meshgrid(height_grid, width_grid, indexing="ij")` repeated once per latent frame, so
|
| 193 |
+
# the grid falls out of the run lengths of the leading rows.
|
| 194 |
+
grid = position_ids[video_start:]
|
| 195 |
+
rows_per_frame = int((grid[:, 0] == grid[0, 0]).sum())
|
| 196 |
+
grid_w = int((grid[:rows_per_frame, 1] == grid[0, 1]).sum())
|
| 197 |
+
if rows_per_frame == 0 or grid_w == 0 or rows_per_frame % grid_w:
|
| 198 |
+
return None
|
| 199 |
+
grid_h = rows_per_frame // grid_w
|
| 200 |
+
num_video_rows = seq_len - video_start
|
| 201 |
+
if num_video_rows % rows_per_frame:
|
| 202 |
+
return None
|
| 203 |
+
grid_t = num_video_rows // rows_per_frame
|
| 204 |
+
|
| 205 |
+
return build_geometry(tuple(prefix_segments), (grid_t, grid_h, grid_w), token_tags.device, sparsity)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# --------------------------------------------------------------------------------------------------------------
|
| 209 |
+
# The sparse attention itself
|
| 210 |
+
# --------------------------------------------------------------------------------------------------------------
|
| 211 |
+
|
| 212 |
+
# One reusable padded tile buffer per projection. Tiles are written by scatter and pad slots are never touched, so a
|
| 213 |
+
# buffer only has to be re-zeroed when the geometry behind it changes.
|
| 214 |
+
_TILE_BUFFERS: dict[str, tuple[torch.Tensor, int]] = {}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def reset_tile_buffers() -> None:
|
| 218 |
+
_TILE_BUFFERS.clear()
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def _tile(x: torch.Tensor, geometry: VSAGeometry, slot: str) -> torch.Tensor:
|
| 222 |
+
"""`[B, S, H, D]` -> the kernel's `[B, H, S_pad, D]`, pad slots zero."""
|
| 223 |
+
batch, _, heads, dim = x.shape
|
| 224 |
+
shape = (batch, heads, geometry.padded_len, dim)
|
| 225 |
+
cached = _TILE_BUFFERS.get(slot)
|
| 226 |
+
if cached is None or cached[0].shape != shape or cached[0].dtype != x.dtype or cached[0].device != x.device:
|
| 227 |
+
buffer = torch.zeros(shape, dtype=x.dtype, device=x.device)
|
| 228 |
+
else:
|
| 229 |
+
buffer = cached[0]
|
| 230 |
+
if cached[1] != id(geometry):
|
| 231 |
+
buffer.zero_()
|
| 232 |
+
buffer.index_copy_(2, geometry.untile_index, x.transpose(1, 2))
|
| 233 |
+
_TILE_BUFFERS[slot] = (buffer, id(geometry))
|
| 234 |
+
return buffer
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def _pool(tiled: torch.Tensor, geometry: VSAGeometry) -> torch.Tensor:
|
| 238 |
+
"""fp32 masked mean over each 64-token tile. `[B, H, S_pad, D]` -> `[B, H, n_tiles, D]`."""
|
| 239 |
+
batch, heads, _, dim = tiled.shape
|
| 240 |
+
pooled = tiled.view(batch, heads, geometry.n_tiles, TILE_ELEMS, dim).sum(dim=3, dtype=torch.float32)
|
| 241 |
+
return pooled / geometry.tile_divisor
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _block_mask(scores: torch.Tensor, geometry: VSAGeometry) -> torch.Tensor:
|
| 245 |
+
"""Top-k video tiles per query tile, with the prefix exempt and prefix queries dense."""
|
| 246 |
+
prefix = geometry.num_prefix_tiles
|
| 247 |
+
mask = torch.zeros_like(scores, dtype=torch.bool)
|
| 248 |
+
indices = scores[..., prefix:].topk(geometry.topk, dim=-1).indices + prefix
|
| 249 |
+
mask.scatter_(-1, indices, True)
|
| 250 |
+
mask[..., :prefix] = True
|
| 251 |
+
mask[:, :, :prefix, :] = True
|
| 252 |
+
return mask
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def sparse_attention(
|
| 256 |
+
query: torch.Tensor,
|
| 257 |
+
key: torch.Tensor,
|
| 258 |
+
value: torch.Tensor,
|
| 259 |
+
gate_compress: torch.Tensor | None,
|
| 260 |
+
geometry: VSAGeometry,
|
| 261 |
+
) -> torch.Tensor:
|
| 262 |
+
"""VSA-H3 over one packed sequence. All tensors are `[B, S, H, D]`; the result is too."""
|
| 263 |
+
from vsa_kernel import map_to_index, triton_block_sparse_attn_forward
|
| 264 |
+
|
| 265 |
+
query_tiled = _tile(query, geometry, "q")
|
| 266 |
+
key_tiled = _tile(key, geometry, "k")
|
| 267 |
+
value_tiled = _tile(value, geometry, "v")
|
| 268 |
+
|
| 269 |
+
scores = torch.matmul(_pool(query_tiled, geometry), _pool(key_tiled, geometry).transpose(-2, -1))
|
| 270 |
+
scores = scores / math.sqrt(query.shape[-1])
|
| 271 |
+
|
| 272 |
+
mask = _block_mask(scores, geometry)
|
| 273 |
+
q2k_index, q2k_num = map_to_index(mask)
|
| 274 |
+
out_tiled, _ = triton_block_sparse_attn_forward(
|
| 275 |
+
query_tiled,
|
| 276 |
+
key_tiled,
|
| 277 |
+
value_tiled,
|
| 278 |
+
q2k_index,
|
| 279 |
+
q2k_num,
|
| 280 |
+
geometry.variable_block_sizes,
|
| 281 |
+
)
|
| 282 |
+
out = out_tiled.index_select(2, geometry.untile_index)
|
| 283 |
+
|
| 284 |
+
if gate_compress is not None:
|
| 285 |
+
# The compression branch: dense attention over the pooled tiles, broadcast back to every row of its tile and
|
| 286 |
+
# scaled by the trained gate. Applied here rather than on the tile buffer so the gate never needs one.
|
| 287 |
+
pooled = torch.matmul(torch.softmax(scores, dim=-1), _pool(value_tiled, geometry)).to(out.dtype)
|
| 288 |
+
contribution = pooled.index_select(2, geometry.row_tile_index)
|
| 289 |
+
del pooled
|
| 290 |
+
contribution.mul_(gate_compress.transpose(1, 2))
|
| 291 |
+
out.add_(contribution)
|
| 292 |
+
del contribution
|
| 293 |
+
|
| 294 |
+
return out.transpose(1, 2)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
# --------------------------------------------------------------------------------------------------------------
|
| 298 |
+
# The processor, and installing it on a loaded transformer
|
| 299 |
+
# --------------------------------------------------------------------------------------------------------------
|
| 300 |
+
|
| 301 |
+
# `forward` fills this in from the layout it was handed; the 50 block processors read it. One request at a time —
|
| 302 |
+
# `@spaces.GPU` serializes them anyway.
|
| 303 |
+
_ACTIVE: dict[str, VSAGeometry | None] = {"geometry": None}
|
| 304 |
+
|
| 305 |
+
# What the last transformer forward actually ran on. A silent fall back to dense would otherwise be invisible — the
|
| 306 |
+
# video still comes out, just off-distribution — so every distinct layout logs one line and the Space surfaces the
|
| 307 |
+
# most recent one in its status banner.
|
| 308 |
+
LAST_LAYOUT = "no forward yet"
|
| 309 |
+
_REPORTED: set[str] = set()
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _report(summary: str) -> None:
|
| 313 |
+
global LAST_LAYOUT
|
| 314 |
+
|
| 315 |
+
LAST_LAYOUT = summary
|
| 316 |
+
if summary not in _REPORTED:
|
| 317 |
+
_REPORTED.add(summary)
|
| 318 |
+
print(f"[vsa] {summary}", flush=True)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
class MiniMaxH3VSAAttnProcessor:
|
| 322 |
+
"""`MiniMaxH3AttnProcessor` with `dispatch_attention_fn` replaced by VSA-H3."""
|
| 323 |
+
|
| 324 |
+
_attention_backend = None
|
| 325 |
+
_parallel_config = None
|
| 326 |
+
|
| 327 |
+
def __call__(self, attn, hidden_states, rotary_emb=None, attention_mask=None):
|
| 328 |
+
from diffusers.models.transformers.transformer_minimax_h3 import _apply_rotary_emb
|
| 329 |
+
|
| 330 |
+
geometry = _ACTIVE["geometry"]
|
| 331 |
+
|
| 332 |
+
query = attn.to_q(hidden_states).unflatten(-1, (attn.heads, -1))
|
| 333 |
+
key = attn.to_k(hidden_states).unflatten(-1, (attn.heads, -1))
|
| 334 |
+
value = attn.to_v(hidden_states).unflatten(-1, (attn.heads, -1))
|
| 335 |
+
query = attn.norm_q(query)
|
| 336 |
+
key = attn.norm_k(key)
|
| 337 |
+
if rotary_emb is not None:
|
| 338 |
+
query = _apply_rotary_emb(query, *rotary_emb)
|
| 339 |
+
key = _apply_rotary_emb(key, *rotary_emb)
|
| 340 |
+
|
| 341 |
+
if geometry is None or geometry.seq_len != hidden_states.shape[1]:
|
| 342 |
+
_report(f"DENSE FALLBACK in the attention processor at seq_len={hidden_states.shape[1]}")
|
| 343 |
+
from diffusers.models.attention_dispatch import dispatch_attention_fn
|
| 344 |
+
|
| 345 |
+
out = dispatch_attention_fn(
|
| 346 |
+
query,
|
| 347 |
+
key,
|
| 348 |
+
value,
|
| 349 |
+
attn_mask=attention_mask,
|
| 350 |
+
dropout_p=0.0,
|
| 351 |
+
is_causal=False,
|
| 352 |
+
backend=self._attention_backend,
|
| 353 |
+
parallel_config=self._parallel_config,
|
| 354 |
+
)
|
| 355 |
+
else:
|
| 356 |
+
gate_compress = None
|
| 357 |
+
gate = getattr(attn, "to_gate_compress", None)
|
| 358 |
+
if gate is not None:
|
| 359 |
+
gate_compress = gate(hidden_states).unflatten(-1, (attn.heads, -1))
|
| 360 |
+
out = sparse_attention(query, key, value, gate_compress, geometry)
|
| 361 |
+
|
| 362 |
+
out = out.flatten(2, 3).type_as(query)
|
| 363 |
+
out = attn.to_out[0](out)
|
| 364 |
+
return attn.to_out[1](out)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def add_gate_compress_modules() -> None:
|
| 368 |
+
"""Give every block's attention the `to_gate_compress` projection the base `diffusers` port has no use for.
|
| 369 |
+
|
| 370 |
+
The trained gate lives in the checkpoint as 50 `transformer_blocks.*.attn.to_gate_compress.weight` tensors, but
|
| 371 |
+
`MiniMaxH3Attention` does not declare the module, so `from_pretrained` reports them as unexpected and drops them.
|
| 372 |
+
Declaring it before the transformer is instantiated is what makes them load.
|
| 373 |
+
"""
|
| 374 |
+
from diffusers.models.transformers import transformer_minimax_h3 as module
|
| 375 |
+
|
| 376 |
+
block_cls = module.MiniMaxH3TransformerBlock
|
| 377 |
+
if getattr(block_cls, "_vsa_gate_patched", False):
|
| 378 |
+
return
|
| 379 |
+
original_init = block_cls.__init__
|
| 380 |
+
|
| 381 |
+
def patched_init(self, hidden_size, num_attention_heads, attention_head_dim, *args, **kwargs):
|
| 382 |
+
original_init(self, hidden_size, num_attention_heads, attention_head_dim, *args, **kwargs)
|
| 383 |
+
self.attn.to_gate_compress = torch.nn.Linear(
|
| 384 |
+
hidden_size, num_attention_heads * attention_head_dim, bias=False
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
block_cls.__init__ = patched_init
|
| 388 |
+
block_cls._vsa_gate_patched = True
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def install(transformer, sparsity: float = DEFAULT_SPARSITY) -> tuple[int, int]:
|
| 392 |
+
"""Put the VSA processor on the 50 packed-sequence blocks and wrap `forward` to publish the layout.
|
| 393 |
+
|
| 394 |
+
The token refiner has its own block class, over the *text* stream rather than the packed sequence, so it is
|
| 395 |
+
untouched. Returns `(blocks, live gates)`.
|
| 396 |
+
"""
|
| 397 |
+
installed = 0
|
| 398 |
+
gates = 0
|
| 399 |
+
for block in transformer.transformer_blocks:
|
| 400 |
+
gate = getattr(block.attn, "to_gate_compress", None)
|
| 401 |
+
if gate is not None and not bool((gate.weight != 0).any()):
|
| 402 |
+
# FastVideo's `_gate_active`: a zero (not-yet-finetuned) gate makes the branch a guaranteed zero — a full
|
| 403 |
+
# GEMM plus a pooled attention per layer for nothing — so drop it rather than pay for it.
|
| 404 |
+
block.attn.to_gate_compress = None
|
| 405 |
+
elif gate is not None:
|
| 406 |
+
gates += 1
|
| 407 |
+
block.attn.set_processor(MiniMaxH3VSAAttnProcessor())
|
| 408 |
+
installed += 1
|
| 409 |
+
|
| 410 |
+
if getattr(transformer, "_vsa_forward_wrapped", False):
|
| 411 |
+
return installed, gates
|
| 412 |
+
original_forward = transformer.forward
|
| 413 |
+
|
| 414 |
+
@functools.wraps(original_forward)
|
| 415 |
+
def forward(*args, **kwargs):
|
| 416 |
+
token_tags = kwargs.get("token_tags")
|
| 417 |
+
position_ids = kwargs.get("position_ids")
|
| 418 |
+
if token_tags is None or position_ids is None:
|
| 419 |
+
# `MiniMaxH3LoopDenoiser` passes the layout by keyword; bind positionally only as a fallback.
|
| 420 |
+
import inspect
|
| 421 |
+
|
| 422 |
+
bound = inspect.signature(original_forward).bind_partial(*args, **kwargs).arguments
|
| 423 |
+
token_tags = token_tags if token_tags is not None else bound.get("token_tags")
|
| 424 |
+
position_ids = position_ids if position_ids is not None else bound.get("position_ids")
|
| 425 |
+
geometry = None
|
| 426 |
+
if token_tags is not None and position_ids is not None:
|
| 427 |
+
geometry = geometry_from_layout(token_tags, position_ids, sparsity)
|
| 428 |
+
if geometry is None:
|
| 429 |
+
_report("DENSE FALLBACK: no VSA geometry could be derived from this forward's layout")
|
| 430 |
+
else:
|
| 431 |
+
_report(
|
| 432 |
+
f"sparse: seq={geometry.seq_len} padded={geometry.padded_len} tiles={geometry.n_tiles} "
|
| 433 |
+
f"(prefix {geometry.num_prefix_tiles} + video {geometry.num_video_tiles}) "
|
| 434 |
+
f"topk={geometry.topk} ({geometry.topk / geometry.num_video_tiles:.1%} of video tiles kept)"
|
| 435 |
+
)
|
| 436 |
+
_ACTIVE["geometry"] = geometry
|
| 437 |
+
try:
|
| 438 |
+
return original_forward(*args, **kwargs)
|
| 439 |
+
finally:
|
| 440 |
+
_ACTIVE["geometry"] = None
|
| 441 |
+
|
| 442 |
+
transformer.forward = forward
|
| 443 |
+
transformer._vsa_forward_wrapped = True
|
| 444 |
+
return installed, gates
|
vsa_kernel/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vendored FastVideo block-sparse attention Triton kernels.
|
| 2 |
+
|
| 3 |
+
Copied verbatim from `fastvideo-kernel` (https://github.com/hao-ai-lab/FastVideo,
|
| 4 |
+
`fastvideo-kernel/python/fastvideo_kernel/triton_kernels/`), Apache License 2.0.
|
| 5 |
+
|
| 6 |
+
Only the two pure-Triton modules are vendored: the package's default route is a
|
| 7 |
+
CUDA extension that has to be compiled per architecture (and whose fastest entry,
|
| 8 |
+
`block_sparse_attn_sm100a`, is GB200-only), while FastVideo's own
|
| 9 |
+
`--vsa-kernel triton` route -- these two files -- runs anywhere Triton does and
|
| 10 |
+
computes exactly the same masked attention.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from .block_sparse_attn_triton import triton_block_sparse_attn_forward
|
| 14 |
+
from .index import map_to_index
|
| 15 |
+
|
| 16 |
+
__all__ = ["triton_block_sparse_attn_forward", "map_to_index"]
|
vsa_kernel/block_sparse_attn_triton.py
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fused Attention
|
| 3 |
+
===============
|
| 4 |
+
|
| 5 |
+
This is a Triton implementation of the Flash Attention v2 algorithm from Tri Dao
|
| 6 |
+
(https://tridao.me/publications/flash2/flash2.pdf)
|
| 7 |
+
|
| 8 |
+
Credits: OpenAI kernel team
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import triton
|
| 13 |
+
import triton.language as tl
|
| 14 |
+
|
| 15 |
+
# ──────────────────────────── SPARSE ADDITION BEGIN ───────────────────────────
|
| 16 |
+
import math # small utility needed by the sparse wrapper
|
| 17 |
+
# ──────────────────────────── SPARSE ADDITION END ─────────────────────────────
|
| 18 |
+
|
| 19 |
+
# BLOCK_M / BLOCK_N are fixed at 64 because they are structural, not tunable:
|
| 20 |
+
# the kernel indexes the top-k list per BLOCK_M q-tile and addresses keys as
|
| 21 |
+
# kv_idx * BLOCK_N, so both must match the granularity q2k_index and
|
| 22 |
+
# variable_block_sizes were built at.
|
| 23 |
+
#
|
| 24 |
+
# num_stages / num_warps ARE free, and the previous {3, 4, 7} was inherited from
|
| 25 |
+
# the upstream tutorial rather than tuned here. It skips 5 and 6; on Blackwell
|
| 26 |
+
# (sm_121) the optimum is num_stages=5, so the search could not reach it. Both
|
| 27 |
+
# block paths independently select 5 once it is available. Autotune still picks
|
| 28 |
+
# per architecture, so other GPUs re-tune rather than inheriting this choice.
|
| 29 |
+
#
|
| 30 |
+
# VENDORING NOTE (the only edit made to this file): Triton re-benchmarks the sweep below on every new `N_CTX_Q` --
|
| 31 |
+
# i.e. on every canvas and duration a visitor picks, and again in each fresh ZeroGPU worker -- and at these sequence
|
| 32 |
+
# lengths one bench round costs seconds of the visitor's GPU quota. Triton skips benchmarking altogether when a
|
| 33 |
+
# single config is offered, so this pins the point the comment above says the search lands on for Blackwell.
|
| 34 |
+
# `H3_VSA_AUTOTUNE=1` restores the upstream sweep.
|
| 35 |
+
import os as _os
|
| 36 |
+
|
| 37 |
+
if _os.environ.get("H3_VSA_AUTOTUNE", "0") == "1":
|
| 38 |
+
configs = [
|
| 39 |
+
triton.Config({'BLOCK_M': BM, 'BLOCK_N': BN}, num_stages=s, num_warps=w) \
|
| 40 |
+
for BM in [64]\
|
| 41 |
+
for BN in [64]\
|
| 42 |
+
for s in [2, 3, 4, 5, 6, 7]\
|
| 43 |
+
for w in [4, 8]\
|
| 44 |
+
]
|
| 45 |
+
else:
|
| 46 |
+
configs = [
|
| 47 |
+
triton.Config({
|
| 48 |
+
'BLOCK_M': 64,
|
| 49 |
+
'BLOCK_N': 64
|
| 50 |
+
},
|
| 51 |
+
num_stages=int(_os.environ.get("H3_VSA_STAGES", "5")),
|
| 52 |
+
num_warps=int(_os.environ.get("H3_VSA_WARPS", "4")))
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ──────────────────────────── SPARSE ADDITION BEGIN ───────────────────────────
|
| 57 |
+
@triton.autotune(configs, key=["N_CTX_Q", "HEAD_DIM"])
|
| 58 |
+
@triton.jit
|
| 59 |
+
def _attn_fwd_sparse(
|
| 60 |
+
Q,
|
| 61 |
+
K,
|
| 62 |
+
V,
|
| 63 |
+
sm_scale, #
|
| 64 |
+
q2k_index,
|
| 65 |
+
q2k_num,
|
| 66 |
+
max_kv_blks, #
|
| 67 |
+
variable_block_sizes,
|
| 68 |
+
M,
|
| 69 |
+
Out, #
|
| 70 |
+
stride_qz,
|
| 71 |
+
stride_qh,
|
| 72 |
+
stride_qm,
|
| 73 |
+
stride_qk,
|
| 74 |
+
stride_kz,
|
| 75 |
+
stride_kh,
|
| 76 |
+
stride_kn,
|
| 77 |
+
stride_kk,
|
| 78 |
+
stride_vz,
|
| 79 |
+
stride_vh,
|
| 80 |
+
stride_vk,
|
| 81 |
+
stride_vn,
|
| 82 |
+
stride_oz,
|
| 83 |
+
stride_oh,
|
| 84 |
+
stride_om,
|
| 85 |
+
stride_on,
|
| 86 |
+
Z,
|
| 87 |
+
H,
|
| 88 |
+
N_CTX_Q, #
|
| 89 |
+
N_CTX_KV, #
|
| 90 |
+
HEAD_DIM: tl.constexpr, #
|
| 91 |
+
BLOCK_M: tl.constexpr,
|
| 92 |
+
BLOCK_N: tl.constexpr,
|
| 93 |
+
STAGE: tl.constexpr):
|
| 94 |
+
"""
|
| 95 |
+
64×64 **block-sparse** forward kernel. Back-prop kernels remain dense
|
| 96 |
+
(32×64 and 64×32) – memory footprint unchanged.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
# ----- program-id mapping -----
|
| 100 |
+
q_blk = tl.program_id(0) # Q-tile index
|
| 101 |
+
off_hz = tl.program_id(1) # fused (batch, head)
|
| 102 |
+
b = off_hz // H
|
| 103 |
+
h = off_hz % H
|
| 104 |
+
q_tiles = N_CTX_Q // BLOCK_M
|
| 105 |
+
meta_base = ((b * H + h) * q_tiles + q_blk)
|
| 106 |
+
|
| 107 |
+
kv_blocks = tl.load(q2k_num + meta_base) # int32
|
| 108 |
+
kv_ptr = q2k_index + meta_base * max_kv_blks # ptr to list
|
| 109 |
+
|
| 110 |
+
# ----- base pointers -----
|
| 111 |
+
# Note: when q and kv have different sequence lengths, their per-(batch,head)
|
| 112 |
+
# strides differ, so we must compute separate base offsets.
|
| 113 |
+
q_off = (b.to(tl.int64) * stride_qz + h.to(tl.int64) * stride_qh)
|
| 114 |
+
k_off = (b.to(tl.int64) * stride_kz + h.to(tl.int64) * stride_kh)
|
| 115 |
+
v_off = (b.to(tl.int64) * stride_vz + h.to(tl.int64) * stride_vh)
|
| 116 |
+
o_off = (b.to(tl.int64) * stride_oz + h.to(tl.int64) * stride_oh)
|
| 117 |
+
|
| 118 |
+
Q_ptr = tl.make_block_ptr(base=Q + q_off,
|
| 119 |
+
shape=(N_CTX_Q, HEAD_DIM),
|
| 120 |
+
strides=(stride_qm, stride_qk),
|
| 121 |
+
offsets=(q_blk * BLOCK_M, 0),
|
| 122 |
+
block_shape=(BLOCK_M, HEAD_DIM),
|
| 123 |
+
order=(1, 0))
|
| 124 |
+
|
| 125 |
+
K_base = tl.make_block_ptr(base=K + k_off,
|
| 126 |
+
shape=(HEAD_DIM, N_CTX_KV),
|
| 127 |
+
strides=(stride_kk, stride_kn),
|
| 128 |
+
offsets=(0, 0),
|
| 129 |
+
block_shape=(HEAD_DIM, BLOCK_N),
|
| 130 |
+
order=(0, 1))
|
| 131 |
+
|
| 132 |
+
v_order: tl.constexpr = (0, 1) if V.dtype.element_ty == tl.float8e5 else (1, 0)
|
| 133 |
+
V_base = tl.make_block_ptr(base=V + v_off,
|
| 134 |
+
shape=(N_CTX_KV, HEAD_DIM),
|
| 135 |
+
strides=(stride_vk, stride_vn),
|
| 136 |
+
offsets=(0, 0),
|
| 137 |
+
block_shape=(BLOCK_N, HEAD_DIM),
|
| 138 |
+
order=v_order)
|
| 139 |
+
|
| 140 |
+
O_ptr = tl.make_block_ptr(base=Out + o_off,
|
| 141 |
+
shape=(N_CTX_Q, HEAD_DIM),
|
| 142 |
+
strides=(stride_om, stride_on),
|
| 143 |
+
offsets=(q_blk * BLOCK_M, 0),
|
| 144 |
+
block_shape=(BLOCK_M, HEAD_DIM),
|
| 145 |
+
order=(1, 0))
|
| 146 |
+
|
| 147 |
+
# ----- accumulators -----
|
| 148 |
+
offs_m = q_blk * BLOCK_M + tl.arange(0, BLOCK_M)
|
| 149 |
+
m_i = tl.full([BLOCK_M], -float("inf"), tl.float32)
|
| 150 |
+
l_i = tl.zeros([BLOCK_M], dtype=tl.float32) + 1.0
|
| 151 |
+
acc = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32)
|
| 152 |
+
qk_scale = sm_scale * 1.44269504 # 1/ln2
|
| 153 |
+
q = tl.load(Q_ptr)
|
| 154 |
+
|
| 155 |
+
# ----- sparse loop over valid K/V tiles -----
|
| 156 |
+
for i in range(0, kv_blocks):
|
| 157 |
+
kv_idx = tl.load(kv_ptr + i).to(tl.int32)
|
| 158 |
+
block_size = tl.load(variable_block_sizes + kv_idx)
|
| 159 |
+
K_ptr = tl.advance(K_base, (0, kv_idx * BLOCK_N))
|
| 160 |
+
V_ptr = tl.advance(V_base, (kv_idx * BLOCK_N, 0))
|
| 161 |
+
|
| 162 |
+
k = tl.load(K_ptr)
|
| 163 |
+
qk = tl.dot(q, k)
|
| 164 |
+
# mask out invalid columns
|
| 165 |
+
mask = tl.arange(0, BLOCK_N) < block_size
|
| 166 |
+
qk = tl.where(mask[None, :], qk, -float("inf"))
|
| 167 |
+
|
| 168 |
+
m_ij = tl.maximum(m_i, tl.max(qk, 1) * qk_scale)
|
| 169 |
+
p = tl.math.exp2(qk * qk_scale - m_ij[:, None])
|
| 170 |
+
l_ij = tl.sum(p, 1)
|
| 171 |
+
|
| 172 |
+
alpha = tl.math.exp2(m_i - m_ij)
|
| 173 |
+
l_i = l_i * alpha + l_ij
|
| 174 |
+
acc = acc * alpha[:, None]
|
| 175 |
+
|
| 176 |
+
v = tl.load(V_ptr)
|
| 177 |
+
acc = tl.dot(p.to(tl.bfloat16), v, acc)
|
| 178 |
+
m_i = m_ij
|
| 179 |
+
|
| 180 |
+
# ----- epilogue -----
|
| 181 |
+
m_i += tl.math.log2(l_i)
|
| 182 |
+
acc = acc / l_i[:, None]
|
| 183 |
+
tl.store(M + off_hz * N_CTX_Q + offs_m, m_i)
|
| 184 |
+
tl.store(O_ptr, acc.to(Out.type.element_ty))
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
# ──────────────────────────── SPARSE ADDITION END ─────────────────────────────
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@triton.jit
|
| 191 |
+
def _attn_bwd_preprocess(
|
| 192 |
+
O,
|
| 193 |
+
DO, #
|
| 194 |
+
Delta, #
|
| 195 |
+
Z,
|
| 196 |
+
H,
|
| 197 |
+
N_CTX, #
|
| 198 |
+
BLOCK_M: tl.constexpr,
|
| 199 |
+
HEAD_DIM: tl.constexpr #
|
| 200 |
+
):
|
| 201 |
+
off_m = tl.program_id(0) * BLOCK_M + tl.arange(0, BLOCK_M)
|
| 202 |
+
off_hz = tl.program_id(1)
|
| 203 |
+
off_n = tl.arange(0, HEAD_DIM)
|
| 204 |
+
# load
|
| 205 |
+
o = tl.load(O + off_hz * HEAD_DIM * N_CTX + off_m[:, None] * HEAD_DIM + off_n[None, :])
|
| 206 |
+
do = tl.load(DO + off_hz * HEAD_DIM * N_CTX + off_m[:, None] * HEAD_DIM + off_n[None, :]).to(tl.float32)
|
| 207 |
+
delta = tl.sum(o * do, axis=1)
|
| 208 |
+
# write-back
|
| 209 |
+
tl.store(Delta + off_hz * N_CTX + off_m, delta)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# The main inner-loop logic for computing dK and dV.
|
| 213 |
+
@triton.jit
|
| 214 |
+
def _attn_bwd_dkdv(
|
| 215 |
+
dk,
|
| 216 |
+
dv, #
|
| 217 |
+
Q,
|
| 218 |
+
k,
|
| 219 |
+
v,
|
| 220 |
+
sm_scale, #
|
| 221 |
+
DO, #
|
| 222 |
+
M,
|
| 223 |
+
D, #
|
| 224 |
+
k2q_index,
|
| 225 |
+
k2q_num,
|
| 226 |
+
max_q_blks,
|
| 227 |
+
variable_block_sizes,
|
| 228 |
+
# shared by Q/K/V/DO.
|
| 229 |
+
stride_tok,
|
| 230 |
+
stride_d, #
|
| 231 |
+
H,
|
| 232 |
+
N_CTX_KV,
|
| 233 |
+
BLOCK_M1: tl.constexpr, #
|
| 234 |
+
BLOCK_N1: tl.constexpr, #
|
| 235 |
+
HEAD_DIM: tl.constexpr, #
|
| 236 |
+
# Filled in by the wrapper.
|
| 237 |
+
start_n,
|
| 238 |
+
start_m,
|
| 239 |
+
num_steps):
|
| 240 |
+
offs_m = start_m + tl.arange(0, BLOCK_M1)
|
| 241 |
+
offs_n = start_n + tl.arange(0, BLOCK_N1)
|
| 242 |
+
offs_k = tl.arange(0, HEAD_DIM)
|
| 243 |
+
qT_ptrs = Q + offs_m[None, :] * stride_tok + offs_k[:, None] * stride_d
|
| 244 |
+
do_ptrs = DO + offs_m[:, None] * stride_tok + offs_k[None, :] * stride_d
|
| 245 |
+
# BLOCK_N1 must be a multiple of BLOCK_M1, otherwise the code wouldn't work.
|
| 246 |
+
tl.static_assert(BLOCK_N1 % BLOCK_M1 == 0)
|
| 247 |
+
step_m = BLOCK_M1
|
| 248 |
+
kv_blk = tl.program_id(0) # Q-tile index
|
| 249 |
+
off_hz = tl.program_id(2) # fused (batch, head)
|
| 250 |
+
b = off_hz // H
|
| 251 |
+
h = off_hz % H
|
| 252 |
+
kv_tiles = N_CTX_KV // BLOCK_N1
|
| 253 |
+
meta_base = ((b * H + h) * kv_tiles + kv_blk)
|
| 254 |
+
|
| 255 |
+
q_blocks = tl.load(k2q_num + meta_base) # int32
|
| 256 |
+
q_ptr = k2q_index + meta_base * max_q_blks # ptr to list
|
| 257 |
+
block_size = tl.load(variable_block_sizes + kv_blk)
|
| 258 |
+
|
| 259 |
+
for blk_idx in range(q_blocks * 2):
|
| 260 |
+
block_sparse_offset = (tl.load(q_ptr + blk_idx // 2).to(tl.int32) * 2 + blk_idx % 2) * step_m
|
| 261 |
+
qT = tl.load(qT_ptrs + block_sparse_offset * stride_tok)
|
| 262 |
+
# Load m before computing qk to reduce pipeline stall.
|
| 263 |
+
offs_m = start_m + block_sparse_offset + tl.arange(0, BLOCK_M1)
|
| 264 |
+
m = tl.load(M + offs_m)
|
| 265 |
+
# Recompute logits exactly as the forward does: raw bf16 operands into
|
| 266 |
+
# the dot, fp32 scale after accumulation. A bf16 pre-scaled K perturbs
|
| 267 |
+
# the recomputed logits relative to the saved M by an error
|
| 268 |
+
# proportional to |logit|, which exp2 amplifies into arbitrarily wrong
|
| 269 |
+
# probabilities at large activations.
|
| 270 |
+
qkT = tl.dot(k, qT) * (sm_scale * 1.4426950408889634)
|
| 271 |
+
pT = tl.math.exp2(qkT - m[None, :])
|
| 272 |
+
mask = tl.arange(0, BLOCK_N1) < block_size
|
| 273 |
+
pT = tl.where(mask[:, None], pT, 0.0)
|
| 274 |
+
|
| 275 |
+
do = tl.load(do_ptrs + block_sparse_offset * stride_tok)
|
| 276 |
+
# Compute dV.
|
| 277 |
+
ppT = pT
|
| 278 |
+
ppT = ppT.to(tl.bfloat16)
|
| 279 |
+
dv += tl.dot(ppT, do)
|
| 280 |
+
# D (= delta) is pre-divided by ds_scale.
|
| 281 |
+
Di = tl.load(D + offs_m)
|
| 282 |
+
# Compute dP and dS.
|
| 283 |
+
dpT = tl.dot(v, tl.trans(do)).to(tl.float32)
|
| 284 |
+
dsT = pT * (dpT - Di[None, :])
|
| 285 |
+
dsT = dsT.to(tl.bfloat16)
|
| 286 |
+
dk += tl.dot(dsT, tl.trans(qT))
|
| 287 |
+
# Increment pointers.
|
| 288 |
+
return dk, dv
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
# the main inner-loop logic for computing dQ
|
| 292 |
+
@triton.jit
|
| 293 |
+
def _attn_bwd_dq(
|
| 294 |
+
dq,
|
| 295 |
+
q,
|
| 296 |
+
K,
|
| 297 |
+
V, #
|
| 298 |
+
do,
|
| 299 |
+
m,
|
| 300 |
+
D,
|
| 301 |
+
sm_scale,
|
| 302 |
+
# shared by Q/K/V/DO.
|
| 303 |
+
q2k_index,
|
| 304 |
+
q2k_num,
|
| 305 |
+
max_kv_blks,
|
| 306 |
+
variable_block_sizes,
|
| 307 |
+
stride_tok,
|
| 308 |
+
stride_d, #
|
| 309 |
+
H,
|
| 310 |
+
N_CTX, #
|
| 311 |
+
BLOCK_M2: tl.constexpr, #
|
| 312 |
+
BLOCK_N2: tl.constexpr, #
|
| 313 |
+
HEAD_DIM: tl.constexpr,
|
| 314 |
+
# Filled in by the wrapper.
|
| 315 |
+
start_m,
|
| 316 |
+
start_n,
|
| 317 |
+
num_steps):
|
| 318 |
+
offs_m = start_m + tl.arange(0, BLOCK_M2)
|
| 319 |
+
offs_n = start_n + tl.arange(0, BLOCK_N2)
|
| 320 |
+
offs_k = tl.arange(0, HEAD_DIM)
|
| 321 |
+
kT_ptrs = K + offs_n[None, :] * stride_tok + offs_k[:, None] * stride_d
|
| 322 |
+
vT_ptrs = V + offs_n[None, :] * stride_tok + offs_k[:, None] * stride_d
|
| 323 |
+
# D (= delta) is pre-divided by ds_scale.
|
| 324 |
+
Di = tl.load(D + offs_m)
|
| 325 |
+
# BLOCK_M2 must be a multiple of BLOCK_N2, otherwise the code wouldn't work.
|
| 326 |
+
tl.static_assert(BLOCK_M2 % BLOCK_N2 == 0)
|
| 327 |
+
step_n = BLOCK_N2
|
| 328 |
+
|
| 329 |
+
q_blk = tl.program_id(0) # Q-tile index
|
| 330 |
+
off_hz = tl.program_id(2) # fused (batch, head)
|
| 331 |
+
b = off_hz // H
|
| 332 |
+
h = off_hz % H
|
| 333 |
+
q_tiles = N_CTX // BLOCK_M2
|
| 334 |
+
meta_base = ((b * H + h) * q_tiles + q_blk)
|
| 335 |
+
|
| 336 |
+
kv_blocks = tl.load(q2k_num + meta_base) # int32
|
| 337 |
+
kv_ptr = q2k_index + meta_base * max_kv_blks # ptr to list
|
| 338 |
+
|
| 339 |
+
for blk_idx in range(kv_blocks * 2):
|
| 340 |
+
kv_idx = tl.load(kv_ptr + blk_idx // 2).to(tl.int32)
|
| 341 |
+
# variable_block_sizes is defined per KV block (tile). Mask must therefore
|
| 342 |
+
# use kv_idx (not q_blk). Also, because we split each 64-token block into
|
| 343 |
+
# two 32-token halves, the mask must account for the half-block offset.
|
| 344 |
+
block_size = tl.load(variable_block_sizes + kv_idx).to(tl.int32)
|
| 345 |
+
half = (blk_idx % 2).to(tl.int32)
|
| 346 |
+
block_sparse_offset = (kv_idx * 2 + half) * step_n * stride_tok
|
| 347 |
+
kT = tl.load(kT_ptrs + block_sparse_offset)
|
| 348 |
+
vT = tl.load(vT_ptrs + block_sparse_offset)
|
| 349 |
+
qk = tl.dot(q, kT) * (sm_scale * 1.4426950408889634)
|
| 350 |
+
p = tl.math.exp2(qk - m)
|
| 351 |
+
offs_in_block = half * step_n + tl.arange(0, BLOCK_N2)
|
| 352 |
+
mask = offs_in_block < block_size
|
| 353 |
+
p = tl.where(mask[None, :], p, 0.0)
|
| 354 |
+
# Compute dP and dS.
|
| 355 |
+
dp = tl.dot(do, vT).to(tl.float32)
|
| 356 |
+
ds = p * (dp - Di[:, None])
|
| 357 |
+
ds = ds.to(tl.bfloat16)
|
| 358 |
+
# Compute dQ (kT is raw; the caller applies sm_scale once at the end).
|
| 359 |
+
dq += tl.dot(ds, tl.trans(kT))
|
| 360 |
+
# Increment pointers.
|
| 361 |
+
return dq
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
@triton.jit
|
| 365 |
+
def _attn_bwd(
|
| 366 |
+
Q,
|
| 367 |
+
K,
|
| 368 |
+
V,
|
| 369 |
+
sm_scale, #
|
| 370 |
+
DO, #
|
| 371 |
+
DQ,
|
| 372 |
+
DK,
|
| 373 |
+
DV, #
|
| 374 |
+
M,
|
| 375 |
+
D,
|
| 376 |
+
q2k_index,
|
| 377 |
+
q2k_num,
|
| 378 |
+
max_kv_blks,
|
| 379 |
+
k2q_index,
|
| 380 |
+
k2q_num,
|
| 381 |
+
max_q_blks,
|
| 382 |
+
variable_block_sizes,
|
| 383 |
+
# shared by Q/K/V/DO.
|
| 384 |
+
stride_z,
|
| 385 |
+
stride_h,
|
| 386 |
+
stride_tok,
|
| 387 |
+
stride_d, #
|
| 388 |
+
H,
|
| 389 |
+
N_CTX, #
|
| 390 |
+
BLOCK_M1: tl.constexpr, #
|
| 391 |
+
BLOCK_N1: tl.constexpr, #
|
| 392 |
+
BLOCK_M2: tl.constexpr, #
|
| 393 |
+
BLOCK_N2: tl.constexpr, #
|
| 394 |
+
HEAD_DIM: tl.constexpr):
|
| 395 |
+
LN2 = 0.6931471824645996 # = ln(2)
|
| 396 |
+
|
| 397 |
+
bhid = tl.program_id(2)
|
| 398 |
+
off_chz = (bhid * N_CTX).to(tl.int64)
|
| 399 |
+
adj = (stride_h * (bhid % H) + stride_z * (bhid // H)).to(tl.int64)
|
| 400 |
+
pid = tl.program_id(0)
|
| 401 |
+
|
| 402 |
+
# offset pointers for batch/head
|
| 403 |
+
Q += adj
|
| 404 |
+
K += adj
|
| 405 |
+
V += adj
|
| 406 |
+
DO += adj
|
| 407 |
+
DQ += adj
|
| 408 |
+
DK += adj
|
| 409 |
+
DV += adj
|
| 410 |
+
M += off_chz
|
| 411 |
+
D += off_chz
|
| 412 |
+
|
| 413 |
+
# load scales
|
| 414 |
+
offs_k = tl.arange(0, HEAD_DIM)
|
| 415 |
+
|
| 416 |
+
start_n = pid * BLOCK_N1
|
| 417 |
+
start_m = 0
|
| 418 |
+
|
| 419 |
+
offs_n = start_n + tl.arange(0, BLOCK_N1)
|
| 420 |
+
|
| 421 |
+
dv = tl.zeros([BLOCK_N1, HEAD_DIM], dtype=tl.float32)
|
| 422 |
+
dk = tl.zeros([BLOCK_N1, HEAD_DIM], dtype=tl.float32)
|
| 423 |
+
|
| 424 |
+
# load K and V: they stay in SRAM throughout the inner loop.
|
| 425 |
+
k = tl.load(K + offs_n[:, None] * stride_tok + offs_k[None, :] * stride_d)
|
| 426 |
+
v = tl.load(V + offs_n[:, None] * stride_tok + offs_k[None, :] * stride_d)
|
| 427 |
+
|
| 428 |
+
num_steps = N_CTX // BLOCK_M1
|
| 429 |
+
|
| 430 |
+
dk, dv = _attn_bwd_dkdv( #
|
| 431 |
+
dk,
|
| 432 |
+
dv, #
|
| 433 |
+
Q,
|
| 434 |
+
k,
|
| 435 |
+
v,
|
| 436 |
+
sm_scale, #
|
| 437 |
+
DO, #
|
| 438 |
+
M,
|
| 439 |
+
D, #
|
| 440 |
+
k2q_index,
|
| 441 |
+
k2q_num,
|
| 442 |
+
max_q_blks,
|
| 443 |
+
variable_block_sizes,
|
| 444 |
+
stride_tok,
|
| 445 |
+
stride_d, #
|
| 446 |
+
H,
|
| 447 |
+
N_CTX, #
|
| 448 |
+
BLOCK_M1,
|
| 449 |
+
BLOCK_N1,
|
| 450 |
+
HEAD_DIM, #
|
| 451 |
+
start_n,
|
| 452 |
+
start_m,
|
| 453 |
+
num_steps #
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
dv_ptrs = DV + offs_n[:, None] * stride_tok + offs_k[None, :] * stride_d
|
| 457 |
+
tl.store(dv_ptrs, dv)
|
| 458 |
+
|
| 459 |
+
# Write back dK.
|
| 460 |
+
dk *= sm_scale
|
| 461 |
+
dk_ptrs = DK + offs_n[:, None] * stride_tok + offs_k[None, :] * stride_d
|
| 462 |
+
tl.store(dk_ptrs, dk)
|
| 463 |
+
|
| 464 |
+
# THIS BLOCK DOES DQ:
|
| 465 |
+
start_m = pid * BLOCK_M2
|
| 466 |
+
end_n = 0
|
| 467 |
+
|
| 468 |
+
offs_m = start_m + tl.arange(0, BLOCK_M2)
|
| 469 |
+
|
| 470 |
+
q = tl.load(Q + offs_m[:, None] * stride_tok + offs_k[None, :] * stride_d)
|
| 471 |
+
dq = tl.zeros([BLOCK_M2, HEAD_DIM], dtype=tl.float32)
|
| 472 |
+
do = tl.load(DO + offs_m[:, None] * stride_tok + offs_k[None, :] * stride_d)
|
| 473 |
+
|
| 474 |
+
m = tl.load(M + offs_m)
|
| 475 |
+
m = m[:, None]
|
| 476 |
+
|
| 477 |
+
num_steps = N_CTX // BLOCK_N2
|
| 478 |
+
dq = _attn_bwd_dq(
|
| 479 |
+
dq,
|
| 480 |
+
q,
|
| 481 |
+
K,
|
| 482 |
+
V, #
|
| 483 |
+
do,
|
| 484 |
+
m,
|
| 485 |
+
D, #
|
| 486 |
+
sm_scale,
|
| 487 |
+
q2k_index,
|
| 488 |
+
q2k_num,
|
| 489 |
+
max_kv_blks,
|
| 490 |
+
variable_block_sizes,
|
| 491 |
+
stride_tok,
|
| 492 |
+
stride_d, #
|
| 493 |
+
H,
|
| 494 |
+
N_CTX, #
|
| 495 |
+
BLOCK_M2,
|
| 496 |
+
BLOCK_N2,
|
| 497 |
+
HEAD_DIM, #
|
| 498 |
+
start_m,
|
| 499 |
+
end_n,
|
| 500 |
+
num_steps #
|
| 501 |
+
)
|
| 502 |
+
# Write back dQ.
|
| 503 |
+
dq_ptrs = DQ + offs_m[:, None] * stride_tok + offs_k[None, :] * stride_d
|
| 504 |
+
dq *= sm_scale
|
| 505 |
+
tl.store(dq_ptrs, dq)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
@triton.jit
|
| 509 |
+
def _attn_bwd_dkdv_kernel(
|
| 510 |
+
Q,
|
| 511 |
+
K,
|
| 512 |
+
V,
|
| 513 |
+
sm_scale, #
|
| 514 |
+
DO, #
|
| 515 |
+
DK,
|
| 516 |
+
DV, #
|
| 517 |
+
M,
|
| 518 |
+
D,
|
| 519 |
+
k2q_index,
|
| 520 |
+
k2q_num,
|
| 521 |
+
max_q_blks,
|
| 522 |
+
variable_block_sizes,
|
| 523 |
+
# shared token/dim strides (assumed contiguous along token and dim)
|
| 524 |
+
stride_tok,
|
| 525 |
+
stride_d, #
|
| 526 |
+
# batch/head strides (may differ between Q and KV)
|
| 527 |
+
stride_qz,
|
| 528 |
+
stride_qh,
|
| 529 |
+
stride_kz,
|
| 530 |
+
stride_kh,
|
| 531 |
+
stride_vz,
|
| 532 |
+
stride_vh,
|
| 533 |
+
stride_doz,
|
| 534 |
+
stride_doh,
|
| 535 |
+
stride_dkz,
|
| 536 |
+
stride_dkh,
|
| 537 |
+
stride_dvz,
|
| 538 |
+
stride_dvh,
|
| 539 |
+
H,
|
| 540 |
+
N_CTX_Q,
|
| 541 |
+
N_CTX_KV,
|
| 542 |
+
BLOCK_M1: tl.constexpr, #
|
| 543 |
+
BLOCK_N1: tl.constexpr, #
|
| 544 |
+
HEAD_DIM: tl.constexpr):
|
| 545 |
+
"""
|
| 546 |
+
Backward kernel that computes dK and dV for each KV block (64 tokens).
|
| 547 |
+
Grid:
|
| 548 |
+
pid0: kv_blk in [0, N_CTX_KV/BLOCK_N1)
|
| 549 |
+
pid2: fused (batch, head) in [0, B*H)
|
| 550 |
+
"""
|
| 551 |
+
bhid = tl.program_id(2)
|
| 552 |
+
b = bhid // H
|
| 553 |
+
h = bhid % H
|
| 554 |
+
kv_blk = tl.program_id(0)
|
| 555 |
+
|
| 556 |
+
q_adj = (b.to(tl.int64) * stride_qz + h.to(tl.int64) * stride_qh)
|
| 557 |
+
kv_adj_k = (b.to(tl.int64) * stride_kz + h.to(tl.int64) * stride_kh)
|
| 558 |
+
kv_adj_v = (b.to(tl.int64) * stride_vz + h.to(tl.int64) * stride_vh)
|
| 559 |
+
do_adj = (b.to(tl.int64) * stride_doz + h.to(tl.int64) * stride_doh)
|
| 560 |
+
dk_adj = (b.to(tl.int64) * stride_dkz + h.to(tl.int64) * stride_dkh)
|
| 561 |
+
dv_adj = (b.to(tl.int64) * stride_dvz + h.to(tl.int64) * stride_dvh)
|
| 562 |
+
|
| 563 |
+
Q = Q + q_adj
|
| 564 |
+
K = K + kv_adj_k
|
| 565 |
+
V = V + kv_adj_v
|
| 566 |
+
DO = DO + do_adj
|
| 567 |
+
DK = DK + dk_adj
|
| 568 |
+
DV = DV + dv_adj
|
| 569 |
+
|
| 570 |
+
# M and D (delta) are always sized by Q length.
|
| 571 |
+
M = M + (bhid * N_CTX_Q).to(tl.int64)
|
| 572 |
+
D = D + (bhid * N_CTX_Q).to(tl.int64)
|
| 573 |
+
|
| 574 |
+
offs_k = tl.arange(0, HEAD_DIM)
|
| 575 |
+
start_n = kv_blk * BLOCK_N1
|
| 576 |
+
offs_n = start_n + tl.arange(0, BLOCK_N1)
|
| 577 |
+
|
| 578 |
+
# load K and V: they stay in SRAM throughout the inner loop.
|
| 579 |
+
k = tl.load(K + offs_n[:, None] * stride_tok + offs_k[None, :] * stride_d)
|
| 580 |
+
v = tl.load(V + offs_n[:, None] * stride_tok + offs_k[None, :] * stride_d)
|
| 581 |
+
|
| 582 |
+
dv_acc = tl.zeros([BLOCK_N1, HEAD_DIM], dtype=tl.float32)
|
| 583 |
+
dk_acc = tl.zeros([BLOCK_N1, HEAD_DIM], dtype=tl.float32)
|
| 584 |
+
|
| 585 |
+
num_steps = N_CTX_Q // BLOCK_M1
|
| 586 |
+
dk_acc, dv_acc = _attn_bwd_dkdv(
|
| 587 |
+
dk_acc,
|
| 588 |
+
dv_acc,
|
| 589 |
+
Q,
|
| 590 |
+
k,
|
| 591 |
+
v,
|
| 592 |
+
sm_scale,
|
| 593 |
+
DO,
|
| 594 |
+
M,
|
| 595 |
+
D,
|
| 596 |
+
k2q_index,
|
| 597 |
+
k2q_num,
|
| 598 |
+
max_q_blks,
|
| 599 |
+
variable_block_sizes,
|
| 600 |
+
stride_tok,
|
| 601 |
+
stride_d,
|
| 602 |
+
H,
|
| 603 |
+
N_CTX_KV,
|
| 604 |
+
BLOCK_M1=BLOCK_M1,
|
| 605 |
+
BLOCK_N1=BLOCK_N1,
|
| 606 |
+
HEAD_DIM=HEAD_DIM,
|
| 607 |
+
start_n=start_n,
|
| 608 |
+
start_m=0,
|
| 609 |
+
num_steps=num_steps,
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
dv_ptrs = DV + offs_n[:, None] * stride_tok + offs_k[None, :] * stride_d
|
| 613 |
+
tl.store(dv_ptrs, dv_acc)
|
| 614 |
+
|
| 615 |
+
dk_acc *= sm_scale
|
| 616 |
+
dk_ptrs = DK + offs_n[:, None] * stride_tok + offs_k[None, :] * stride_d
|
| 617 |
+
tl.store(dk_ptrs, dk_acc)
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
@triton.jit
|
| 621 |
+
def _attn_bwd_dq_kernel(
|
| 622 |
+
Q,
|
| 623 |
+
K,
|
| 624 |
+
V,
|
| 625 |
+
sm_scale,
|
| 626 |
+
DO, #
|
| 627 |
+
DQ,
|
| 628 |
+
M,
|
| 629 |
+
D,
|
| 630 |
+
q2k_index,
|
| 631 |
+
q2k_num,
|
| 632 |
+
max_kv_blks,
|
| 633 |
+
variable_block_sizes,
|
| 634 |
+
# shared token/dim strides (assumed contiguous along token and dim)
|
| 635 |
+
stride_tok,
|
| 636 |
+
stride_d, #
|
| 637 |
+
# batch/head strides (may differ between Q and KV)
|
| 638 |
+
stride_qz,
|
| 639 |
+
stride_qh,
|
| 640 |
+
stride_kz,
|
| 641 |
+
stride_kh,
|
| 642 |
+
stride_vz,
|
| 643 |
+
stride_vh,
|
| 644 |
+
stride_doz,
|
| 645 |
+
stride_doh,
|
| 646 |
+
stride_dqz,
|
| 647 |
+
stride_dqh,
|
| 648 |
+
H,
|
| 649 |
+
N_CTX_Q,
|
| 650 |
+
BLOCK_M2: tl.constexpr, #
|
| 651 |
+
BLOCK_N2: tl.constexpr, #
|
| 652 |
+
HEAD_DIM: tl.constexpr):
|
| 653 |
+
"""
|
| 654 |
+
Backward kernel that computes dQ for each Q block (64 tokens).
|
| 655 |
+
Grid:
|
| 656 |
+
pid0: q_blk in [0, N_CTX_Q/BLOCK_M2)
|
| 657 |
+
pid2: fused (batch, head) in [0, B*H)
|
| 658 |
+
"""
|
| 659 |
+
LN2 = 0.6931471824645996 # = ln(2)
|
| 660 |
+
bhid = tl.program_id(2)
|
| 661 |
+
b = bhid // H
|
| 662 |
+
h = bhid % H
|
| 663 |
+
q_blk = tl.program_id(0)
|
| 664 |
+
|
| 665 |
+
q_adj = (b.to(tl.int64) * stride_qz + h.to(tl.int64) * stride_qh)
|
| 666 |
+
kv_adj_k = (b.to(tl.int64) * stride_kz + h.to(tl.int64) * stride_kh)
|
| 667 |
+
kv_adj_v = (b.to(tl.int64) * stride_vz + h.to(tl.int64) * stride_vh)
|
| 668 |
+
do_adj = (b.to(tl.int64) * stride_doz + h.to(tl.int64) * stride_doh)
|
| 669 |
+
dq_adj = (b.to(tl.int64) * stride_dqz + h.to(tl.int64) * stride_dqh)
|
| 670 |
+
|
| 671 |
+
Q = Q + q_adj
|
| 672 |
+
K = K + kv_adj_k
|
| 673 |
+
V = V + kv_adj_v
|
| 674 |
+
DO = DO + do_adj
|
| 675 |
+
DQ = DQ + dq_adj
|
| 676 |
+
|
| 677 |
+
M = M + (bhid * N_CTX_Q).to(tl.int64)
|
| 678 |
+
D = D + (bhid * N_CTX_Q).to(tl.int64)
|
| 679 |
+
|
| 680 |
+
offs_k = tl.arange(0, HEAD_DIM)
|
| 681 |
+
start_m = q_blk * BLOCK_M2
|
| 682 |
+
offs_m = start_m + tl.arange(0, BLOCK_M2)
|
| 683 |
+
|
| 684 |
+
q = tl.load(Q + offs_m[:, None] * stride_tok + offs_k[None, :] * stride_d)
|
| 685 |
+
do = tl.load(DO + offs_m[:, None] * stride_tok + offs_k[None, :] * stride_d)
|
| 686 |
+
m = tl.load(M + offs_m)[:, None]
|
| 687 |
+
|
| 688 |
+
dq_acc = tl.zeros([BLOCK_M2, HEAD_DIM], dtype=tl.float32)
|
| 689 |
+
num_steps = 0 # unused in _attn_bwd_dq
|
| 690 |
+
dq_acc = _attn_bwd_dq(
|
| 691 |
+
dq_acc,
|
| 692 |
+
q,
|
| 693 |
+
K,
|
| 694 |
+
V,
|
| 695 |
+
do,
|
| 696 |
+
m,
|
| 697 |
+
D,
|
| 698 |
+
sm_scale,
|
| 699 |
+
q2k_index,
|
| 700 |
+
q2k_num,
|
| 701 |
+
max_kv_blks,
|
| 702 |
+
variable_block_sizes,
|
| 703 |
+
stride_tok,
|
| 704 |
+
stride_d,
|
| 705 |
+
H,
|
| 706 |
+
N_CTX_Q,
|
| 707 |
+
BLOCK_M2=BLOCK_M2,
|
| 708 |
+
BLOCK_N2=BLOCK_N2,
|
| 709 |
+
HEAD_DIM=HEAD_DIM,
|
| 710 |
+
start_m=start_m,
|
| 711 |
+
start_n=0,
|
| 712 |
+
num_steps=num_steps,
|
| 713 |
+
)
|
| 714 |
+
|
| 715 |
+
dq_ptrs = DQ + offs_m[:, None] * stride_tok + offs_k[None, :] * stride_d
|
| 716 |
+
dq_acc *= sm_scale
|
| 717 |
+
tl.store(dq_ptrs, dq_acc)
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
# ──────────────────────────── SPARSE ADDITION BEGIN ───────────────────────────
|
| 721 |
+
def triton_block_sparse_attn_forward(q, k, v, q2k_index, q2k_num, variable_block_sizes):
|
| 722 |
+
B, H, Tq, D = q.shape
|
| 723 |
+
Tkv = k.shape[2]
|
| 724 |
+
sm_scale = 1.0 / math.sqrt(D)
|
| 725 |
+
max_kv_blks = q2k_index.shape[-1]
|
| 726 |
+
assert Tq % 64 == 0, f"q length must be a multiple of 64, but got {Tq}"
|
| 727 |
+
assert Tkv % 64 == 0, f"kv length must be a multiple of 64, but got {Tkv}"
|
| 728 |
+
assert q2k_num.shape[
|
| 729 |
+
-1] == Tq // 64, f"shape mismatch, Tq // 64 = {Tq // 64}, q2k_num.shape[-2] = {q2k_num.shape[-2]}"
|
| 730 |
+
assert variable_block_sizes.numel() == Tkv // 64, (
|
| 731 |
+
f"shape mismatch, variable_block_sizes must have length {Tkv // 64}, "
|
| 732 |
+
f"got {variable_block_sizes.numel()}")
|
| 733 |
+
o = torch.empty_like(q)
|
| 734 |
+
M = torch.empty((B, H, Tq), dtype=torch.float32, device=q.device)
|
| 735 |
+
|
| 736 |
+
grid = lambda _: (triton.cdiv(Tq, 64), B * H, 1)
|
| 737 |
+
_attn_fwd_sparse[grid](q,
|
| 738 |
+
k,
|
| 739 |
+
v,
|
| 740 |
+
sm_scale,
|
| 741 |
+
q2k_index,
|
| 742 |
+
q2k_num,
|
| 743 |
+
max_kv_blks,
|
| 744 |
+
variable_block_sizes,
|
| 745 |
+
M,
|
| 746 |
+
o,
|
| 747 |
+
q.stride(0),
|
| 748 |
+
q.stride(1),
|
| 749 |
+
q.stride(2),
|
| 750 |
+
q.stride(3),
|
| 751 |
+
k.stride(0),
|
| 752 |
+
k.stride(1),
|
| 753 |
+
k.stride(2),
|
| 754 |
+
k.stride(3),
|
| 755 |
+
v.stride(0),
|
| 756 |
+
v.stride(1),
|
| 757 |
+
v.stride(2),
|
| 758 |
+
v.stride(3),
|
| 759 |
+
o.stride(0),
|
| 760 |
+
o.stride(1),
|
| 761 |
+
o.stride(2),
|
| 762 |
+
o.stride(3),
|
| 763 |
+
B,
|
| 764 |
+
H,
|
| 765 |
+
Tq,
|
| 766 |
+
Tkv,
|
| 767 |
+
HEAD_DIM=D,
|
| 768 |
+
STAGE=3)
|
| 769 |
+
|
| 770 |
+
return o, M
|
| 771 |
+
|
| 772 |
+
|
| 773 |
+
def triton_block_sparse_attn_backward(do, q, k, v, o, M, q2k_index, q2k_num, k2q_index, k2q_num, variable_block_sizes):
|
| 774 |
+
assert do.is_contiguous()
|
| 775 |
+
|
| 776 |
+
B, H, Tq, D = q.shape
|
| 777 |
+
Tkv = k.shape[2]
|
| 778 |
+
sm_scale = 1.0 / math.sqrt(D)
|
| 779 |
+
dq = torch.empty_like(q)
|
| 780 |
+
dk = torch.empty_like(k)
|
| 781 |
+
dv = torch.empty_like(v)
|
| 782 |
+
BATCH, N_HEAD = q.shape[:2]
|
| 783 |
+
BLOCK_M1, BLOCK_N1, BLOCK_M2, BLOCK_N2 = 32, 64, 64, 32
|
| 784 |
+
# K stays raw: the backward kernels apply sm_scale in fp32 after the dot,
|
| 785 |
+
# matching the forward's rounding exactly. (A bf16 pre-scaled K perturbs
|
| 786 |
+
# the recomputed logits vs the saved M; exp2 turns that into unboundedly
|
| 787 |
+
# wrong probabilities at large activations.)
|
| 788 |
+
arg_k = k
|
| 789 |
+
PRE_BLOCK = 64
|
| 790 |
+
assert Tq % PRE_BLOCK == 0
|
| 791 |
+
pre_grid = (Tq // PRE_BLOCK, BATCH * N_HEAD)
|
| 792 |
+
delta = torch.empty_like(M)
|
| 793 |
+
_attn_bwd_preprocess[pre_grid](
|
| 794 |
+
o,
|
| 795 |
+
do, #
|
| 796 |
+
delta, #
|
| 797 |
+
BATCH,
|
| 798 |
+
N_HEAD,
|
| 799 |
+
Tq, #
|
| 800 |
+
BLOCK_M=PRE_BLOCK,
|
| 801 |
+
HEAD_DIM=D #
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
max_q_blks = k2q_index.shape[-1]
|
| 805 |
+
max_kv_blks = q2k_index.shape[-1]
|
| 806 |
+
|
| 807 |
+
# dK/dV kernel: grid over KV blocks
|
| 808 |
+
grid_kv = (Tkv // BLOCK_N1, 1, BATCH * N_HEAD)
|
| 809 |
+
_attn_bwd_dkdv_kernel[grid_kv](
|
| 810 |
+
q,
|
| 811 |
+
arg_k,
|
| 812 |
+
v,
|
| 813 |
+
sm_scale,
|
| 814 |
+
do,
|
| 815 |
+
dk,
|
| 816 |
+
dv,
|
| 817 |
+
M,
|
| 818 |
+
delta,
|
| 819 |
+
k2q_index,
|
| 820 |
+
k2q_num,
|
| 821 |
+
max_q_blks,
|
| 822 |
+
variable_block_sizes,
|
| 823 |
+
q.stride(2),
|
| 824 |
+
q.stride(3),
|
| 825 |
+
q.stride(0),
|
| 826 |
+
q.stride(1),
|
| 827 |
+
arg_k.stride(0),
|
| 828 |
+
arg_k.stride(1),
|
| 829 |
+
v.stride(0),
|
| 830 |
+
v.stride(1),
|
| 831 |
+
do.stride(0),
|
| 832 |
+
do.stride(1),
|
| 833 |
+
dk.stride(0),
|
| 834 |
+
dk.stride(1),
|
| 835 |
+
dv.stride(0),
|
| 836 |
+
dv.stride(1),
|
| 837 |
+
N_HEAD,
|
| 838 |
+
Tq,
|
| 839 |
+
Tkv,
|
| 840 |
+
BLOCK_M1=BLOCK_M1,
|
| 841 |
+
BLOCK_N1=BLOCK_N1,
|
| 842 |
+
HEAD_DIM=D,
|
| 843 |
+
)
|
| 844 |
+
|
| 845 |
+
# dQ kernel: grid over Q blocks
|
| 846 |
+
grid_q = (Tq // BLOCK_M2, 1, BATCH * N_HEAD)
|
| 847 |
+
_attn_bwd_dq_kernel[grid_q](
|
| 848 |
+
q,
|
| 849 |
+
arg_k,
|
| 850 |
+
v,
|
| 851 |
+
sm_scale,
|
| 852 |
+
do,
|
| 853 |
+
dq,
|
| 854 |
+
M,
|
| 855 |
+
delta,
|
| 856 |
+
q2k_index,
|
| 857 |
+
q2k_num,
|
| 858 |
+
max_kv_blks,
|
| 859 |
+
variable_block_sizes,
|
| 860 |
+
q.stride(2),
|
| 861 |
+
q.stride(3),
|
| 862 |
+
q.stride(0),
|
| 863 |
+
q.stride(1),
|
| 864 |
+
arg_k.stride(0),
|
| 865 |
+
arg_k.stride(1),
|
| 866 |
+
v.stride(0),
|
| 867 |
+
v.stride(1),
|
| 868 |
+
do.stride(0),
|
| 869 |
+
do.stride(1),
|
| 870 |
+
dq.stride(0),
|
| 871 |
+
dq.stride(1),
|
| 872 |
+
N_HEAD,
|
| 873 |
+
Tq,
|
| 874 |
+
BLOCK_M2=BLOCK_M2,
|
| 875 |
+
BLOCK_N2=BLOCK_N2,
|
| 876 |
+
HEAD_DIM=D,
|
| 877 |
+
)
|
| 878 |
+
|
| 879 |
+
return dq, dk, dv
|
vsa_kernel/index.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## pytorch sdpa version of block sparse ##
|
| 2 |
+
from typing import Tuple
|
| 3 |
+
|
| 4 |
+
import triton
|
| 5 |
+
import triton.language as tl
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@triton.jit
|
| 10 |
+
def topk_index_to_map_kernel(
|
| 11 |
+
map_ptr,
|
| 12 |
+
index_ptr,
|
| 13 |
+
map_bs_stride,
|
| 14 |
+
map_h_stride,
|
| 15 |
+
map_q_stride,
|
| 16 |
+
map_kv_stride,
|
| 17 |
+
index_bs_stride,
|
| 18 |
+
index_h_stride,
|
| 19 |
+
index_q_stride,
|
| 20 |
+
index_kv_stride,
|
| 21 |
+
topk,
|
| 22 |
+
):
|
| 23 |
+
b, h, q = tl.program_id(0), tl.program_id(1), tl.program_id(2)
|
| 24 |
+
index_ptr_base = index_ptr + b * index_bs_stride + h * index_h_stride + q * index_q_stride
|
| 25 |
+
map_ptr_base = map_ptr + b * map_bs_stride + h * map_h_stride + q * map_q_stride
|
| 26 |
+
|
| 27 |
+
for i in tl.static_range(topk):
|
| 28 |
+
index = tl.load(index_ptr_base + i * index_kv_stride)
|
| 29 |
+
tl.store(map_ptr_base + index * map_kv_stride, 1.0)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@triton.jit
|
| 33 |
+
def map_to_index_kernel(
|
| 34 |
+
map_ptr,
|
| 35 |
+
index_ptr,
|
| 36 |
+
index_num_ptr,
|
| 37 |
+
map_bs_stride,
|
| 38 |
+
map_h_stride,
|
| 39 |
+
map_q_stride,
|
| 40 |
+
map_kv_stride,
|
| 41 |
+
index_bs_stride,
|
| 42 |
+
index_h_stride,
|
| 43 |
+
index_q_stride,
|
| 44 |
+
index_kv_stride,
|
| 45 |
+
index_num_bs_stride,
|
| 46 |
+
index_num_h_stride,
|
| 47 |
+
index_num_q_stride,
|
| 48 |
+
num_kv_blocks,
|
| 49 |
+
):
|
| 50 |
+
b, h, q = tl.program_id(0), tl.program_id(1), tl.program_id(2)
|
| 51 |
+
index_ptr_base = index_ptr + b * index_bs_stride + h * index_h_stride + q * index_q_stride
|
| 52 |
+
map_ptr_base = map_ptr + b * map_bs_stride + h * map_h_stride + q * map_q_stride
|
| 53 |
+
|
| 54 |
+
num = 0
|
| 55 |
+
for i in tl.range(num_kv_blocks):
|
| 56 |
+
map_entry = tl.load(map_ptr_base + i * map_kv_stride)
|
| 57 |
+
if map_entry:
|
| 58 |
+
tl.store(index_ptr_base + num * index_kv_stride, i)
|
| 59 |
+
num += 1
|
| 60 |
+
|
| 61 |
+
tl.store(index_num_ptr + b * index_num_bs_stride + h * index_num_h_stride + q * index_num_q_stride, num)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def topk_index_to_map(index: torch.Tensor, num_kv_blocks: int, transpose_map: bool = False):
|
| 65 |
+
"""
|
| 66 |
+
Convert topk indices to a map.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
index: [bs, h, num_q_blocks, topk]
|
| 70 |
+
The topk indices tensor.
|
| 71 |
+
num_kv_blocks: int
|
| 72 |
+
The number of key-value blocks in the block_map returned
|
| 73 |
+
transpose_map: bool
|
| 74 |
+
If True, the block_map will be transposed on the final two dimensions.
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
block_map: [bs, h, num_q_blocks, num_kv_blocks]
|
| 78 |
+
A binary map where 1 indicates that the q block attends to the kv block.
|
| 79 |
+
"""
|
| 80 |
+
bs, h, num_q_blocks, topk = index.shape
|
| 81 |
+
|
| 82 |
+
if transpose_map is False:
|
| 83 |
+
block_map = torch.zeros((bs, h, num_q_blocks, num_kv_blocks), dtype=torch.bool, device=index.device)
|
| 84 |
+
else:
|
| 85 |
+
block_map = torch.zeros((bs, h, num_kv_blocks, num_q_blocks), dtype=torch.bool, device=index.device)
|
| 86 |
+
block_map = block_map.transpose(2, 3)
|
| 87 |
+
|
| 88 |
+
grid = (bs, h, num_q_blocks)
|
| 89 |
+
topk_index_to_map_kernel[grid](
|
| 90 |
+
block_map,
|
| 91 |
+
index,
|
| 92 |
+
block_map.stride(0),
|
| 93 |
+
block_map.stride(1),
|
| 94 |
+
block_map.stride(2),
|
| 95 |
+
block_map.stride(3),
|
| 96 |
+
index.stride(0),
|
| 97 |
+
index.stride(1),
|
| 98 |
+
index.stride(2),
|
| 99 |
+
index.stride(3),
|
| 100 |
+
topk=topk,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
return block_map
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def map_to_index(block_map: torch.Tensor):
|
| 107 |
+
"""
|
| 108 |
+
Convert a block map to indices and counts.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
block_map: [bs, h, num_q_blocks, num_kv_blocks]
|
| 112 |
+
The block map tensor.
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
index: [bs, h, num_q_blocks, num_kv_blocks]
|
| 116 |
+
The indices of the blocks.
|
| 117 |
+
index_num: [bs, h, num_q_blocks]
|
| 118 |
+
The number of blocks for each q block.
|
| 119 |
+
"""
|
| 120 |
+
bs, h, num_q_blocks, num_kv_blocks = block_map.shape
|
| 121 |
+
|
| 122 |
+
index = torch.full((block_map.shape), -1, dtype=torch.int32, device=block_map.device)
|
| 123 |
+
index_num = torch.empty((bs, h, num_q_blocks), dtype=torch.int32, device=block_map.device)
|
| 124 |
+
|
| 125 |
+
grid = (bs, h, num_q_blocks)
|
| 126 |
+
map_to_index_kernel[grid](
|
| 127 |
+
block_map,
|
| 128 |
+
index,
|
| 129 |
+
index_num,
|
| 130 |
+
block_map.stride(0),
|
| 131 |
+
block_map.stride(1),
|
| 132 |
+
block_map.stride(2),
|
| 133 |
+
block_map.stride(3),
|
| 134 |
+
index.stride(0),
|
| 135 |
+
index.stride(1),
|
| 136 |
+
index.stride(2),
|
| 137 |
+
index.stride(3),
|
| 138 |
+
index_num.stride(0),
|
| 139 |
+
index_num.stride(1),
|
| 140 |
+
index_num.stride(2),
|
| 141 |
+
num_kv_blocks=num_kv_blocks,
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
return index, index_num
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@triton.jit
|
| 148 |
+
def _invert_indices_kernel(
|
| 149 |
+
q2k_idx_ptr,
|
| 150 |
+
q2k_num_ptr,
|
| 151 |
+
k2q_idx_ptr,
|
| 152 |
+
k2q_num_ptr,
|
| 153 |
+
q2k_idx_b,
|
| 154 |
+
q2k_idx_h,
|
| 155 |
+
q2k_idx_q,
|
| 156 |
+
q2k_idx_k,
|
| 157 |
+
q2k_num_b,
|
| 158 |
+
q2k_num_h,
|
| 159 |
+
q2k_num_q,
|
| 160 |
+
k2q_idx_b,
|
| 161 |
+
k2q_idx_h,
|
| 162 |
+
k2q_idx_k,
|
| 163 |
+
k2q_idx_q,
|
| 164 |
+
k2q_num_b,
|
| 165 |
+
k2q_num_h,
|
| 166 |
+
k2q_num_k,
|
| 167 |
+
MAX_KV_PER_Q: tl.constexpr,
|
| 168 |
+
):
|
| 169 |
+
# One program per (b, h, q): reserve a slot in k2q via atomicAdd, write q.
|
| 170 |
+
pid_b = tl.program_id(0)
|
| 171 |
+
pid_h = tl.program_id(1)
|
| 172 |
+
pid_q = tl.program_id(2)
|
| 173 |
+
|
| 174 |
+
n = tl.load(q2k_num_ptr + pid_b * q2k_num_b + pid_h * q2k_num_h + pid_q * q2k_num_q)
|
| 175 |
+
|
| 176 |
+
q2k_row = (q2k_idx_ptr + pid_b * q2k_idx_b + pid_h * q2k_idx_h + pid_q * q2k_idx_q)
|
| 177 |
+
|
| 178 |
+
for i in tl.range(0, MAX_KV_PER_Q):
|
| 179 |
+
if i < n:
|
| 180 |
+
kv = tl.load(q2k_row + i * q2k_idx_k)
|
| 181 |
+
count_ptr = (k2q_num_ptr + pid_b * k2q_num_b + pid_h * k2q_num_h + kv * k2q_num_k)
|
| 182 |
+
pos = tl.atomic_add(count_ptr, 1)
|
| 183 |
+
tl.store(
|
| 184 |
+
k2q_idx_ptr + pid_b * k2q_idx_b + pid_h * k2q_idx_h + kv * k2q_idx_k + pos * k2q_idx_q,
|
| 185 |
+
pid_q,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def invert_indices(
|
| 190 |
+
q2k_idx: torch.Tensor,
|
| 191 |
+
q2k_num: torch.Tensor,
|
| 192 |
+
num_kv_blocks: int,
|
| 193 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 194 |
+
"""Transpose a Q->KV index list into a K->Q one via atomic compaction (GPU)."""
|
| 195 |
+
if q2k_idx.dim() != 4:
|
| 196 |
+
raise ValueError(f"q2k_idx must be [B, H, Nq, Mk], got shape={tuple(q2k_idx.shape)}")
|
| 197 |
+
if q2k_num.dim() != 3:
|
| 198 |
+
raise ValueError(f"q2k_num must be [B, H, Nq], got shape={tuple(q2k_num.shape)}")
|
| 199 |
+
if not q2k_idx.is_cuda or not q2k_num.is_cuda:
|
| 200 |
+
raise RuntimeError("invert_indices requires CUDA tensors.")
|
| 201 |
+
|
| 202 |
+
B, H, Nq, Mk = q2k_idx.shape
|
| 203 |
+
if q2k_num.shape != (B, H, Nq):
|
| 204 |
+
raise ValueError(f"q2k_num shape {tuple(q2k_num.shape)} does not match q2k_idx "
|
| 205 |
+
f"[B, H, Nq] = {(B, H, Nq)}")
|
| 206 |
+
|
| 207 |
+
q2k_idx = q2k_idx.contiguous()
|
| 208 |
+
q2k_num = q2k_num.contiguous()
|
| 209 |
+
if q2k_idx.dtype != torch.int32:
|
| 210 |
+
q2k_idx = q2k_idx.to(torch.int32)
|
| 211 |
+
if q2k_num.dtype != torch.int32:
|
| 212 |
+
q2k_num = q2k_num.to(torch.int32)
|
| 213 |
+
|
| 214 |
+
# Any KV block is attended by at most Nq Q blocks (one per Q row), so
|
| 215 |
+
# `Nq` is a tight upper bound on the compacted K->Q slots.
|
| 216 |
+
k2q_idx = torch.empty(
|
| 217 |
+
(B, H, num_kv_blocks, Nq),
|
| 218 |
+
dtype=torch.int32,
|
| 219 |
+
device=q2k_idx.device,
|
| 220 |
+
)
|
| 221 |
+
k2q_num = torch.zeros(
|
| 222 |
+
(B, H, num_kv_blocks),
|
| 223 |
+
dtype=torch.int32,
|
| 224 |
+
device=q2k_idx.device,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
grid = (B, H, Nq)
|
| 228 |
+
_invert_indices_kernel[grid](
|
| 229 |
+
q2k_idx,
|
| 230 |
+
q2k_num,
|
| 231 |
+
k2q_idx,
|
| 232 |
+
k2q_num,
|
| 233 |
+
q2k_idx.stride(0),
|
| 234 |
+
q2k_idx.stride(1),
|
| 235 |
+
q2k_idx.stride(2),
|
| 236 |
+
q2k_idx.stride(3),
|
| 237 |
+
q2k_num.stride(0),
|
| 238 |
+
q2k_num.stride(1),
|
| 239 |
+
q2k_num.stride(2),
|
| 240 |
+
k2q_idx.stride(0),
|
| 241 |
+
k2q_idx.stride(1),
|
| 242 |
+
k2q_idx.stride(2),
|
| 243 |
+
k2q_idx.stride(3),
|
| 244 |
+
k2q_num.stride(0),
|
| 245 |
+
k2q_num.stride(1),
|
| 246 |
+
k2q_num.stride(2),
|
| 247 |
+
MAX_KV_PER_Q=Mk,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
return k2q_idx, k2q_num
|