bibibi12345 commited on
Commit
b96c13c
·
verified ·
1 Parent(s): 6316322

Prism ZeroGPU demo

Browse files
Files changed (42) hide show
  1. .gitattributes +6 -0
  2. LICENSE +87 -0
  3. README.md +14 -7
  4. __pycache__/app.cpython-312.pyc +0 -0
  5. app.py +360 -0
  6. examples/case-1/prompt.txt +1 -0
  7. examples/case-1/reference_image.jpg +3 -0
  8. examples/case-2/prompt.txt +1 -0
  9. examples/case-2/reference_image.jpg +3 -0
  10. examples/case-3/prompt.txt +1 -0
  11. examples/case-3/reference_image.jpg +3 -0
  12. examples/case-4/prompt.txt +1 -0
  13. examples/case-4/reference_image.jpg +3 -0
  14. examples/case-5/prompt.txt +1 -0
  15. examples/case-5/reference_image.jpg +3 -0
  16. examples/case-6/prompt.txt +1 -0
  17. examples/case-6/reference_image.jpg +3 -0
  18. hymm/diffusion/__init__.py +0 -0
  19. hymm/diffusion/pipelines/__init__.py +0 -0
  20. hymm/diffusion/pipelines/mova_pipeline.py +485 -0
  21. hymm/diffusion/schedulers/__init__.py +1 -0
  22. hymm/diffusion/schedulers/flow_match_pair.py +277 -0
  23. hymm/models/__init__.py +0 -0
  24. hymm/models/modules/__init__.py +1 -0
  25. hymm/models/modules/block_sparse_attention/__init__.py +16 -0
  26. hymm/models/modules/block_sparse_attention/bias_rectification.py +452 -0
  27. hymm/models/modules/block_sparse_attention/bias_rectification_triton.py +363 -0
  28. hymm/models/modules/block_sparse_attention/bsa_interface.py +1503 -0
  29. hymm/models/modules/block_sparse_attention/common.py +111 -0
  30. hymm/models/modules/block_sparse_attention/dynamic_block_attention.py +335 -0
  31. hymm/models/modules/block_sparse_attention/dynamic_block_shape.py +406 -0
  32. hymm/models/modules/block_sparse_attention/flash_attn_bsa_varlen_mask.py +1014 -0
  33. hymm/models/modules/dac_vae.py +1103 -0
  34. hymm/models/modules/interactionv2.py +708 -0
  35. hymm/models/modules/mova.py +964 -0
  36. hymm/models/modules/wan_audio_dit.py +271 -0
  37. hymm/models/modules/wan_video_dit.py +1324 -0
  38. hymm/utils/__init__.py +1 -0
  39. hymm/utils/communications.py +413 -0
  40. hymm/utils/parallel_states.py +453 -0
  41. packages.txt +1 -0
  42. requirements.txt +14 -0
.gitattributes CHANGED
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/case-1/reference_image.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/case-2/reference_image.jpg filter=lfs diff=lfs merge=lfs -text
38
+ examples/case-3/reference_image.jpg filter=lfs diff=lfs merge=lfs -text
39
+ examples/case-4/reference_image.jpg filter=lfs diff=lfs merge=lfs -text
40
+ examples/case-5/reference_image.jpg filter=lfs diff=lfs merge=lfs -text
41
+ examples/case-6/reference_image.jpg filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Tencent is pleased to support the community by making Prism available.
2
+
3
+ Copyright (C) 2026 Tencent. All rights reserved.
4
+
5
+ The open-source software and/or model(s) included in this distribution may have been modified by Tencent ("Tencent Modifications"). All Tencent Modifications are Copyright (C) Tencent.
6
+
7
+ Prism is licensed under MIT, except for the third-party components listed below, which remain licensed under their respective original terms. Prism does not impose any additional restrictions beyond those specified in the original licenses of these third-party components. Users are required to comply with all applicable terms and conditions of the original licenses and to ensure that the use of these third-party components conforms to all relevant laws and regulations.
8
+
9
+ For the avoidance of doubt, Prism refers solely to code, parameters and weights made publicly available by Tencent in accordance with MIT.
10
+
11
+ Terms of the MIT License:
12
+ --------------------------------------------------------------------
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18
+
19
+
20
+ Dependencies and Licenses:
21
+
22
+ This open-source project builds upon the following open-source model(s) and/or software, each of which remains licensed under its original license(s). Certain component(s) may include modifications made by Tencent ("Tencent Modifications"), which are Copyright (C) Tencent.
23
+
24
+ In case you believe there have been errors in the attribution below, you may submit the concerns to us for review and correction.
25
+
26
+
27
+ Open Source Model(s)/Software Licensed under the Apache-2.0:
28
+ --------------------------------------------------------------------
29
+ 1. MOVA-360p
30
+ Copyright (c) MOVA-360p original author and authors
31
+ Please note this model/software has been modified by Tencent in this distribution.
32
+ Terms of the Apache-2.0:
33
+ --------------------------------------------------------------------
34
+ Apache License
35
+ Version 2.0, January 2004
36
+ http://www.apache.org/licenses/
37
+
38
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
39
+
40
+ 1. Definitions.
41
+
42
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
43
+
44
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
45
+
46
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
47
+
48
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
49
+
50
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
51
+
52
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
53
+
54
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
55
+
56
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
57
+
58
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
59
+
60
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
61
+
62
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
63
+
64
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
65
+
66
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
67
+
68
+ You must give any other recipients of the Work or Derivative Works a copy of this License; and
69
+ You must cause any modified files to carry prominent notices stating that You changed the files; and
70
+ You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
71
+ If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
72
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
73
+
74
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
75
+
76
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
77
+
78
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
79
+
80
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
81
+
82
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
83
+
84
+ END OF TERMS AND CONDITIONS
85
+
86
+ ==================================================
87
+ End of the Attribution Notice of this project.
README.md CHANGED
@@ -1,13 +1,20 @@
1
  ---
2
- title: Prism
3
- emoji: 🐠
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.29.1
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: Prism Video Audio
3
+ emoji: 🎬
4
+ colorFrom: indigo
5
+ colorTo: pink
6
  sdk: gradio
7
+ sdk_version: 5.50.0
8
+ python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
+ license: mit
12
+ models:
13
+ - FrancisRing/Prism
14
+ startup_duration_timeout: 1h
15
+ short_description: Prism image+text to video with audio (ZeroGPU xlarge)
16
  ---
17
 
18
+ ZeroGPU demo of [FrancisRing/Prism](https://huggingface.co/FrancisRing/Prism)
19
+ (dynamic sparse attention, joint video + audio generation on the MOVA backbone).
20
+ Inference code is adapted from [Tencent-Hunyuan/Prism](https://github.com/Tencent-Hunyuan/Prism) (MIT).
__pycache__/app.cpython-312.pyc ADDED
Binary file (21.9 kB). View file
 
app.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prism (FrancisRing/Prism) joint video-audio generation on ZeroGPU (xlarge, 96GB)."""
2
+
3
+ import os
4
+ import sys
5
+ import time
6
+ import types
7
+ import random
8
+ import tempfile
9
+ import subprocess
10
+
11
+ import torch
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # ZeroGPU does not support torch.compile -> turn every @torch.compile into a no-op
15
+ # (must happen before the model code is imported).
16
+ # ---------------------------------------------------------------------------
17
+ def _no_compile(fn=None, *args, **kwargs):
18
+ if callable(fn):
19
+ return fn
20
+ return lambda f: f
21
+
22
+
23
+ torch.compile = _no_compile
24
+
25
+ # The DAC audio VAE only needs `audiotools` for training/IO helpers. Stub it so we
26
+ # don't have to install descript-audiotools (it pins protobuf<3.20).
27
+ _at = types.ModuleType("audiotools")
28
+ _at_ml = types.ModuleType("audiotools.ml")
29
+
30
+
31
+ class _AudioSignal: # never used in the decode path
32
+ def __init__(self, *a, **k):
33
+ raise RuntimeError("audiotools.AudioSignal is not available in this Space")
34
+
35
+
36
+ _at.AudioSignal = _AudioSignal
37
+ _at_ml.BaseModel = torch.nn.Module
38
+ _at.ml = _at_ml
39
+ sys.modules.setdefault("audiotools", _at)
40
+ sys.modules.setdefault("audiotools.ml", _at_ml)
41
+
42
+ import spaces # noqa: E402 (must be imported before CUDA is touched)
43
+ import numpy as np # noqa: E402
44
+ import gradio as gr # noqa: E402
45
+ from PIL import Image # noqa: E402
46
+ from huggingface_hub import snapshot_download # noqa: E402
47
+ from safetensors import safe_open # noqa: E402
48
+ from accelerate import init_empty_weights # noqa: E402
49
+ from accelerate.utils import set_module_tensor_to_device # noqa: E402
50
+
51
+ from hymm.models.modules.mova import MOVABridge # noqa: E402
52
+ from hymm.models.modules.wan_video_dit import WanModel # noqa: E402
53
+ from hymm.models.modules.wan_audio_dit import WanAudioModel # noqa: E402
54
+ from hymm.models.modules.interactionv2 import DualTowerConditionalBridge # noqa: E402
55
+ from hymm.models.modules.dac_vae import DAC # noqa: E402
56
+ from hymm.diffusion.pipelines.mova_pipeline import MOVAPipeline # noqa: E402
57
+ from hymm.diffusion.schedulers.flow_match_pair import FlowMatchPairScheduler # noqa: E402
58
+ from diffusers.models.autoencoders import AutoencoderKLWan # noqa: E402
59
+ from transformers import T5TokenizerFast, UMT5EncoderModel # noqa: E402
60
+
61
+ REPO_ID = "FrancisRing/Prism"
62
+ VARIANT = os.environ.get("PRISM_VARIANT", "preview_alpha") # or preview_beta (motion)
63
+ BASE = "pretrained_models/MOVA-360p"
64
+ DTYPE = torch.bfloat16
65
+
66
+ NEGATIVE_PROMPT = (
67
+ "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,"
68
+ "整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指"
69
+ )
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Download only what inference needs: the Prism transformer checkpoint (it holds
73
+ # both video experts, the audio tower and the bridge) + text encoder / VAEs.
74
+ # The 57GB of base MOVA DiT weights are fully overridden, so they are skipped.
75
+ # ---------------------------------------------------------------------------
76
+ t0 = time.time()
77
+ root = snapshot_download(
78
+ REPO_ID,
79
+ allow_patterns=[
80
+ f"{VARIANT}/*",
81
+ f"{BASE}/model_index.json",
82
+ f"{BASE}/*/config.json",
83
+ f"{BASE}/text_encoder/*",
84
+ f"{BASE}/tokenizer/*",
85
+ f"{BASE}/scheduler/*",
86
+ f"{BASE}/video_vae/*",
87
+ f"{BASE}/audio_vae/*",
88
+ ],
89
+ )
90
+ base = os.path.join(root, BASE)
91
+ print(f"[prism] download done in {time.time() - t0:.0f}s -> {root}")
92
+
93
+ # --- transformer: build on meta, then stream the checkpoint straight to cuda ---
94
+ t0 = time.time()
95
+ with init_empty_weights():
96
+ video_dit = WanModel.from_config(WanModel.load_config(base, subfolder="video_dit"))
97
+ video_dit_2 = WanModel.from_config(WanModel.load_config(base, subfolder="video_dit_2"))
98
+ audio_dit = WanAudioModel.from_config(WanAudioModel.load_config(base, subfolder="audio_dit"))
99
+ dual_bridge = DualTowerConditionalBridge.from_config(
100
+ DualTowerConditionalBridge.load_config(base, subfolder="dual_tower_bridge")
101
+ )
102
+ transformer = MOVABridge(
103
+ video_dit=video_dit,
104
+ video_dit_2=video_dit_2,
105
+ audio_dit=audio_dit,
106
+ dual_tower_bridge=dual_bridge,
107
+ boundary_ratio=0.9,
108
+ )
109
+
110
+ ckpt_file = os.path.join(root, VARIANT, "diffusion_pytorch_model.safetensors")
111
+ expected = set(transformer.state_dict().keys())
112
+ loaded = set()
113
+ with safe_open(ckpt_file, framework="pt", device="cpu") as f:
114
+ for k in f.keys():
115
+ if k not in expected:
116
+ print(f"[prism] unexpected key {k}")
117
+ continue
118
+ set_module_tensor_to_device(transformer, k, "cuda", value=f.get_tensor(k), dtype=DTYPE)
119
+ loaded.add(k)
120
+ missing = expected - loaded
121
+ if missing:
122
+ raise RuntimeError(f"[prism] {len(missing)} transformer keys missing, e.g. {sorted(missing)[:5]}")
123
+ transformer.to("cuda")
124
+ transformer.eval().requires_grad_(False)
125
+ print(f"[prism] transformer ready ({len(loaded)} tensors) in {time.time() - t0:.0f}s")
126
+
127
+ # --- frozen components ---
128
+ video_vae = AutoencoderKLWan.from_pretrained(base, subfolder="video_vae", torch_dtype=DTYPE).to("cuda").eval()
129
+ audio_vae = DAC.from_pretrained(base, subfolder="audio_vae", torch_dtype=DTYPE).to("cuda").eval()
130
+ text_encoder = UMT5EncoderModel.from_pretrained(base, subfolder="text_encoder", torch_dtype=DTYPE).to("cuda").eval()
131
+ tokenizer = T5TokenizerFast.from_pretrained(base, subfolder="tokenizer")
132
+ scheduler = FlowMatchPairScheduler.from_pretrained(base, subfolder="scheduler")
133
+
134
+ pipe = MOVAPipeline(
135
+ transformer=transformer,
136
+ video_vae=video_vae,
137
+ audio_vae=audio_vae,
138
+ text_encoder=text_encoder,
139
+ tokenizer=tokenizer,
140
+ scheduler=scheduler,
141
+ boundary_ratio=0.9,
142
+ device=torch.device("cuda"),
143
+ )
144
+ print("[prism] pipeline ready")
145
+
146
+ RESOLUTIONS = {
147
+ "480p (848×480)": (480, 848),
148
+ "720p (1280×720)": (720, 1280),
149
+ }
150
+
151
+
152
+ def configure_sparse(sparsity, use_ivpq):
153
+ transformer.set_sparse_high_noise_only(False)
154
+ transformer.configure_bsa(
155
+ enable_bsa=True,
156
+ bsa_params={
157
+ "sparsity": float(sparsity),
158
+ "cdf_threshold": 0.20,
159
+ "chunk_3d_shape_q": [4, 4, 4],
160
+ "chunk_3d_shape_k": [4, 4, 4],
161
+ },
162
+ enable_bsa_v2a=False,
163
+ bsa_params_v2a=None,
164
+ )
165
+ for sa in transformer._video_self_attns():
166
+ sa.bsa_params["dynamic_block_lambda_a"] = 0.5
167
+ sa.bsa_params["dynamic_block_tau_128"] = 0.25
168
+ sa.bsa_params["dynamic_block_lambda_128"] = 1.0
169
+ transformer.configure_ivpq_dynamic_block(enable=bool(use_ivpq))
170
+
171
+
172
+ def crop_and_resize(img, height, width):
173
+ w, h = img.size
174
+ target = width / height
175
+ if w / h > target:
176
+ nw = int(h * target)
177
+ img = img.crop(((w - nw) // 2, 0, (w - nw) // 2 + nw, h))
178
+ elif w / h < target:
179
+ nh = int(w / target)
180
+ img = img.crop((0, (h - nh) // 2, w, (h - nh) // 2 + nh))
181
+ return img.resize((width, height), Image.LANCZOS)
182
+
183
+
184
+ def target_size(image, resolution):
185
+ h, w = RESOLUTIONS[resolution]
186
+ if image is not None and image.height > image.width: # portrait input -> portrait video
187
+ h, w = w, h
188
+ return h, w
189
+
190
+
191
+ def num_tokens(h, w, frames):
192
+ return ((frames - 1) // 4 + 1) * (h // 16) * (w // 16)
193
+
194
+
195
+ # Rough GPU-time model (seconds per transformer forward), calibrated on the Space.
196
+ SEC_PER_TOKEN = float(os.environ.get("PRISM_SEC_PER_TOKEN", "1.0e-4"))
197
+ SEC_PER_TOKEN2 = float(os.environ.get("PRISM_SEC_PER_TOKEN2", "4.0e-9"))
198
+ MAX_DURATION = int(os.environ.get("PRISM_MAX_DURATION", "1500"))
199
+
200
+
201
+ def estimate_seconds(image, resolution, frames, steps, cfg, sparsity):
202
+ h, w = target_size(image, resolution)
203
+ tok = num_tokens(h, w, int(frames))
204
+ per_forward = SEC_PER_TOKEN * tok + SEC_PER_TOKEN2 * tok * tok * (1.0 - float(sparsity) + 0.05)
205
+ passes = 1 if float(cfg) == 1.0 else 2
206
+ return int(60 + int(steps) * passes * per_forward)
207
+
208
+
209
+ def get_duration(image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift,
210
+ seed, sparsity, use_ivpq, *args, **kwargs):
211
+ return min(MAX_DURATION, int(estimate_seconds(image, resolution, frames, steps, cfg, sparsity) * 1.15))
212
+
213
+
214
+ @spaces.GPU(size="xlarge", duration=get_duration)
215
+ def _generate_gpu(image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift,
216
+ seed, sparsity, use_ivpq, progress=gr.Progress(track_tqdm=True)):
217
+ t_start = time.time()
218
+ h, w = target_size(image, resolution)
219
+ ref = crop_and_resize(image.convert("RGB"), h, w)
220
+ configure_sparse(sparsity, use_ivpq)
221
+ torch.manual_seed(int(seed))
222
+ video, audio = pipe(
223
+ prompt=prompt,
224
+ image=ref,
225
+ audio_prompt=audio_prompt or prompt,
226
+ negative_prompt=NEGATIVE_PROMPT,
227
+ seed=int(seed),
228
+ height=h,
229
+ width=w,
230
+ num_frames=int(frames),
231
+ video_fps=24.0,
232
+ num_inference_steps=int(steps),
233
+ visual_shift=float(shift),
234
+ audio_shift=float(audio_shift),
235
+ cfg_scale=float(cfg),
236
+ enable_vae_tiling=(h * w >= 720 * 1280),
237
+ )
238
+ frames_np = np.stack([np.asarray(fr) for fr in video[0]])
239
+ audio_np = audio[0].float().cpu().squeeze().numpy()
240
+ elapsed = time.time() - t_start
241
+ print(f"[prism] {h}x{w} {frames}f {steps} steps cfg={cfg} -> {elapsed:.1f}s on GPU "
242
+ f"(peak mem {torch.cuda.max_memory_allocated() / 2**30:.1f} GiB)")
243
+ return frames_np, audio_np, elapsed
244
+
245
+
246
+ def save_video_with_audio(frames, audio_np, path, fps, sample_rate):
247
+ import wave
248
+ import imageio
249
+
250
+ with tempfile.TemporaryDirectory() as tmp:
251
+ tmp_video = os.path.join(tmp, "video.mp4")
252
+ tmp_audio = os.path.join(tmp, "audio.wav")
253
+ writer = imageio.get_writer(tmp_video, fps=fps, quality=9, macro_block_size=8)
254
+ for fr in frames:
255
+ writer.append_data(fr)
256
+ writer.close()
257
+
258
+ if audio_np.ndim == 1:
259
+ audio_np = audio_np[None, :]
260
+ audio_np = audio_np[:2]
261
+ pcm = (np.clip(audio_np, -1.0, 1.0) * 32767.0).astype(np.int16)
262
+ with wave.open(tmp_audio, "wb") as wf:
263
+ wf.setnchannels(pcm.shape[0])
264
+ wf.setsampwidth(2)
265
+ wf.setframerate(int(sample_rate))
266
+ wf.writeframes(pcm.T.reshape(-1).tobytes())
267
+
268
+ cmd = ["ffmpeg", "-y", "-i", tmp_video, "-i", tmp_audio, "-c:v", "copy", "-c:a", "aac",
269
+ "-b:a", "192k", "-movflags", "+faststart", "-shortest", path]
270
+ r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
271
+ if r.returncode != 0:
272
+ print("[prism] ffmpeg mux failed:", r.stderr.decode(errors="ignore")[-500:])
273
+ import shutil
274
+ shutil.copyfile(tmp_video, path)
275
+
276
+
277
+ def generate(image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift,
278
+ seed, randomize_seed, sparsity, use_ivpq, progress=gr.Progress(track_tqdm=True)):
279
+ if image is None:
280
+ raise gr.Error("Please upload a reference image (Prism is an image+text → video+audio model).")
281
+ if not prompt or not prompt.strip():
282
+ raise gr.Error("Please enter a prompt.")
283
+ if randomize_seed:
284
+ seed = random.randint(0, 2**31 - 1)
285
+ frames = int(frames)
286
+ frames = frames - (frames - 1) % 4
287
+ frames_np, audio_np, elapsed = _generate_gpu(
288
+ image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift,
289
+ seed, sparsity, use_ivpq,
290
+ )
291
+ out = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
292
+ save_video_with_audio(frames_np, audio_np, out, fps=24.0, sample_rate=pipe.audio_sample_rate)
293
+ return out, seed, f"GPU time: {elapsed:.0f}s (xlarge quota counts ×2)"
294
+
295
+
296
+ def update_estimate(image, resolution, frames, steps, cfg, sparsity):
297
+ h, w = target_size(image, resolution)
298
+ est = estimate_seconds(image, resolution, int(frames), steps, cfg, sparsity)
299
+ return (f"Output {w}×{h}, {int(frames)} frames ({int(frames) / 24:.1f}s) · "
300
+ f"rough GPU estimate ≈ {est / 60:.1f} min → ≈ {2 * est / 60:.1f} min of daily ZeroGPU quota")
301
+
302
+
303
+ EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples")
304
+ examples = []
305
+ if os.path.isdir(EXAMPLES_DIR):
306
+ for name in sorted(os.listdir(EXAMPLES_DIR)):
307
+ d = os.path.join(EXAMPLES_DIR, name)
308
+ img, txt = os.path.join(d, "reference_image.jpg"), os.path.join(d, "prompt.txt")
309
+ if os.path.exists(img) and os.path.exists(txt):
310
+ with open(txt, encoding="utf-8") as fh:
311
+ examples.append([img, fh.read().strip()])
312
+
313
+ with gr.Blocks(title="Prism · joint video + audio") as demo:
314
+ gr.Markdown(
315
+ "# Prism: image + text → video with audio\n"
316
+ f"[Model](https://huggingface.co/{REPO_ID}) · [Code](https://github.com/Tencent-Hunyuan/Prism) · "
317
+ f"[Paper](https://arxiv.org/abs/2610.05416). Checkpoint: `{VARIANT}`. Runs on one ZeroGPU **xlarge** "
318
+ "(RTX Pro 6000, 96GB). xlarge uses quota at 2×, so keep clips short. "
319
+ "Sound can be tagged in the prompt with `<music>`, `<sfx>`, `<speech>`."
320
+ )
321
+ with gr.Row():
322
+ with gr.Column():
323
+ image = gr.Image(label="Reference image (first frame)", type="pil")
324
+ prompt = gr.Textbox(label="Prompt (video)", lines=6)
325
+ audio_prompt = gr.Textbox(label="Audio prompt (optional, defaults to the video prompt)", lines=2)
326
+ with gr.Row():
327
+ resolution = gr.Radio(list(RESOLUTIONS), value="720p (1280×720)", label="Resolution")
328
+ frames = gr.Slider(25, 121, value=49, step=4, label="Frames (24 fps)")
329
+ steps = gr.Slider(10, 50, value=30, step=1, label="Denoising steps")
330
+ estimate = gr.Markdown()
331
+ with gr.Accordion("Advanced", open=False):
332
+ cfg = gr.Slider(1.0, 10.0, value=5.0, step=0.5, label="CFG scale (1.0 = no CFG, ~2× faster)")
333
+ shift = gr.Slider(1.0, 17.0, value=7.0, step=0.5, label="Visual shift (720p: 7, 480p: 5)")
334
+ audio_shift = gr.Slider(1.0, 15.0, value=7.0, step=0.5, label="Audio shift")
335
+ sparsity = gr.Dropdown([0.75, 0.85, 0.93], value=0.75, label="Block-sparse attention sparsity")
336
+ use_ivpq = gr.Checkbox(value=True, label="Dynamic block shape (IVPQ)")
337
+ with gr.Row():
338
+ seed = gr.Number(value=42, precision=0, label="Seed")
339
+ randomize_seed = gr.Checkbox(value=False, label="Random seed")
340
+ run = gr.Button("Generate", variant="primary")
341
+ with gr.Column():
342
+ out_video = gr.Video(label="Result (with audio)")
343
+ used_seed = gr.Number(label="Seed used", interactive=False)
344
+ info = gr.Markdown()
345
+
346
+ est_inputs = [image, resolution, frames, steps, cfg, sparsity]
347
+ for c in est_inputs:
348
+ c.change(update_estimate, est_inputs, estimate)
349
+ demo.load(update_estimate, est_inputs, estimate)
350
+
351
+ run.click(
352
+ generate,
353
+ [image, prompt, audio_prompt, resolution, frames, steps, cfg, shift, audio_shift,
354
+ seed, randomize_seed, sparsity, use_ivpq],
355
+ [out_video, used_seed, info],
356
+ )
357
+ if examples:
358
+ gr.Examples(examples, [image, prompt])
359
+
360
+ demo.queue(max_size=20).launch()
examples/case-1/prompt.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ No background music, quiet recording environment, close-mic capture, no noticeable reverb. Close-up, eye-level view, softly lit with frontal lighting. The frame features an elderly white woman with short gray curly hair, blue eyes, deeply lined face, flushed cheeks, and red lipstick. She wears a black coat and a red scarf with a white floral pattern, a black microphone clipped to her collar. She sits in front of a brown wooden piece of furniture, with a pale yellow textured wall behind her, on which hangs a miniature model depicting a canal and Dutch-style buildings on both sides. Initially, the elderly white woman faces the camera, lips moving, speaking in a hoarse voice with a heavy expression<speech>weg haden ze nog gezegd, ze liepen weg.</speech>. Then, she briefly lowers her head, her gaze moving downward, mouth slightly open as if sighing. Immediately afterward, she raises her head to look at the camera again, continuing<speech>Nou, dan kwam de directie er bij, want de directeur moest oepen.</speech>, blinking slowly throughout, her expression solemn.
examples/case-1/reference_image.jpg ADDED

Git LFS Details

  • SHA256: fb2304affef5d07de47eb31385ac319b1a511a0ab0852dfdfaba175ce4c2ab11
  • Pointer size: 131 Bytes
  • Size of remote file: 184 kB
examples/case-2/prompt.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Continuous <sfx>ambient store sounds, including people talking and footsteps</sfx> throughout. Medium close-up, eye-level view. Inside an indoor store with bright lighting. The main focus is a man and a woman conversing. On the left side of the frame is a young-to-middle-aged white woman wearing glasses, with brown wavy medium-short hair, wearing a black jacket and a dark red inner layer, holding a dark-colored wine bottle with a black label featuring gold English text <text>THE髭振酒</text>. On the right side is a middle-aged white man with a beard, receding hairline, wearing a gray plaid suit jacket and a light blue shirt, holding a green wine bottle with a white label featuring a minimalist black line pattern and text. Behind them are shelves filled with products, with multiple customers and staff walking and shopping in the background, and a row of track spotlights on the ceiling. Initially, the man in the gray suit holds up the green wine bottle, looking at the woman across from him. Then he dramatically widens his eyes, showing the bottle to her, enthusiastically saying <speech>And the second one that we have a wine.</speech>. The woman lowers her head, looking at her own dark-colored bottle, smiling. Immediately afterward, the woman in the black jacket looks up at the man, asking curiously <speech>They are also do a wine?</speech>. The man nods affirmatively, saying <speech>Yep.</speech>. The woman confirms, saying <speech>The messa.</speech>. The man then raises his left hand in a gesture of explanation, continuing to speak <speech>So when you don't know it, you're kind of afraid because maple.</speech>"}
examples/case-2/reference_image.jpg ADDED

Git LFS Details

  • SHA256: add9bdcad3baeccced14bdc73b7254bc012a32852755be0c4f450b59f2659e5d
  • Pointer size: 131 Bytes
  • Size of remote file: 183 kB
examples/case-3/prompt.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Continuous <music>solemn and expressive classical chamber music performance throughout, featuring two violins, one cello, one harpsichord, and one bassoon, with intertwined melodies and rich harmonies producing noticeable reverb in the stone-walled church space.</music>. High-angle wide shot, fixed frame: Gothic stone church interior. Soft, cool light from left arched window. White walls with carved memorials and shield emblems. Gray stone floor, red-and-black diamond tile path center. Eight musicians in semicircle. Center: young white male harpsichordist (black long-sleeve, facing away). Foreground left: middle-aged white female violinist (blonde, purple lace top, black dress). Beside her: balding middle-aged white male violinist. Foreground right: young white male cellist (brown short hair). Background: middle-aged white female violinist (blonde), balding middle-aged white male bassoonist, two standing male musicians. Each has black music stand. Microphones on floor. As the shot begins, all musicians start playing their instruments, gently swaying their bodies in rhythm with the music. <music>A solemn and expressive classical chamber music performance begins, featuring two violins, one cello, one harpsichord, and one bassoon, with intertwined melodies and rich harmonies producing noticeable reverb in the stone-walled church space.</music> The male and female violinists on the left and the cellist on the right are focusedly drawing their bows across the strings, fingers moving swiftly along the strings. The central harpsichordist's hands move rapidly across the keyboard while glancing sideways at the sheet music. The bassoonist in the background is playing his instrument. As the piece progresses, a cellist from the far right edge of the frame enters the shot and begins playing.
examples/case-3/reference_image.jpg ADDED

Git LFS Details

  • SHA256: 3674a36ab770f91a29051a10d641906d207c93d3da6f28787ac775e25c73345e
  • Pointer size: 131 Bytes
  • Size of remote file: 264 kB
examples/case-4/prompt.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ <sfx>Constant noisy background sounds from the gym throughout, including equipment clanging, dumbbell plates hitting, and muffled conversations, with noticeable spatial reverb.</sfx>. Medium shot with a slight high-angle view, indoor gym setting, bright lighting. The main subject is a young white woman sitting on a black gym bench, with long golden ponytail, wearing white in-ear headphones, and a large black mandala tattoo on her left arm, though details like the medical giant logo beneath are somewhat blurred. She wears a white sports bra and olive-green ribbed leggings, and black-and-white Adidas sneakers. On the left side of the frame is a blue fitness equipment column with a warning label, an orange warning tag marked <text>WARNING</text>, and a white label below. The background is a white wall with blue brand text visible, partially showing <text>Nordic</text>, and <text>to</text> visible on the right. Other gym equipment and multiple members exercising are visible, including a man in a black hoodie and a woman in a light gray hoodie. Initially, the blonde woman keeps her upper body lifted with bent legs, arms hugging her knees close to her chest. Her mouth is slightly open, looking directly at the camera, beginning her explanation in a slightly hoarse but clear and strong voice in Swedish: <speech>So try to do or this can a try to move bengal。</speech>. Then, while maintaining the knee-hug position, she extends her right index finger forward, then brings it back, adjusting her left hand's position on her knee. Next, she moves her left hand to support her left hip and waist, while bringing her right knee to the side and tilting her body backward. Subsequently, she keeps her left leg bent for support, extends her right leg, and slowly moves it upward and downward in rhythm with the command, completing one snow angel-like motion. Throughout and after the movement, she continues speaking directly to the camera: <speech>Can she tell me about the room and lift it little from that? Are they just broke in them now we could。</speech>
examples/case-4/reference_image.jpg ADDED

Git LFS Details

  • SHA256: 42ee459d229ed90da415204d6230adbb9c3d8d3f0fd0c3d4cbac20f74a61111b
  • Pointer size: 131 Bytes
  • Size of remote file: 143 kB
examples/case-5/prompt.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Continuous <sfx>sound of waves and water currents around the boat</sfx> throughout. Medium shot, eye-level view. During the day, in clear outdoor conditions, a middle-aged white man stands on a boat facing the camera. He wears a gray baseball cap and blue-tinted sunglasses, a gray short-sleeved T-shirt with a brand logo featuring <text>YETI</text> on the chest, mostly obscured by the large fish he holds. He cradles a massive dark brown grouper with a wet, shiny surface reflecting sunlight and a serrated dorsal fin. A white outboard motor is located behind his left, marked with black <text>EVINRUDE</text>, partially hidden by the fish's tail at times. On the right side of the frame, another man's profile is visible—bearded, wearing a hat, holding a fishing rod. The background shows a vast blue ocean under a sky with a few scattered clouds. Initially, the man with sunglasses beams a bright smile, enthusiastically saying <speech>I mean, so far we've caught Marlin, Cooper, I mean, a Wahoo,we got slam. Why stop now?</speech> to the camera. His expression is highly animated. Then, he briefly turns his head to the right to look at the man with the fishing rod beside him, before turning back, maintaining a joyful expression throughout. The camera gently shakes due to the boat's movement during the entire sequence.
examples/case-5/reference_image.jpg ADDED

Git LFS Details

  • SHA256: e7ca6d54b3c02169dc1a05bf51da0597a4b15c36456478e296195f648140960b
  • Pointer size: 131 Bytes
  • Size of remote file: 129 kB
examples/case-6/prompt.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ No background music, with slight ambient outdoor noise. Eye-level medium close-up, centered composition. On a sunny day, a young white woman and a young South Asian man sit at a round black grid table under a black patio umbrella. The woman is on the left side of the frame, with long brown hair, wearing a pink sleeveless dress, and a thin black bracelet on her left wrist. The man is on the right side, with short black hair, wearing a blue short-sleeve Polo shirt with a black microphone clipped at the collar. A glass of water sits on the table in front of them. The scene is set on a wooden deck, with open water, distant apartment buildings, and green trees in the background, and large white clouds in the sky. Initially, the man in the blue Polo shirt looks at the woman across from him, raises both hands in gesture, and says in a gentle tone <speech>That's okay. We can order。</speech>. Then, the woman in the pink dress becomes serious, lowers her hands, and gently shakes her head, responding with a disappointed tone <speech>It's not about the wine. Nothing ever works out for me.</speech>. She then lifts her right hand and touches the glass on the table. The man leans forward slightly, listening intently. The woman then asks in a curious, probing tone <speech>Why do you say that?</speech>. The woman continues to look at him seriously and explains <speech>You never smelled at me when I smelled at you back at the start, so I decided to use hope this time.</speech>
examples/case-6/reference_image.jpg ADDED

Git LFS Details

  • SHA256: 12c7f8ddc7b35ccc56b545aa9a417450b7edb848c57906bb0df4b39f5f402933
  • Pointer size: 131 Bytes
  • Size of remote file: 185 kB
hymm/diffusion/__init__.py ADDED
File without changes
hymm/diffusion/pipelines/__init__.py ADDED
File without changes
hymm/diffusion/pipelines/mova_pipeline.py ADDED
@@ -0,0 +1,485 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import gc
3
+ import html
4
+ import re
5
+ from typing import Any, List, Optional, Tuple, Union
6
+
7
+ import ftfy
8
+ import torch
9
+ import torch.distributed as dist
10
+ from diffusers.models.autoencoders import AutoencoderKLWan
11
+ from diffusers.utils.torch_utils import randn_tensor
12
+ from diffusers.video_processor import VideoProcessor
13
+ from diffusers.image_processor import PipelineImageInput
14
+ from tqdm import tqdm
15
+ from transformers import T5TokenizerFast, UMT5EncoderModel
16
+
17
+ from hymm.models.modules.mova import MOVABridge
18
+ from hymm.models.modules.dac_vae import DAC
19
+ from hymm.diffusion.schedulers.flow_match_pair import FlowMatchPairScheduler
20
+
21
+
22
+ def _basic_clean(text):
23
+ text = ftfy.fix_text(text)
24
+ text = html.unescape(html.unescape(text))
25
+ return text.strip()
26
+
27
+
28
+ def _whitespace_clean(text):
29
+ text = re.sub(r"\s+", " ", text)
30
+ return text.strip()
31
+
32
+
33
+ def _prompt_clean(text):
34
+ return _whitespace_clean(_basic_clean(text))
35
+
36
+
37
+ def _retrieve_latents(encoder_output, generator=None, sample_mode="sample"):
38
+ if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
39
+ return encoder_output.latent_dist.sample(generator)
40
+ elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
41
+ return encoder_output.latent_dist.mode()
42
+ elif hasattr(encoder_output, "latents"):
43
+ return encoder_output.latents
44
+ else:
45
+ raise AttributeError("Could not access latents of provided encoder_output")
46
+
47
+
48
+ class MOVAPipeline:
49
+ """
50
+ Pipeline that drives MOVA inference identically to the original
51
+ MOVA/mova/diffusion/pipelines/pipeline_mova.py.
52
+
53
+ Differences from the original:
54
+ - Does NOT inherit from DiffusionPipeline (no auto-offload magic).
55
+ - Uses MOVABridge as the transformer, which already holds video_dit(s),
56
+ audio_dit, and bridge.
57
+ - Compatible with the codebase's DeepSpeed / FSDP launcher.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ transformer: MOVABridge,
63
+ video_vae: AutoencoderKLWan,
64
+ audio_vae: DAC,
65
+ text_encoder: UMT5EncoderModel,
66
+ tokenizer: T5TokenizerFast,
67
+ scheduler: FlowMatchPairScheduler,
68
+ boundary_ratio: float = 0.9,
69
+ device=None,
70
+ ):
71
+ self.transformer = transformer
72
+ self.video_vae = video_vae
73
+ self.audio_vae = audio_vae
74
+ self.text_encoder = text_encoder
75
+ self.tokenizer = tokenizer
76
+ self.scheduler = scheduler
77
+ self.boundary_ratio = boundary_ratio
78
+
79
+ self.vae_scale_factor_spatial = self.video_vae.config.scale_factor_spatial
80
+ self.vae_scale_factor_temporal = self.video_vae.config.scale_factor_temporal
81
+ self.video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
82
+
83
+ self.audio_vae_scale_factor = int(self.audio_vae.hop_length)
84
+ self.audio_sample_rate = self.audio_vae.sample_rate
85
+
86
+ self._device = device
87
+ self._cpu_offload = False
88
+
89
+ @property
90
+ def device(self):
91
+ if self._device is not None:
92
+ return self._device
93
+ for p in self.transformer.parameters():
94
+ return p.device
95
+ return torch.device("cpu")
96
+
97
+ def to(self, device):
98
+ self._device = device
99
+ self.transformer.to(device)
100
+ self.video_vae.to(device)
101
+ self.audio_vae.to(device)
102
+ self.text_encoder.to(device)
103
+ return self
104
+
105
+ def enable_cpu_offload(self, gpu_device=None):
106
+ """Enable sequential CPU offload to reduce peak GPU memory.
107
+
108
+ Only the active component lives on GPU at any time:
109
+ 1. text_encoder on GPU -> encode prompts -> move to CPU
110
+ 2. video_vae on GPU -> encode condition latents -> move to CPU
111
+ 3. transformer on GPU -> denoising loop -> move to CPU
112
+ 4. video_vae + audio_vae on GPU -> decode -> move to CPU
113
+ """
114
+ self._cpu_offload = True
115
+ if gpu_device is not None:
116
+ self._device = gpu_device
117
+
118
+ def normalize_video_latents(self, latents):
119
+ mean = torch.tensor(
120
+ self.video_vae.config.latents_mean, device=latents.device, dtype=latents.dtype,
121
+ ).view(1, self.video_vae.config.z_dim, 1, 1, 1)
122
+ inv_std = (1.0 / torch.tensor(
123
+ self.video_vae.config.latents_std, device=latents.device, dtype=latents.dtype,
124
+ )).view(1, self.video_vae.config.z_dim, 1, 1, 1)
125
+ return (latents - mean) * inv_std
126
+
127
+ def denormalize_video_latents(self, latents):
128
+ mean = torch.tensor(
129
+ self.video_vae.config.latents_mean, device=latents.device, dtype=latents.dtype,
130
+ ).view(1, self.video_vae.config.z_dim, 1, 1, 1)
131
+ std = torch.tensor(
132
+ self.video_vae.config.latents_std, device=latents.device, dtype=latents.dtype,
133
+ ).view(1, self.video_vae.config.z_dim, 1, 1, 1)
134
+ return latents * std + mean
135
+
136
+ def check_inputs(self, height, width, num_frames):
137
+ target_div = self.vae_scale_factor_spatial * 2
138
+ if height % target_div != 0 or width % target_div != 0:
139
+ raise ValueError(
140
+ f"`height` and `width` must be divisible by {target_div}, "
141
+ f"got {height} and {width}."
142
+ )
143
+ if num_frames % self.vae_scale_factor_temporal != 1:
144
+ raise ValueError(
145
+ f"`num_frames - 1` must be divisible by {self.vae_scale_factor_temporal}, "
146
+ f"got {num_frames - 1}."
147
+ )
148
+
149
+ def prepare_latents(
150
+ self,
151
+ image,
152
+ batch_size,
153
+ num_channels_latents,
154
+ height,
155
+ width,
156
+ num_frames,
157
+ dtype,
158
+ device,
159
+ generator=None,
160
+ latents=None,
161
+ last_image=None,
162
+ ):
163
+ num_latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
164
+ latent_height = height // self.vae_scale_factor_spatial
165
+ latent_width = width // self.vae_scale_factor_spatial
166
+
167
+ shape = (batch_size, num_channels_latents, num_latent_frames, latent_height, latent_width)
168
+
169
+ if latents is None:
170
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
171
+ else:
172
+ latents = latents.to(device=device, dtype=dtype)
173
+
174
+ image = image.unsqueeze(2)
175
+
176
+ if last_image is None:
177
+ video_condition = torch.cat(
178
+ [image, image.new_zeros(image.shape[0], image.shape[1], num_frames - 1, height, width)], dim=2,
179
+ )
180
+ else:
181
+ last_image = last_image.unsqueeze(2)
182
+ video_condition = torch.cat(
183
+ [image, image.new_zeros(image.shape[0], image.shape[1], num_frames - 2, height, width), last_image],
184
+ dim=2,
185
+ )
186
+ video_condition = video_condition.to(device=device, dtype=self.video_vae.dtype)
187
+
188
+ if isinstance(generator, list):
189
+ latent_condition = [
190
+ _retrieve_latents(self.video_vae.encode(video_condition), sample_mode="argmax")
191
+ for _ in generator
192
+ ]
193
+ latent_condition = torch.cat(latent_condition)
194
+ else:
195
+ latent_condition = _retrieve_latents(
196
+ self.video_vae.encode(video_condition), sample_mode="argmax",
197
+ )
198
+ latent_condition = latent_condition.repeat(batch_size, 1, 1, 1, 1)
199
+
200
+ latent_condition = latent_condition.to(dtype)
201
+ latent_condition = self.normalize_video_latents(latent_condition)
202
+
203
+ mask_lat_size = torch.ones(batch_size, 1, num_frames, latent_height, latent_width)
204
+ if last_image is None:
205
+ mask_lat_size[:, :, list(range(1, num_frames))] = 0
206
+ else:
207
+ mask_lat_size[:, :, list(range(1, num_frames - 1))] = 0
208
+ first_frame_mask = mask_lat_size[:, :, 0:1]
209
+ first_frame_mask = torch.repeat_interleave(first_frame_mask, dim=2, repeats=self.vae_scale_factor_temporal)
210
+ mask_lat_size = torch.concat([first_frame_mask, mask_lat_size[:, :, 1:, :]], dim=2)
211
+ mask_lat_size = mask_lat_size.view(batch_size, -1, self.vae_scale_factor_temporal, latent_height, latent_width)
212
+ mask_lat_size = mask_lat_size.transpose(1, 2)
213
+ mask_lat_size = mask_lat_size.to(latent_condition.device)
214
+
215
+ return latents, torch.concat([mask_lat_size, latent_condition], dim=1)
216
+
217
+ def prepare_audio_latents(
218
+ self,
219
+ audio,
220
+ batch_size,
221
+ num_channels,
222
+ num_samples,
223
+ dtype,
224
+ device,
225
+ generator=None,
226
+ latents=None,
227
+ ):
228
+ latent_t = (num_samples - 1) // self.audio_vae_scale_factor + 1
229
+ shape = (batch_size, num_channels, latent_t)
230
+ if latents is None:
231
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
232
+ else:
233
+ latents = latents.to(device=device, dtype=dtype)
234
+ return latents
235
+
236
+ def _get_t5_prompt_embeds(
237
+ self,
238
+ prompt,
239
+ num_videos_per_prompt=1,
240
+ max_sequence_length=512,
241
+ device=None,
242
+ dtype=None,
243
+ ):
244
+ device = device or self.device
245
+ dtype = dtype or self.text_encoder.dtype
246
+
247
+ prompt = [prompt] if isinstance(prompt, str) else prompt
248
+ prompt = [_prompt_clean(u) for u in prompt]
249
+ batch_size = len(prompt)
250
+
251
+ text_inputs = self.tokenizer(
252
+ prompt,
253
+ padding="max_length",
254
+ max_length=max_sequence_length,
255
+ truncation=True,
256
+ add_special_tokens=True,
257
+ return_attention_mask=True,
258
+ return_tensors="pt",
259
+ )
260
+ text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask
261
+ seq_lens = mask.gt(0).sum(dim=1).long()
262
+
263
+ prompt_embeds = self.text_encoder(
264
+ text_input_ids.to(device), mask.to(device),
265
+ ).last_hidden_state
266
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
267
+ prompt_embeds = [u[:v] for u, v in zip(prompt_embeds, seq_lens)]
268
+ prompt_embeds = torch.stack(
269
+ [torch.cat([u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))]) for u in prompt_embeds],
270
+ dim=0,
271
+ )
272
+
273
+ _, seq_len, _ = prompt_embeds.shape
274
+ prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1)
275
+ prompt_embeds = prompt_embeds.view(batch_size * num_videos_per_prompt, seq_len, -1)
276
+ return prompt_embeds
277
+
278
+ @torch.no_grad()
279
+ def __call__(
280
+ self,
281
+ prompt,
282
+ image,
283
+ audio_prompt=None,
284
+ negative_prompt="",
285
+ seed=42,
286
+ height=360,
287
+ width=640,
288
+ num_frames=193,
289
+ video_fps=24.0,
290
+ num_inference_steps=50,
291
+ visual_shift=5.0,
292
+ audio_shift=5.0,
293
+ cfg_scale=5.0,
294
+ remove_video_dit=False,
295
+ enable_vae_tiling=False,
296
+ vae_tile_sample_min_size=None,
297
+ vae_tile_sample_stride=None,
298
+ ):
299
+ """
300
+ Run the full MOVA denoising loop.
301
+ This replicates the exact logic from MOVA's __call__.
302
+
303
+ VAE tiling (Phase 4 decode only, fully isolated / default OFF):
304
+ enable_vae_tiling: when True, the video VAE decodes the latents tile by
305
+ tile (spatial), drastically cutting peak decode VRAM so that high-res
306
+ (e.g. 2K) videos no longer OOM. When False the decode path is byte-for-
307
+ byte identical to the original (VAE tiling is never touched).
308
+ vae_tile_sample_min_size: tile size (pixels, applied to both H and W).
309
+ None -> diffusers/Wan default (256), same as decode_wan_vae.py.
310
+ vae_tile_sample_stride: stride between tiles (pixels, H and W). overlap =
311
+ tile_size - stride. None -> diffusers/Wan default (192), i.e. 25% overlap,
312
+ same as decode_wan_vae.py.
313
+
314
+ Note on FSDP + SP: only the transformer is FSDP-sharded; the video VAE is a
315
+ full replica on every rank and the post-denoise latents are identical across
316
+ all SP ranks. VAE tiling is deterministic, so every rank produces the SAME
317
+ decoded video whether or not tiling is enabled.
318
+ """
319
+ self.check_inputs(height, width, num_frames)
320
+
321
+ audio_num_samples = int(self.audio_sample_rate * num_frames / video_fps)
322
+ device = self.device
323
+ offload = self._cpu_offload
324
+
325
+ # Deterministic noise generator (independent of global random state)
326
+ generator = torch.Generator(device=device)
327
+ generator.manual_seed(seed)
328
+
329
+ # ------------------------------------------------------------------
330
+ # Phase 1: Text encoding
331
+ # ------------------------------------------------------------------
332
+ if offload:
333
+ self.text_encoder.to(device)
334
+
335
+ self.scheduler.set_timesteps(num_inference_steps, shift=visual_shift, device=device)
336
+ self.scheduler.set_pair_postprocess_by_name(
337
+ "dual_sigma_shift",
338
+ visual_shift=float(visual_shift),
339
+ audio_shift=float(audio_shift),
340
+ )
341
+ paired_timesteps = self.scheduler.get_pairs()
342
+
343
+ prompt_embeds = self._get_t5_prompt_embeds(prompt)
344
+ audio_prompt_embeds = self._get_t5_prompt_embeds(audio_prompt) if audio_prompt is not None else None
345
+ negative_prompt_embeds = self._get_t5_prompt_embeds(negative_prompt)
346
+
347
+ if offload:
348
+ self.text_encoder.to("cpu")
349
+ torch.cuda.empty_cache()
350
+
351
+ # ------------------------------------------------------------------
352
+ # Phase 2: VAE encode (prepare latents)
353
+ # ------------------------------------------------------------------
354
+ if offload:
355
+ self.video_vae.to(device)
356
+
357
+ num_channels_latents = self.video_vae.config.z_dim
358
+ image = self.video_processor.preprocess(image, height=height, width=width).to(device, dtype=torch.float32)
359
+
360
+ latents, condition = self.prepare_latents(
361
+ image, 1, num_channels_latents, height, width, num_frames,
362
+ torch.float32, device, generator=generator, latents=None, last_image=None,
363
+ )
364
+ audio_latents = self.prepare_audio_latents(
365
+ None, 1, self.audio_vae.latent_dim, audio_num_samples,
366
+ torch.float32, device, generator=generator, latents=None,
367
+ )
368
+
369
+ if offload:
370
+ self.video_vae.to("cpu")
371
+ torch.cuda.empty_cache()
372
+
373
+ # ------------------------------------------------------------------
374
+ # Phase 3: Denoising loop (transformer on GPU)
375
+ # ------------------------------------------------------------------
376
+ if offload:
377
+ self.transformer.to(device)
378
+
379
+ cur_use_dit_2 = False
380
+ total_steps = paired_timesteps.shape[0]
381
+ switched = False
382
+ boundary_timestep = self.boundary_ratio * self.scheduler.config.num_train_timesteps
383
+
384
+ is_main = not dist.is_initialized() or dist.get_rank() == 0
385
+
386
+ for idx_step in tqdm(range(total_steps), disable=not is_main):
387
+ timestep, audio_timestep = paired_timesteps[idx_step]
388
+
389
+ if not switched and timestep.item() < boundary_timestep:
390
+ cur_use_dit_2 = True
391
+ if remove_video_dit:
392
+ self.transformer.video_dit = None
393
+ gc.collect()
394
+ switched = True
395
+
396
+ latent_model_input = torch.cat([latents, condition], dim=1)
397
+ timestep_t = timestep.unsqueeze(0).to(device=device, dtype=torch.float32)
398
+ audio_timestep_t = audio_timestep.unsqueeze(0).to(device=device, dtype=torch.float32)
399
+
400
+ noise_pred_posi = self.transformer(
401
+ visual_latents=latent_model_input,
402
+ audio_latents=audio_latents,
403
+ context=prompt_embeds,
404
+ timestep=timestep_t,
405
+ audio_context=audio_prompt_embeds,
406
+ audio_timestep=audio_timestep_t,
407
+ video_fps=video_fps,
408
+ num_train_timesteps=self.scheduler.config.num_train_timesteps,
409
+ use_video_dit_2=cur_use_dit_2,
410
+ )
411
+
412
+ if cfg_scale == 1.0:
413
+ visual_noise_pred = noise_pred_posi[0].float()
414
+ audio_noise_pred = noise_pred_posi[1].float()
415
+ else:
416
+ noise_pred_nega = self.transformer(
417
+ visual_latents=latent_model_input,
418
+ audio_latents=audio_latents,
419
+ context=negative_prompt_embeds,
420
+ timestep=timestep_t,
421
+ audio_context=None,
422
+ audio_timestep=audio_timestep_t,
423
+ video_fps=video_fps,
424
+ num_train_timesteps=self.scheduler.config.num_train_timesteps,
425
+ use_video_dit_2=cur_use_dit_2,
426
+ )
427
+ visual_noise_pred_nega = noise_pred_nega[0].float()
428
+ audio_noise_pred_nega = noise_pred_nega[1].float()
429
+ visual_noise_pred_posi = noise_pred_posi[0].float()
430
+ audio_noise_pred_posi = noise_pred_posi[1].float()
431
+ visual_noise_pred = visual_noise_pred_nega + cfg_scale * (visual_noise_pred_posi - visual_noise_pred_nega)
432
+ audio_noise_pred = audio_noise_pred_nega + cfg_scale * (audio_noise_pred_posi - audio_noise_pred_nega)
433
+
434
+ next_timestep = paired_timesteps[idx_step + 1, 0] if idx_step + 1 < total_steps else None
435
+ next_audio_timestep = paired_timesteps[idx_step + 1, 1] if idx_step + 1 < total_steps else None
436
+ latents = self.scheduler.step_from_to(
437
+ visual_noise_pred, timestep, next_timestep, latents,
438
+ )
439
+ audio_latents = self.scheduler.step_from_to(
440
+ audio_noise_pred, audio_timestep, next_audio_timestep, audio_latents,
441
+ )
442
+
443
+ if offload:
444
+ self.transformer.to("cpu")
445
+ torch.cuda.empty_cache()
446
+
447
+ # ------------------------------------------------------------------
448
+ # Phase 4: VAE decode
449
+ # ------------------------------------------------------------------
450
+ if offload:
451
+ self.video_vae.to(device)
452
+
453
+ # Optional tiled VAE decode (default OFF -> original path untouched). When
454
+ # enabled, decode the latents tile-by-tile spatially to cut peak decode VRAM
455
+ # for high-res (e.g. 2K) videos. Mirrors decode_wan_vae.py: min_size/stride
456
+ # map to the AutoencoderKLWan (height & width) tiling knobs; None keeps the
457
+ # diffusers defaults (256 / 192).
458
+ if enable_vae_tiling:
459
+ tiling_kwargs = {}
460
+ if vae_tile_sample_min_size is not None:
461
+ tiling_kwargs["tile_sample_min_height"] = vae_tile_sample_min_size
462
+ tiling_kwargs["tile_sample_min_width"] = vae_tile_sample_min_size
463
+ if vae_tile_sample_stride is not None:
464
+ tiling_kwargs["tile_sample_stride_height"] = vae_tile_sample_stride
465
+ tiling_kwargs["tile_sample_stride_width"] = vae_tile_sample_stride
466
+ self.video_vae.enable_tiling(**tiling_kwargs)
467
+
468
+ video_latents = self.denormalize_video_latents(latents)
469
+ with torch.autocast("cuda", dtype=torch.bfloat16):
470
+ video = self.video_vae.decode(video_latents).sample
471
+ video = self.video_processor.postprocess_video(video, output_type="pil")
472
+
473
+ if offload:
474
+ self.video_vae.to("cpu")
475
+ self.audio_vae.to(device)
476
+ torch.cuda.empty_cache()
477
+
478
+ with torch.autocast("cuda", dtype=torch.float32):
479
+ audio = self.audio_vae.decode(audio_latents)
480
+
481
+ if offload:
482
+ self.audio_vae.to("cpu")
483
+ torch.cuda.empty_cache()
484
+
485
+ return video, audio
hymm/diffusion/schedulers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .flow_match_pair import FlowMatchPairScheduler
hymm/diffusion/schedulers/flow_match_pair.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+
4
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
5
+ from diffusers.schedulers.scheduling_utils import SchedulerMixin
6
+
7
+
8
+ class FlowMatchSchedulerBase:
9
+ def __init__(
10
+ self,
11
+ num_inference_steps=100,
12
+ num_train_timesteps=1000,
13
+ shift=3.0,
14
+ sigma_max=1.0,
15
+ sigma_min=0.003 / 1.002,
16
+ inverse_timesteps=False,
17
+ extra_one_step=False,
18
+ reverse_sigmas=False,
19
+ exponential_shift=False,
20
+ exponential_shift_mu=None,
21
+ shift_terminal=None,
22
+ ):
23
+ self.num_train_timesteps = num_train_timesteps
24
+ self.shift = shift
25
+ self.sigma_max = sigma_max
26
+ self.sigma_min = sigma_min
27
+ self.inverse_timesteps = inverse_timesteps
28
+ self.extra_one_step = extra_one_step
29
+ self.reverse_sigmas = reverse_sigmas
30
+ self.exponential_shift = exponential_shift
31
+ self.exponential_shift_mu = exponential_shift_mu
32
+ self.shift_terminal = shift_terminal
33
+ self.train_timesteps = None
34
+ self.train_sigmas = None
35
+ self.set_timesteps(num_train_timesteps)
36
+ self.set_timesteps(num_inference_steps)
37
+
38
+ def set_timesteps(self, num_inference_steps=100, denoising_strength=1.0, training=False, shift=None, dynamic_shift_len=None, device=None):
39
+ if shift is not None:
40
+ self.shift = shift
41
+ sigma_start = self.sigma_min + (self.sigma_max - self.sigma_min) * denoising_strength
42
+ if self.extra_one_step:
43
+ self.sigmas = torch.linspace(sigma_start, self.sigma_min, num_inference_steps + 1)[:-1]
44
+ else:
45
+ self.sigmas = torch.linspace(sigma_start, self.sigma_min, num_inference_steps)
46
+ if self.inverse_timesteps:
47
+ self.sigmas = torch.flip(self.sigmas, dims=[0])
48
+ if self.exponential_shift:
49
+ mu = self.calculate_shift(dynamic_shift_len) if dynamic_shift_len is not None else self.exponential_shift_mu
50
+ self.sigmas = math.exp(mu) / (math.exp(mu) + (1 / self.sigmas - 1))
51
+ else:
52
+ self.sigmas = self.shift * self.sigmas / (1 + (self.shift - 1) * self.sigmas)
53
+ if self.shift_terminal is not None:
54
+ one_minus_z = 1 - self.sigmas
55
+ scale_factor = one_minus_z[-1] / (1 - self.shift_terminal)
56
+ self.sigmas = 1 - (one_minus_z / scale_factor)
57
+ if self.reverse_sigmas:
58
+ self.sigmas = 1 - self.sigmas
59
+ self.timesteps = self.sigmas * self.num_train_timesteps
60
+ if self.train_timesteps is None:
61
+ self.train_timesteps = self.timesteps
62
+ self.train_sigmas = self.sigmas
63
+ if training:
64
+ x = self.timesteps
65
+ y = torch.exp(-2 * ((x - num_inference_steps / 2) / num_inference_steps) ** 2)
66
+ y_shifted = y - y.min()
67
+ bsmntw_weighing = y_shifted * (num_inference_steps / y_shifted.sum())
68
+ self.linear_timesteps_weights = bsmntw_weighing
69
+ self.training = True
70
+ else:
71
+ self.training = False
72
+
73
+ def step(self, model_output, timestep, sample, to_final=False, **kwargs):
74
+ if isinstance(timestep, torch.Tensor):
75
+ timestep = timestep.cpu()
76
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
77
+ sigma = self.sigmas[timestep_id]
78
+ if to_final or timestep_id + 1 >= len(self.timesteps):
79
+ sigma_ = 1 if (self.inverse_timesteps or self.reverse_sigmas) else 0
80
+ else:
81
+ sigma_ = self.sigmas[timestep_id + 1]
82
+ prev_sample = sample + model_output * (sigma_ - sigma)
83
+ return prev_sample
84
+
85
+ def add_noise(self, original_samples, noise, timestep):
86
+ if isinstance(timestep, torch.Tensor):
87
+ timestep = timestep.cpu()
88
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
89
+ sigma = self.sigmas[timestep_id]
90
+ sample = (1 - sigma) * original_samples + sigma * noise
91
+ return sample
92
+
93
+ def training_target(self, sample, noise, timestep):
94
+ return noise - sample
95
+
96
+ def calculate_shift(
97
+ self,
98
+ image_seq_len,
99
+ base_seq_len: int = 256,
100
+ max_seq_len: int = 8192,
101
+ base_shift: float = 0.5,
102
+ max_shift: float = 0.9,
103
+ ):
104
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
105
+ b = base_shift - m * base_seq_len
106
+ mu = image_seq_len * m + b
107
+ return mu
108
+
109
+
110
+ class FlowMatchPairScheduler(FlowMatchSchedulerBase, SchedulerMixin, ConfigMixin):
111
+ """
112
+ Paired timestep scheduler for dual-tower (video + audio) diffusion.
113
+ Returns tensor with shape [num_timesteps, 2], each row is (visual_t, audio_t).
114
+ """
115
+
116
+ @register_to_config
117
+ def __init__(
118
+ self,
119
+ num_inference_steps=100,
120
+ num_train_timesteps=1000,
121
+ shift=3.0,
122
+ sigma_max=1.0,
123
+ sigma_min=0.003 / 1.002,
124
+ inverse_timesteps=False,
125
+ extra_one_step=False,
126
+ reverse_sigmas=False,
127
+ exponential_shift=False,
128
+ exponential_shift_mu=None,
129
+ shift_terminal=None,
130
+ ):
131
+ self._pair_postprocess_fn = None
132
+ self._pair_postprocess_requires_source = False
133
+ self.pair_timesteps = None
134
+ self.pair_sigmas = None
135
+ self.timesteps = None
136
+ self.sigmas = None
137
+ super().__init__(
138
+ num_inference_steps=num_inference_steps,
139
+ num_train_timesteps=num_train_timesteps,
140
+ shift=shift,
141
+ sigma_max=sigma_max,
142
+ sigma_min=sigma_min,
143
+ inverse_timesteps=inverse_timesteps,
144
+ extra_one_step=extra_one_step,
145
+ reverse_sigmas=reverse_sigmas,
146
+ exponential_shift=exponential_shift,
147
+ exponential_shift_mu=exponential_shift_mu,
148
+ shift_terminal=shift_terminal,
149
+ )
150
+
151
+ def set_pair_postprocess(self, fn):
152
+ if fn is not None and not callable(fn):
153
+ raise TypeError("pair_postprocess must be a callable object or None")
154
+ self._pair_postprocess_fn = fn
155
+ self._pair_postprocess_requires_source = False if fn is None else bool(getattr(fn, "_requires_source", False))
156
+ if self.timesteps is None or self.sigmas is None:
157
+ raise RuntimeError("Scheduler not initialized, please call set_timesteps() first")
158
+ self._refresh_pair_cache()
159
+
160
+ def set_pair_postprocess_by_name(self, name, **kwargs):
161
+ if name is None or str(name).lower() in ("none", "off", "false", "no"):
162
+ self.set_pair_postprocess(None)
163
+ return
164
+ if name == "dual_sigma_shift":
165
+ visual_shift = float(kwargs.get("visual_shift", self.shift))
166
+ audio_shift = float(kwargs.get("audio_shift", self.shift))
167
+ visual_denoising_strength = float(kwargs.get("visual_denoising_strength", 1.0))
168
+ audio_denoising_strength = float(kwargs.get("audio_denoising_strength", 1.0))
169
+ visual_mu = kwargs.get("visual_exponential_shift_mu", self.exponential_shift_mu)
170
+ audio_mu = kwargs.get("audio_exponential_shift_mu", self.exponential_shift_mu)
171
+
172
+ def _dual_sigma_shift(pairs, *, source):
173
+ num_steps = pairs.shape[0]
174
+ device = pairs.device
175
+ dtype = pairs.dtype
176
+
177
+ def _build_column(shift_value, denoising_strength, mu_override):
178
+ sigma_start = self.sigma_min + (self.sigma_max - self.sigma_min) * denoising_strength
179
+ if self.extra_one_step:
180
+ base = torch.linspace(sigma_start, self.sigma_min, num_steps + 1, device=device, dtype=dtype)[:-1]
181
+ else:
182
+ base = torch.linspace(sigma_start, self.sigma_min, num_steps, device=device, dtype=dtype)
183
+ if self.inverse_timesteps:
184
+ base = torch.flip(base, dims=[0])
185
+ if self.exponential_shift:
186
+ mu_value = mu_override
187
+ if mu_value is None:
188
+ raise RuntimeError("exponential_shift is enabled but exponential_shift_mu is not provided")
189
+ exp_mu = math.exp(float(mu_value))
190
+ base = exp_mu / (exp_mu + (1 / base - 1))
191
+ else:
192
+ base = shift_value * base / (1 + (shift_value - 1) * base)
193
+ if self.shift_terminal is not None:
194
+ one_minus_z = 1 - base
195
+ scale_factor = one_minus_z[-1] / (1 - self.shift_terminal)
196
+ base = 1 - (one_minus_z / scale_factor)
197
+ if self.reverse_sigmas:
198
+ base = 1 - base
199
+ if source == "timesteps":
200
+ return base * self.num_train_timesteps
201
+ return base
202
+
203
+ visual_column = _build_column(visual_shift, visual_denoising_strength, visual_mu)
204
+ audio_column = _build_column(audio_shift, audio_denoising_strength, audio_mu)
205
+ return torch.stack([visual_column, audio_column], dim=1)
206
+
207
+ _dual_sigma_shift._requires_source = True
208
+ self.set_pair_postprocess(_dual_sigma_shift)
209
+ return
210
+ raise ValueError(f"Unsupported pair postprocessing name: {name}")
211
+
212
+ def _make_pairs_from_vector(self, vec):
213
+ return torch.stack([vec, vec], dim=1)
214
+
215
+ def get_pairs(self, source="timesteps"):
216
+ if source == "timesteps":
217
+ pairs = self.pair_timesteps
218
+ elif source == "sigmas":
219
+ pairs = self.pair_sigmas
220
+ else:
221
+ raise ValueError("source only supports 'timesteps' or 'sigmas'")
222
+ if pairs is None:
223
+ raise RuntimeError("Scheduler not initialized, please call set_timesteps() first")
224
+ return pairs
225
+
226
+ @property
227
+ def visual_timesteps(self):
228
+ if self.pair_timesteps is None:
229
+ raise RuntimeError("Scheduler not initialized")
230
+ return self.pair_timesteps[:, 0]
231
+
232
+ @property
233
+ def audio_timesteps(self):
234
+ if self.pair_timesteps is None:
235
+ raise RuntimeError("Scheduler not initialized")
236
+ return self.pair_timesteps[:, 1]
237
+
238
+ def set_timesteps(self, *args, **kwargs):
239
+ super().set_timesteps(*args, **kwargs)
240
+ self._refresh_pair_cache()
241
+
242
+ def timestep_to_sigma(self, timestep):
243
+ t_value = float(timestep)
244
+ t_cpu = torch.tensor(t_value)
245
+ idx = torch.argmin((self.train_timesteps - t_cpu).abs())
246
+ return self.train_sigmas[idx]
247
+
248
+ def step_from_to(self, model_output, timestep_from, timestep_to, sample):
249
+ sigma_from = self.timestep_to_sigma(timestep_from)
250
+ if timestep_to is None:
251
+ sigma_to = torch.tensor(
252
+ 1.0 if (self.inverse_timesteps or self.reverse_sigmas) else 0.0,
253
+ device=sigma_from.device, dtype=sigma_from.dtype,
254
+ )
255
+ else:
256
+ sigma_to = self.timestep_to_sigma(timestep_to)
257
+ prev_sample = sample + model_output * (sigma_to - sigma_from)
258
+ return prev_sample
259
+
260
+ def _refresh_pair_cache(self):
261
+ if self.timesteps is None or self.sigmas is None:
262
+ return
263
+
264
+ def _apply_postprocess(pairs, source):
265
+ if self._pair_postprocess_fn is None:
266
+ return pairs
267
+ if self._pair_postprocess_requires_source:
268
+ modified = self._pair_postprocess_fn(pairs, source=source)
269
+ else:
270
+ modified = self._pair_postprocess_fn(pairs)
271
+ return modified
272
+
273
+ base_pairs_timesteps = self._make_pairs_from_vector(self.timesteps)
274
+ base_pairs_sigmas = self._make_pairs_from_vector(self.sigmas)
275
+
276
+ self.pair_timesteps = _apply_postprocess(base_pairs_timesteps, "timesteps")
277
+ self.pair_sigmas = _apply_postprocess(base_pairs_sigmas, "sigmas")
hymm/models/__init__.py ADDED
File without changes
hymm/models/modules/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .mova import MOVABridge, FusedMOVABlock
hymm/models/modules/block_sparse_attention/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .bsa_interface import (
2
+ flash_attn_bsa_3d,
3
+ flash_attn_bsa_cross,
4
+ flash_attn_bsa_3d_audio_guided,
5
+ flash_attn_bsa_3d_variance_guided,
6
+ compute_audio_spatial_concentration_gate,
7
+ compute_timestep_reliability_gate,
8
+ compute_block_audio_saliency,
9
+ audio_weighted_mean_pooling_compression,
10
+ compute_channel_variance_density,
11
+ )
12
+
13
+ from .bias_rectification import (
14
+ bsa_taylor_sparse_attn,
15
+ bsa_rectified_sparse_attn,
16
+ )
hymm/models/modules/block_sparse_attention/bias_rectification.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ from typing import Optional, Tuple
4
+
5
+ import torch
6
+ import torch.distributed as dist
7
+ import torch.nn.functional as F
8
+
9
+ from .bsa_interface import (
10
+ mean_pooling_compression,
11
+ masked_mean_pooling_compression,
12
+ cal_score,
13
+ get_select_indices_topk_from_score,
14
+ get_select_indices_cdf_from_score,
15
+ get_select_indices_cdf_topk_from_score,
16
+ create_mask_from_indices_varlen,
17
+ attn_fwd_bsa_varlen_triton,
18
+ attn_bwd_bsa_varlen_triton,
19
+ )
20
+
21
+
22
+ # =====================================================================
23
+ # Pure BSA Triton wrapper — only this is an autograd Function.
24
+ # Everything else (pooling, scoring, correction) lives outside in
25
+ # autograd-friendly PyTorch ops so gradients flow correctly.
26
+ # =====================================================================
27
+ class _bsa_kernel_only(torch.autograd.Function):
28
+ """Forward + backward through `attn_fwd_bsa_varlen_triton` only.
29
+
30
+ Returns (o, lse_natural). lse is the log-sum-exp of the *attended* keys
31
+ in natural log units (= LN2 * the kernel's log2 lse).
32
+ """
33
+
34
+ @staticmethod
35
+ def forward(ctx, q, k, v, sm_scale, block_indices, block_indices_lens,
36
+ chunk_size_q, chunk_size_k, sparsity, kv_valid_mask):
37
+ o, lse_natural = attn_fwd_bsa_varlen_triton(
38
+ q, k, v, sm_scale, block_indices, block_indices_lens,
39
+ chunk_size_q, chunk_size_k, sparsity,
40
+ kv_valid_mask=kv_valid_mask,
41
+ )
42
+ ctx.save_for_backward(q, k, v, o, lse_natural, block_indices, block_indices_lens)
43
+ ctx.sm_scale = sm_scale
44
+ ctx.chunk_size_q = chunk_size_q
45
+ ctx.chunk_size_k = chunk_size_k
46
+ ctx.sparsity = sparsity
47
+ ctx.kv_valid_mask = kv_valid_mask
48
+ return o, lse_natural
49
+
50
+ @staticmethod
51
+ def backward(ctx, do, dlse_unused):
52
+ q, k, v, o, lse, block_indices, block_indices_lens = ctx.saved_tensors
53
+ dq = torch.empty_like(q)
54
+ dk = torch.empty_like(k)
55
+ dv = torch.empty_like(v)
56
+ do = do.contiguous()
57
+ attn_bwd_bsa_varlen_triton(
58
+ do, q, k, v, o, dq, dk, dv,
59
+ ctx.sm_scale, lse,
60
+ block_indices, block_indices_lens,
61
+ ctx.chunk_size_q, ctx.chunk_size_k, ctx.sparsity,
62
+ kv_valid_mask=ctx.kv_valid_mask,
63
+ )
64
+ # 10 inputs: q, k, v, sm_scale, block_indices, block_indices_lens,
65
+ # chunk_size_q, chunk_size_k, sparsity, kv_valid_mask
66
+ return dq, dk, dv, None, None, None, None, None, None, None
67
+
68
+
69
+ def _bsa_kernel(q, k, v, sm_scale, block_indices, block_indices_lens,
70
+ chunk_size_q, chunk_size_k, sparsity, kv_valid_mask):
71
+ """Wrapper that exposes (o, lse) but always detaches lse.
72
+
73
+ `lse` is an attention normalisation statistic used downstream as a
74
+ coefficient in the Taylor merge weights. We treat it as a fixed
75
+ quantity (FlashAttention convention) and explicitly detach it so the
76
+ autograd engine does not try to backpropagate through a path we
77
+ intentionally do not implement in `_bsa_kernel_only.backward`.
78
+ """
79
+ o, lse = _bsa_kernel_only.apply(
80
+ q, k, v, sm_scale, block_indices, block_indices_lens,
81
+ chunk_size_q, chunk_size_k, sparsity, kv_valid_mask,
82
+ )
83
+ return o, lse.detach()
84
+
85
+
86
+ # =====================================================================
87
+ # Shared helpers (block-level pooling, selection, validity)
88
+ # =====================================================================
89
+ def _pool_qk(q, k, chunk_size_q, chunk_size_k, kv_valid_mask, q_valid_mask):
90
+ """Pool Q and K to block representatives, masking padded tokens.
91
+
92
+ Self-attn case (Q/K share grid): both use kv_valid_mask.
93
+ Cross-attn case: each side uses its own valid_mask.
94
+
95
+ All ops are autograd-tracked (gradients to q / k flow through pooling).
96
+ """
97
+ if q_valid_mask is not None:
98
+ q_cmp = masked_mean_pooling_compression(q, chunk_size_q, q_valid_mask)
99
+ elif kv_valid_mask is not None and q.shape[2] == k.shape[2]:
100
+ # self-attention: Q shares K's valid mask
101
+ q_cmp = masked_mean_pooling_compression(q, chunk_size_q, kv_valid_mask)
102
+ else:
103
+ q_cmp = mean_pooling_compression(q, chunk_size_q)
104
+ if kv_valid_mask is not None:
105
+ k_cmp = masked_mean_pooling_compression(k, chunk_size_k, kv_valid_mask)
106
+ else:
107
+ k_cmp = mean_pooling_compression(k, chunk_size_k)
108
+ return q_cmp, k_cmp
109
+
110
+
111
+ def _pool_v(v, chunk_size_k, kv_valid_mask):
112
+ if kv_valid_mask is not None:
113
+ return masked_mean_pooling_compression(v, chunk_size_k, kv_valid_mask)
114
+ return mean_pooling_compression(v, chunk_size_k)
115
+
116
+
117
+ def _select_blocks(selection_score, sparsity, cdf_threshold, sm_scale):
118
+ """topk / top-p / hybrid selection (non-differentiable)."""
119
+ if sparsity is not None and cdf_threshold is None:
120
+ return get_select_indices_topk_from_score(selection_score, sparsity)
121
+ if sparsity is None and cdf_threshold is not None:
122
+ return get_select_indices_cdf_from_score(selection_score, cdf_threshold, sm_scale)
123
+ if sparsity is not None and cdf_threshold is not None:
124
+ return get_select_indices_cdf_topk_from_score(selection_score, sparsity, cdf_threshold, sm_scale)
125
+ raise ValueError("Either sparsity or cdf_threshold must be provided")
126
+
127
+
128
+ def _compute_block_valid_mask(kv_valid_mask: torch.Tensor, chunk_size: int) -> torch.Tensor:
129
+ """A K block is valid if any token within it is valid."""
130
+ S = kv_valid_mask.shape[0]
131
+ num_blocks = S // chunk_size
132
+ return kv_valid_mask.view(num_blocks, chunk_size).any(dim=-1)
133
+
134
+
135
+ def _compute_block_valid_token_counts(kv_valid_mask: torch.Tensor, chunk_size: int) -> torch.Tensor:
136
+ S = kv_valid_mask.shape[0]
137
+ num_blocks = S // chunk_size
138
+ return kv_valid_mask.view(num_blocks, chunk_size).sum(dim=-1).float()
139
+
140
+
141
+ def _masked_softmax_along_last(score: torch.Tensor, sm_scale: float,
142
+ block_valid: Optional[torch.Tensor]) -> torch.Tensor:
143
+ """softmax(score * sm_scale, dim=-1) with -inf at padded K blocks."""
144
+ if block_valid is None:
145
+ return torch.softmax(score * sm_scale, dim=-1)
146
+ masked = score.masked_fill(~block_valid.view(1, 1, 1, -1), float('-inf'))
147
+ return torch.softmax(masked * sm_scale, dim=-1)
148
+
149
+
150
+ def _masked_var_along_last(probs: torch.Tensor,
151
+ block_valid: Optional[torch.Tensor]) -> torch.Tensor:
152
+ """Variance of softmax distribution restricted to valid K blocks.
153
+
154
+ Without masking, padded blocks contribute zero (since A_pool=0 there) but
155
+ inflate the denominator → biases variance downward → biases the LIVEditor
156
+ flat/sharp split. We compute Var[p] only over valid entries.
157
+ """
158
+ if block_valid is None:
159
+ return probs.var(dim=-1, unbiased=False)
160
+ bv = block_valid.view(1, 1, 1, -1).to(probs.dtype)
161
+ n_valid = bv.sum(dim=-1).clamp(min=1.0)
162
+ p_masked = probs * bv
163
+ mean = p_masked.sum(dim=-1, keepdim=True) / n_valid
164
+ var = ((p_masked - mean) ** 2 * bv).sum(dim=-1) / n_valid
165
+ return var
166
+
167
+
168
+ def _global_quantile_across_sp(values: torch.Tensor, q: float) -> torch.Tensor:
169
+
170
+ # Imported at call scope but NOT guarded by try/except: swallowing an import
171
+ # error here would make one rank skip the all_gather below while its peers
172
+ # still enter it, which is an unrecoverable NCCL hang rather than a crash.
173
+ from hymm.utils.parallel_states import nccl_info, get_sequence_parallel_state
174
+
175
+ if (not get_sequence_parallel_state()) or nccl_info.sp_size <= 1:
176
+ return torch.quantile(values.float().reshape(-1), q)
177
+
178
+ sp_group = nccl_info.sp_group
179
+ flat = values.float().reshape(-1).contiguous()
180
+
181
+ # All ranks in SP have the same flat shape because Q/K/V are split by HEAD,
182
+ # not by token. So we can use a tensor all-gather.
183
+ world_size = nccl_info.sp_size
184
+ gather_list = [torch.empty_like(flat) for _ in range(world_size)]
185
+ dist.all_gather(gather_list, flat, group=sp_group)
186
+ global_flat = torch.cat(gather_list, dim=0)
187
+ return torch.quantile(global_flat, q)
188
+
189
+
190
+ def _taylor_merge_pytorch(
191
+ q: torch.Tensor,
192
+ k_cmp: torch.Tensor,
193
+ v_cmp: torch.Tensor,
194
+ o_spa: torch.Tensor,
195
+ lse_spa: torch.Tensor,
196
+ nonsel_mask: torch.Tensor,
197
+ is_flat: torch.Tensor,
198
+ block_token_counts: torch.Tensor,
199
+ chunk_size_q: int,
200
+ sm_scale: float,
201
+ ) -> torch.Tensor:
202
+
203
+ B, H, S_q, D = o_spa.shape
204
+ N_q, N_k = nonsel_mask.shape[-2], nonsel_mask.shape[-1]
205
+
206
+ nonsel_per_tok = (
207
+ nonsel_mask.unsqueeze(3)
208
+ .expand(B, H, N_q, chunk_size_q, N_k)
209
+ .contiguous()
210
+ .reshape(B, H, S_q, N_k)
211
+ )
212
+
213
+ raw_scores = torch.matmul(q, k_cmp.transpose(-1, -2)) * sm_scale # [B, H, S_q, N_k]
214
+ log_counts = torch.log(block_token_counts.float().clamp(min=1.0)).view(1, 1, 1, N_k)
215
+ scores_full = raw_scores + log_counts.to(raw_scores.dtype)
216
+
217
+ # ---- LSE: -inf-masked path; logsumexp handles all-(-inf) rows safely ----
218
+ scores_inf = scores_full.masked_fill(~nonsel_per_tok, float('-inf'))
219
+ lse_taylor = torch.logsumexp(scores_inf, dim=-1) # [B, H, S_q]
220
+ has_taylor = torch.isfinite(lse_taylor) # bool
221
+
222
+ # ---- Softmax: use a finite sentinel to avoid NaN; gate rows w/o nonsel ----
223
+ LARGE_NEG = -1.0e30
224
+ scores_safe = scores_full.masked_fill(~nonsel_per_tok, LARGE_NEG)
225
+ weights = torch.softmax(scores_safe, dim=-1) # finite everywhere
226
+ # For rows with NO valid nonsel, every entry is LARGE_NEG → softmax = uniform
227
+ # → zero them out so o_taylor = 0 there.
228
+ weights = weights * has_taylor.unsqueeze(-1).to(weights.dtype)
229
+ o_taylor = torch.matmul(weights, v_cmp)
230
+
231
+ # ---- Stable lse merge ----
232
+ lse_max = torch.maximum(lse_spa, lse_taylor)
233
+ finite = torch.isfinite(lse_max)
234
+
235
+ # diff is undefined when both lses are -inf (rare; lse_spa is always finite
236
+ # in practice because BSA selects ≥1 block). Replace those NaNs by 0 inside
237
+ # the discarded `where` branch so backward never sees NaN.
238
+ diff = torch.minimum(lse_spa, lse_taylor) - lse_max
239
+ diff = torch.where(finite, diff, torch.zeros_like(diff))
240
+ lse_total = torch.where(finite, lse_max + torch.log1p(torch.exp(diff)), lse_max)
241
+
242
+ w_spa = torch.where(finite, torch.exp(lse_spa - lse_total), torch.ones_like(lse_total))
243
+ w_taylor = torch.where(
244
+ finite & has_taylor,
245
+ torch.exp(lse_taylor - lse_total),
246
+ torch.zeros_like(lse_total),
247
+ )
248
+
249
+ merged = w_spa.unsqueeze(-1) * o_spa + w_taylor.unsqueeze(-1) * o_taylor
250
+
251
+ # Sharp queries (block-level boolean) keep o_spa unchanged.
252
+ flat_tok = (
253
+ is_flat.unsqueeze(-1)
254
+ .expand(B, H, N_q, chunk_size_q)
255
+ .contiguous()
256
+ .reshape(B, H, S_q)
257
+ )
258
+ return torch.where(flat_tok.unsqueeze(-1), merged, o_spa).to(o_spa.dtype).contiguous()
259
+
260
+
261
+ def bsa_taylor_sparse_attn(
262
+ q: torch.Tensor, # [B, H, S_q, D] in 3d-block layout
263
+ k: torch.Tensor, # [B, H, S_k, D]
264
+ v: torch.Tensor, # [B, H, S_k, D]
265
+ chunk_size_q: int,
266
+ chunk_size_k: int,
267
+ sparsity: Optional[float],
268
+ cdf_threshold: Optional[float],
269
+ sm_scale: float,
270
+ *,
271
+ alpha_f: float = 0.5,
272
+ kv_valid_mask: Optional[torch.Tensor] = None,
273
+ q_valid_mask: Optional[torch.Tensor] = None,
274
+ score_modifier: Optional[torch.Tensor] = None,
275
+ use_triton_inference: bool = True,
276
+ ) -> torch.Tensor:
277
+ # --- (a) pool Q and K (autograd-tracked) ---
278
+ q_cmp, k_cmp = _pool_qk(q, k, chunk_size_q, chunk_size_k, kv_valid_mask, q_valid_mask)
279
+
280
+ # --- (b) block scoring + selection (selection itself is non-differentiable) ---
281
+ raw_score = cal_score(q_cmp, k_cmp) # [B, H, N_q, N_k]
282
+ with torch.no_grad():
283
+ sel_score = raw_score * score_modifier if score_modifier is not None else raw_score
284
+ block_indices, block_indices_lens = _select_blocks(sel_score, sparsity, cdf_threshold, sm_scale)
285
+
286
+ # --- (c) BSA Triton kernel; this is the only autograd Function call ---
287
+ o_spa, lse_spa = _bsa_kernel(
288
+ q, k, v, sm_scale, block_indices, block_indices_lens,
289
+ chunk_size_q, chunk_size_k, sparsity, kv_valid_mask,
290
+ )
291
+
292
+ # --- (d) Taylor side products (block_valid, A_pool, M_n, is_flat) ---
293
+ block_valid = _compute_block_valid_mask(kv_valid_mask, chunk_size_k) if kv_valid_mask is not None else None
294
+
295
+ with torch.no_grad():
296
+ sel_mask = create_mask_from_indices_varlen(block_indices, raw_score.shape[-1])
297
+ nonsel_mask = ~sel_mask
298
+ if block_valid is not None:
299
+ nonsel_mask = nonsel_mask & block_valid.view(1, 1, 1, -1)
300
+
301
+ # Block-level true probability distribution (no score_modifier, no scale boost)
302
+ A_pool_for_class = _masked_softmax_along_last(raw_score.detach(), sm_scale, block_valid)
303
+ M_n = _masked_var_along_last(A_pool_for_class, block_valid) # [B, H, N_q]
304
+ tau_M = _global_quantile_across_sp(M_n, alpha_f)
305
+ is_flat = M_n < tau_M
306
+
307
+ if kv_valid_mask is not None:
308
+ btc = _compute_block_valid_token_counts(kv_valid_mask, chunk_size_k)
309
+ else:
310
+ btc = torch.full(
311
+ (k.shape[2] // chunk_size_k,), float(chunk_size_k),
312
+ device=q.device, dtype=torch.float32,
313
+ )
314
+
315
+ # --- (e) v_cmp (autograd-tracked: gradients to V flow through this) ---
316
+ v_cmp = _pool_v(v, chunk_size_k, kv_valid_mask)
317
+
318
+ # --- (f) merge (autograd-tracked) ---
319
+ use_triton = (
320
+ use_triton_inference
321
+ and not torch.is_grad_enabled()
322
+ and q.is_cuda
323
+ )
324
+ if use_triton:
325
+ try:
326
+ from .bias_rectification_triton import taylor_merge_triton
327
+ return taylor_merge_triton(
328
+ q, k_cmp, v_cmp, o_spa, lse_spa, nonsel_mask, is_flat, btc,
329
+ chunk_size_q, sm_scale,
330
+ )
331
+ except Exception as e:
332
+ # Fall through to PyTorch path on any kernel error.
333
+ print(f"[bias_rectification] taylor_merge_triton fallback: {e}")
334
+
335
+ return _taylor_merge_pytorch(
336
+ q, k_cmp, v_cmp, o_spa, lse_spa, nonsel_mask, is_flat, btc,
337
+ chunk_size_q, sm_scale,
338
+ )
339
+
340
+ def _rectified_correction_pytorch(
341
+ o_spa: torch.Tensor,
342
+ R_n: torch.Tensor,
343
+ A_pool: torch.Tensor,
344
+ rectify_mask: torch.Tensor,
345
+ v_cmp: torch.Tensor,
346
+ chunk_size_q: int,
347
+ ) -> torch.Tensor:
348
+ """O' = R_n * O_spa + (A_pool * rectify_mask) @ V_cmp, autograd-tracked.
349
+
350
+ Memory-contiguous via expand+contiguous+reshape: each broadcast tensor is
351
+ materialised exactly once, never produces a non-contiguous view that the
352
+ next op would silently re-materialise.
353
+ """
354
+ B, H, S, D = o_spa.shape
355
+ N_q = R_n.shape[-1]
356
+
357
+ # Broadcast R_n [B, H, N_q] → [B, H, S, 1]: one materialisation, contiguous.
358
+ R_tok = (
359
+ R_n.unsqueeze(-1).unsqueeze(-1)
360
+ .expand(B, H, N_q, chunk_size_q, 1)
361
+ .contiguous()
362
+ .reshape(B, H, S, 1)
363
+ )
364
+ o_cri = R_tok * o_spa
365
+
366
+ # Non-critical contribution: weighted sum of pooled V over rectifiable blocks.
367
+ A_rect = A_pool * rectify_mask.to(A_pool.dtype)
368
+ o_ncri_block = torch.matmul(A_rect, v_cmp) # [B, H, N_q, D]
369
+
370
+ # Broadcast block-level output to per-token, again contiguous.
371
+ o_ncri = (
372
+ o_ncri_block.unsqueeze(3)
373
+ .expand(B, H, N_q, chunk_size_q, D)
374
+ .contiguous()
375
+ .reshape(B, H, S, D)
376
+ )
377
+ return (o_cri + o_ncri).to(o_spa.dtype).contiguous()
378
+
379
+
380
+ def bsa_rectified_sparse_attn(
381
+ q: torch.Tensor,
382
+ k: torch.Tensor,
383
+ v: torch.Tensor,
384
+ chunk_size_q: int,
385
+ chunk_size_k: int,
386
+ sparsity: Optional[float],
387
+ cdf_threshold: Optional[float],
388
+ sm_scale: float,
389
+ *,
390
+ kv_valid_mask: Optional[torch.Tensor] = None,
391
+ q_valid_mask: Optional[torch.Tensor] = None,
392
+ score_modifier: Optional[torch.Tensor] = None,
393
+ use_triton_inference: bool = True,
394
+ ) -> torch.Tensor:
395
+ # --- (a) pooling (autograd-tracked) ---
396
+ q_cmp, k_cmp = _pool_qk(q, k, chunk_size_q, chunk_size_k, kv_valid_mask, q_valid_mask)
397
+
398
+ # --- (b) raw scoring (autograd-tracked) ---
399
+ raw_score = cal_score(q_cmp, k_cmp)
400
+
401
+ # --- (c) selection (no grad) ---
402
+ with torch.no_grad():
403
+ sel_score = raw_score * score_modifier if score_modifier is not None else raw_score
404
+ block_indices, block_indices_lens = _select_blocks(sel_score, sparsity, cdf_threshold, sm_scale)
405
+
406
+ # --- (d) BSA Triton (autograd via _bsa_kernel_only) ---
407
+ o_spa, _ = _bsa_kernel(
408
+ q, k, v, sm_scale, block_indices, block_indices_lens,
409
+ chunk_size_q, chunk_size_k, sparsity, kv_valid_mask,
410
+ )
411
+
412
+ # --- (e) sel_mask / rectify_mask (no grad; bool tensors, contiguous) ---
413
+ block_valid = _compute_block_valid_mask(kv_valid_mask, chunk_size_k) if kv_valid_mask is not None else None
414
+ with torch.no_grad():
415
+ sel_mask = create_mask_from_indices_varlen(block_indices, raw_score.shape[-1])
416
+ rectify_mask = ~sel_mask # all non-selected blocks
417
+ if block_valid is not None:
418
+ rectify_mask = rectify_mask & block_valid.view(1, 1, 1, -1)
419
+ rectify_mask = rectify_mask.contiguous()
420
+
421
+ # --- (f) A_pool & R_n (autograd-tracked: ∂L/∂q,∂L/∂k flow through these) ---
422
+ A_pool = _masked_softmax_along_last(raw_score, sm_scale, block_valid)
423
+ R_n = (1.0 - (A_pool * rectify_mask.to(A_pool.dtype)).sum(dim=-1)).clamp(min=1e-6)
424
+
425
+ # --- (g) v_cmp (autograd-tracked: ∂L/∂v flows through pooling) ---
426
+ v_cmp = _pool_v(v, chunk_size_k, kv_valid_mask)
427
+
428
+ # --- (h) correction (fused Triton kernel for inference, PyTorch for training) ---
429
+ use_triton = (
430
+ use_triton_inference
431
+ and not torch.is_grad_enabled()
432
+ and q.is_cuda
433
+ )
434
+ if use_triton:
435
+ try:
436
+ from .bias_rectification_triton import rectified_correction_triton
437
+ return rectified_correction_triton(
438
+ o_spa, R_n.detach(), A_pool.detach(), rectify_mask, v_cmp.detach(),
439
+ chunk_size_q,
440
+ )
441
+ except Exception as e:
442
+ print(f"[bias_rectification] rectified_correction_triton fallback: {e}")
443
+
444
+ return _rectified_correction_pytorch(
445
+ o_spa, R_n, A_pool, rectify_mask, v_cmp, chunk_size_q,
446
+ )
447
+
448
+
449
+ __all__ = [
450
+ "bsa_taylor_sparse_attn",
451
+ "bsa_rectified_sparse_attn",
452
+ ]
hymm/models/modules/block_sparse_attention/bias_rectification_triton.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import triton
3
+ import triton.language as tl
4
+
5
+
6
+ # =====================================================================
7
+ # Rectified-SpaAttn: fused (R_n * o_spa + (A_pool * rectify_mask) @ v_cmp)
8
+ # =====================================================================
9
+ @triton.jit
10
+ def _rectified_correction_fwd_kernel(
11
+ O_spa, R_n, A_pool, rectify_mask, V_cmp, Out,
12
+ stride_oz, stride_oh, stride_om, stride_od,
13
+ stride_rz, stride_rh, stride_rn, # R_n: [B, H, N_q]
14
+ stride_az, stride_ah, stride_an, stride_ak, # A_pool: [B, H, N_q, N_k]
15
+ stride_mz, stride_mh, stride_mn, stride_mk, # rectify_mask: [B, H, N_q, N_k]
16
+ stride_vz, stride_vh, stride_vk, stride_vd, # V_cmp: [B, H, N_k, D]
17
+ stride_cz, stride_ch, stride_cm, stride_cd, # Out: [B, H, S_q, D]
18
+ H, N_q, N_k,
19
+ CHUNK_SIZE_Q: tl.constexpr, # tokens per Q block
20
+ BLOCK_M: tl.constexpr, # tokens per program (<= CHUNK_SIZE_Q)
21
+ BLOCK_N: tl.constexpr, # K blocks per inner step
22
+ HEAD_DIM: tl.constexpr,
23
+ ):
24
+ """Compute Out[b, h, s, :] = R_n[b, h, n_q(s)] * O_spa[b, h, s, :]
25
+ + sum_{j} A_pool[b, h, n_q(s), j]
26
+ * rectify_mask[b, h, n_q(s), j]
27
+ * V_cmp[b, h, j, :]
28
+ where n_q(s) = s // CHUNK_SIZE_Q.
29
+
30
+ Each program handles BLOCK_M consecutive query tokens (within one Q block,
31
+ BLOCK_M <= CHUNK_SIZE_Q so every token in the program shares the same n_q).
32
+ """
33
+ pid_m = tl.program_id(0)
34
+ pid_zh = tl.program_id(1)
35
+ off_z = pid_zh // H
36
+ off_h = pid_zh % H
37
+
38
+ # All BLOCK_M rows belong to the same Q block (BLOCK_M divides CHUNK_SIZE_Q).
39
+ n_q = (pid_m * BLOCK_M) // CHUNK_SIZE_Q
40
+
41
+ offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
42
+ offs_d = tl.arange(0, HEAD_DIM)
43
+
44
+ # -------- Load R_n and the BSA output --------
45
+ r_ptr = R_n + off_z * stride_rz + off_h * stride_rh + n_q * stride_rn
46
+ r = tl.load(r_ptr).to(tl.float32)
47
+
48
+ o_spa_ptrs = (
49
+ O_spa
50
+ + off_z * stride_oz + off_h * stride_oh
51
+ + offs_m[:, None] * stride_om + offs_d[None, :] * stride_od
52
+ )
53
+ out_ptrs = (
54
+ Out
55
+ + off_z * stride_cz + off_h * stride_ch
56
+ + offs_m[:, None] * stride_cm + offs_d[None, :] * stride_cd
57
+ )
58
+
59
+ s_q_total = pid_m * BLOCK_M + BLOCK_M # valid rows are those < S_q_total in caller
60
+ # We don't know S_q here; rely on caller to guarantee S_q is a multiple of CHUNK_SIZE_Q
61
+ # via padding. But we still do a row mask to be safe for the last program.
62
+ row_mask = offs_m[:, None] < (n_q + 1) * CHUNK_SIZE_Q
63
+
64
+ o_spa = tl.load(o_spa_ptrs, mask=row_mask, other=0.0).to(tl.float32)
65
+ acc = r * o_spa # broadcast scalar * [BLOCK_M, HEAD_DIM]
66
+
67
+ # -------- Accumulate non-critical contribution: sum_j A * mask * V --------
68
+ a_base = (
69
+ A_pool
70
+ + off_z * stride_az + off_h * stride_ah + n_q * stride_an
71
+ )
72
+ m_base = (
73
+ rectify_mask
74
+ + off_z * stride_mz + off_h * stride_mh + n_q * stride_mn
75
+ )
76
+ v_base = V_cmp + off_z * stride_vz + off_h * stride_vh
77
+
78
+ for j_start in range(0, N_k, BLOCK_N):
79
+ offs_n = j_start + tl.arange(0, BLOCK_N)
80
+ n_mask = offs_n < N_k
81
+
82
+ a = tl.load(a_base + offs_n * stride_ak, mask=n_mask, other=0.0).to(tl.float32)
83
+ m = tl.load(m_base + offs_n * stride_mk, mask=n_mask, other=0).to(tl.float32)
84
+ coef = a * m # [BLOCK_N], zero where mask is False
85
+
86
+ v_ptrs = v_base + offs_n[:, None] * stride_vk + offs_d[None, :] * stride_vd
87
+ v = tl.load(v_ptrs, mask=n_mask[:, None], other=0.0).to(tl.float32)
88
+
89
+ # acc += coef[:, None] * v summed over BLOCK_N (one Q row uses all K blocks)
90
+ # Contribution is the same for all Q tokens in this block, so we add a row-wise
91
+ # contribution: acc[i, :] += sum_j coef[j] * v[j, :]
92
+ contrib = tl.sum(coef[:, None] * v, axis=0) # [HEAD_DIM]
93
+ acc += contrib[None, :]
94
+
95
+ tl.store(out_ptrs, acc.to(Out.type.element_ty), mask=row_mask)
96
+
97
+
98
+ def rectified_correction_triton(
99
+ o_spa: torch.Tensor, # [B, H, S_q, D]
100
+ R_n: torch.Tensor, # [B, H, N_q]
101
+ A_pool: torch.Tensor, # [B, H, N_q, N_k] (probabilities, fp32)
102
+ rectify_mask: torch.Tensor, # [B, H, N_q, N_k] (bool)
103
+ v_cmp: torch.Tensor, # [B, H, N_k, D]
104
+ chunk_size_q: int,
105
+ ) -> torch.Tensor:
106
+ """Fused Rectified-SpaAttn correction without materialising the
107
+ [B, H, S_q, D] o_ncri intermediate.
108
+
109
+ Output: out = R_n_token_level * o_spa + (A_pool * rectify_mask) @ v_cmp,
110
+ where R_n is broadcast from N_q to S_q via the chunk index.
111
+ """
112
+ B, H, S_q, D = o_spa.shape
113
+ N_q = R_n.shape[-1]
114
+ N_k = A_pool.shape[-1]
115
+ assert S_q == N_q * chunk_size_q, f"S_q={S_q} must equal N_q*chunk_size_q={N_q*chunk_size_q}"
116
+
117
+ # Choose BLOCK_M = chunk_size_q for chunk_size_q <= 64; else use 64.
118
+ if chunk_size_q <= 64:
119
+ BLOCK_M = chunk_size_q
120
+ else:
121
+ # When chunk_size_q is large, split each Q block into multiple programs.
122
+ BLOCK_M = 64
123
+ assert chunk_size_q % BLOCK_M == 0, (
124
+ f"chunk_size_q={chunk_size_q} must be divisible by BLOCK_M={BLOCK_M}")
125
+
126
+ BLOCK_N = 64 if N_k >= 64 else max(1, triton.next_power_of_2(N_k))
127
+
128
+ o_spa = o_spa.contiguous()
129
+ A_pool = A_pool.contiguous()
130
+ rectify_mask = rectify_mask.contiguous()
131
+ v_cmp = v_cmp.contiguous()
132
+ R_n = R_n.contiguous()
133
+ out = torch.empty_like(o_spa)
134
+
135
+ grid = (triton.cdiv(S_q, BLOCK_M), B * H, 1)
136
+
137
+ _rectified_correction_fwd_kernel[grid](
138
+ o_spa, R_n, A_pool, rectify_mask, v_cmp, out,
139
+ o_spa.stride(0), o_spa.stride(1), o_spa.stride(2), o_spa.stride(3),
140
+ R_n.stride(0), R_n.stride(1), R_n.stride(2),
141
+ A_pool.stride(0), A_pool.stride(1), A_pool.stride(2), A_pool.stride(3),
142
+ rectify_mask.stride(0), rectify_mask.stride(1), rectify_mask.stride(2), rectify_mask.stride(3),
143
+ v_cmp.stride(0), v_cmp.stride(1), v_cmp.stride(2), v_cmp.stride(3),
144
+ out.stride(0), out.stride(1), out.stride(2), out.stride(3),
145
+ H, N_q, N_k,
146
+ CHUNK_SIZE_Q=chunk_size_q,
147
+ BLOCK_M=BLOCK_M,
148
+ BLOCK_N=BLOCK_N,
149
+ HEAD_DIM=D,
150
+ num_warps=4,
151
+ num_stages=2,
152
+ )
153
+ return out
154
+
155
+
156
+ @triton.jit
157
+ def _taylor_merge_fwd_kernel(
158
+ Q, K_cmp, V_cmp,
159
+ O_spa, LSE_spa,
160
+ Out,
161
+ block_token_counts, # [N_k] (fp32, log already taken outside)
162
+ nonsel_mask, # [B, H, N_q, N_k] bool
163
+ is_flat, # [B, H, N_q] bool
164
+ sm_scale_log2, # scalar, in log2
165
+ sm_scale, # scalar, natural
166
+ stride_qz, stride_qh, stride_qm, stride_qd,
167
+ stride_kz, stride_kh, stride_kk, stride_kd,
168
+ stride_vz, stride_vh, stride_vk, stride_vd,
169
+ stride_oz, stride_oh, stride_om, stride_od,
170
+ stride_lz, stride_lh, stride_lm, # LSE_spa [B, H, S_q]
171
+ stride_cz, stride_ch, stride_cm, stride_cd, # Out
172
+ stride_btc, # block_token_counts [N_k]
173
+ stride_mz, stride_mh, stride_mn, stride_mk,
174
+ stride_fz, stride_fh, stride_fn,
175
+ H, N_q, N_k,
176
+ CHUNK_SIZE_Q: tl.constexpr,
177
+ BLOCK_M: tl.constexpr,
178
+ BLOCK_N: tl.constexpr,
179
+ HEAD_DIM: tl.constexpr,
180
+ ):
181
+ """For each query token i in a Q block n_q:
182
+ z_taylor[j] = q_i · K_cmp[j] * sm_scale + log_count[j] (only for j in nonsel)
183
+ lse_taylor = logsumexp_j z_taylor (over nonsel j)
184
+ o_taylor = sum_j softmax(z_taylor)[j] * V_cmp[j]
185
+ Then if is_flat[n_q]:
186
+ w_spa = exp(lse_spa - lse_total)
187
+ w_taylor = exp(lse_taylor - lse_total)
188
+ out = w_spa * o_spa + w_taylor * o_taylor
189
+ else:
190
+ out = o_spa
191
+ """
192
+ pid_m = tl.program_id(0)
193
+ pid_zh = tl.program_id(1)
194
+ off_z = pid_zh // H
195
+ off_h = pid_zh % H
196
+
197
+ n_q = (pid_m * BLOCK_M) // CHUNK_SIZE_Q
198
+ offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
199
+ offs_d = tl.arange(0, HEAD_DIM)
200
+
201
+ flat_ptr = is_flat + off_z * stride_fz + off_h * stride_fh + n_q * stride_fn
202
+ flat_val = tl.load(flat_ptr).to(tl.int32)
203
+
204
+ # Load Q rows for this program
205
+ q_ptrs = (
206
+ Q + off_z * stride_qz + off_h * stride_qh
207
+ + offs_m[:, None] * stride_qm + offs_d[None, :] * stride_qd
208
+ )
209
+ q = tl.load(q_ptrs).to(tl.float32)
210
+
211
+ # Always read o_spa
212
+ o_spa_ptrs = (
213
+ O_spa + off_z * stride_oz + off_h * stride_oh
214
+ + offs_m[:, None] * stride_om + offs_d[None, :] * stride_od
215
+ )
216
+ o_spa = tl.load(o_spa_ptrs).to(tl.float32)
217
+
218
+ out_ptrs = (
219
+ Out + off_z * stride_cz + off_h * stride_ch
220
+ + offs_m[:, None] * stride_cm + offs_d[None, :] * stride_cd
221
+ )
222
+
223
+ if flat_val == 0:
224
+ # Sharp queries: keep o_spa unchanged.
225
+ tl.store(out_ptrs, o_spa.to(Out.type.element_ty))
226
+ return
227
+
228
+ # --- Streaming softmax over nonsel K blocks for Taylor ---
229
+ m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf")
230
+ l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
231
+ acc = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32)
232
+
233
+ mask_base = nonsel_mask + off_z * stride_mz + off_h * stride_mh + n_q * stride_mn
234
+ k_base = K_cmp + off_z * stride_kz + off_h * stride_kh
235
+ v_base = V_cmp + off_z * stride_vz + off_h * stride_vh
236
+
237
+ for j_start in range(0, N_k, BLOCK_N):
238
+ offs_n = j_start + tl.arange(0, BLOCK_N)
239
+ n_mask = offs_n < N_k
240
+
241
+ # Load K_cmp [BLOCK_N, HEAD_DIM]
242
+ k_ptrs = k_base + offs_n[:, None] * stride_kk + offs_d[None, :] * stride_kd
243
+ k_cmp_b = tl.load(k_ptrs, mask=n_mask[:, None], other=0.0).to(tl.float32)
244
+
245
+ # Score q @ k_cmp^T : [BLOCK_M, BLOCK_N]
246
+ qk = tl.dot(q, tl.trans(k_cmp_b)) * sm_scale
247
+
248
+ # Add log(block_token_count[j]) to make Taylor weights commensurate.
249
+ log_cnt = tl.load(block_token_counts + offs_n * stride_btc, mask=n_mask, other=0.0)
250
+ qk = qk + log_cnt[None, :]
251
+
252
+ # Mask out non-eligible blocks (selected, padded, etc.)
253
+ nm = tl.load(mask_base + offs_n * stride_mk, mask=n_mask, other=0).to(tl.int32)
254
+ valid = (nm != 0) & n_mask
255
+ qk = tl.where(valid[None, :], qk, float("-inf"))
256
+
257
+ # Streaming online softmax update
258
+ m_ij = tl.maximum(m_i, tl.max(qk, axis=1))
259
+ alpha = tl.math.exp2((m_i - m_ij) * 1.4426950408889634)
260
+ # exp in natural log: convert qk (already natural) to natural exp
261
+ # We want p = exp(qk - m_ij). Use tl.math.exp.
262
+ p = tl.math.exp(qk - m_ij[:, None])
263
+ p = tl.where(valid[None, :], p, 0.0)
264
+
265
+ # Load V_cmp [BLOCK_N, HEAD_DIM]
266
+ v_ptrs = v_base + offs_n[:, None] * stride_vk + offs_d[None, :] * stride_vd
267
+ v_cmp_b = tl.load(v_ptrs, mask=n_mask[:, None], other=0.0).to(tl.float32)
268
+
269
+ l_i = l_i * alpha + tl.sum(p, axis=1)
270
+ acc = acc * alpha[:, None] + tl.dot(p, v_cmp_b)
271
+ m_i = m_ij
272
+
273
+ # lse_taylor = m_i + log(l_i); guard l_i = 0 (no nonsel blocks → o_taylor = 0, lse = -inf)
274
+ l_safe = tl.where(l_i > 0.0, l_i, 1.0)
275
+ lse_taylor = tl.where(l_i > 0.0, m_i + tl.log(l_safe), tl.full([BLOCK_M], -float("inf"), tl.float32))
276
+ o_taylor = tl.where(l_i[:, None] > 0.0, acc / l_safe[:, None], tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32))
277
+
278
+ # Load lse_spa for these tokens
279
+ lse_spa_ptrs = LSE_spa + off_z * stride_lz + off_h * stride_lh + offs_m * stride_lm
280
+ lse_spa = tl.load(lse_spa_ptrs)
281
+
282
+ # Stable lse merge: lse_total = max + log(exp(lse_a - max) + exp(lse_b - max))
283
+ lse_max = tl.maximum(lse_spa, lse_taylor)
284
+ finite = (lse_max > -float("inf"))
285
+ a_ext = tl.where(finite, lse_spa - lse_max, 0.0)
286
+ b_ext = tl.where(finite, lse_taylor - lse_max, 0.0)
287
+ lse_total = tl.where(finite, lse_max + tl.log(tl.math.exp(a_ext) + tl.math.exp(b_ext)), lse_max)
288
+
289
+ w_spa = tl.where(finite, tl.math.exp(lse_spa - lse_total), 1.0)
290
+ w_taylor = tl.where(finite, tl.math.exp(lse_taylor - lse_total), 0.0)
291
+
292
+ out = w_spa[:, None] * o_spa + w_taylor[:, None] * o_taylor
293
+ tl.store(out_ptrs, out.to(Out.type.element_ty))
294
+
295
+
296
+ def taylor_merge_triton(
297
+ q: torch.Tensor, # [B, H, S_q, D] float (bf16/fp16/fp32)
298
+ k_cmp: torch.Tensor, # [B, H, N_k, D]
299
+ v_cmp: torch.Tensor, # [B, H, N_k, D]
300
+ o_spa: torch.Tensor, # [B, H, S_q, D]
301
+ lse_spa: torch.Tensor, # [B, H, S_q] natural-log LSE from BSA
302
+ nonsel_mask: torch.Tensor, # [B, H, N_q, N_k] bool
303
+ is_flat: torch.Tensor, # [B, H, N_q] bool
304
+ block_token_counts: torch.Tensor, # [N_k] fp32 — number of valid tokens per K block
305
+ chunk_size_q: int,
306
+ sm_scale: float,
307
+ ) -> torch.Tensor:
308
+ """Fused Taylor merge.
309
+
310
+ Materialises NONE of the [B, H, S_q, N_k] taylor_scores tensor.
311
+ Sharp queries (is_flat=False) skip the streaming softmax entirely.
312
+ """
313
+ B, H, S_q, D = o_spa.shape
314
+ N_q = nonsel_mask.shape[-2]
315
+ N_k = nonsel_mask.shape[-1]
316
+ assert S_q == N_q * chunk_size_q
317
+
318
+ if chunk_size_q <= 64:
319
+ BLOCK_M = chunk_size_q
320
+ else:
321
+ BLOCK_M = 64
322
+ assert chunk_size_q % BLOCK_M == 0
323
+
324
+ BLOCK_N = 64 if N_k >= 64 else max(1, triton.next_power_of_2(N_k))
325
+
326
+ # Take log of token counts once; pad blocks (count=0) → log(1) = 0, never reached
327
+ # because they're already excluded via nonsel_mask & block_valid in the caller.
328
+ log_counts = torch.log(block_token_counts.float().clamp(min=1.0)).contiguous()
329
+
330
+ q = q.contiguous()
331
+ k_cmp = k_cmp.contiguous()
332
+ v_cmp = v_cmp.contiguous()
333
+ o_spa = o_spa.contiguous()
334
+ lse_spa = lse_spa.contiguous()
335
+ nonsel_mask = nonsel_mask.contiguous()
336
+ is_flat = is_flat.contiguous()
337
+ out = torch.empty_like(o_spa)
338
+
339
+ grid = (triton.cdiv(S_q, BLOCK_M), B * H, 1)
340
+
341
+ _taylor_merge_fwd_kernel[grid](
342
+ q, k_cmp, v_cmp,
343
+ o_spa, lse_spa, out,
344
+ log_counts, nonsel_mask, is_flat,
345
+ sm_scale * 1.4426950408889634, sm_scale,
346
+ q.stride(0), q.stride(1), q.stride(2), q.stride(3),
347
+ k_cmp.stride(0), k_cmp.stride(1), k_cmp.stride(2), k_cmp.stride(3),
348
+ v_cmp.stride(0), v_cmp.stride(1), v_cmp.stride(2), v_cmp.stride(3),
349
+ o_spa.stride(0), o_spa.stride(1), o_spa.stride(2), o_spa.stride(3),
350
+ lse_spa.stride(0), lse_spa.stride(1), lse_spa.stride(2),
351
+ out.stride(0), out.stride(1), out.stride(2), out.stride(3),
352
+ log_counts.stride(0),
353
+ nonsel_mask.stride(0), nonsel_mask.stride(1), nonsel_mask.stride(2), nonsel_mask.stride(3),
354
+ is_flat.stride(0), is_flat.stride(1), is_flat.stride(2),
355
+ H, N_q, N_k,
356
+ CHUNK_SIZE_Q=chunk_size_q,
357
+ BLOCK_M=BLOCK_M,
358
+ BLOCK_N=BLOCK_N,
359
+ HEAD_DIM=D,
360
+ num_warps=4,
361
+ num_stages=2,
362
+ )
363
+ return out
hymm/models/modules/block_sparse_attention/bsa_interface.py ADDED
@@ -0,0 +1,1503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn.functional as F
4
+ import triton
5
+ import triton.language as tl
6
+ import math
7
+
8
+ from .common import _attn_fwd_gating, _attn_bwd_preprocess, configs_gating_preset
9
+ from .flash_attn_bsa_varlen_mask import (
10
+ _attn_fwd_bsa_varlen, _attn_fwd_bsa_varlen_align, _attn_bwd_dkdv_bsa_varlen_wrapper, _attn_bwd_dq_bsa_varlen_wrapper, _attn_bwd_dq_bsa_varlen_align_wrapper,
11
+ configs_fwd_bsa_varlen_preset, configs_fwd_bsa_varlen_align_preset, configs_bwd_dkdv_bsa_varlen_preset, configs_bwd_dq_bsa_varlen_preset, configs_bwd_dq_bsa_varlen_align_preset
12
+ )
13
+
14
+ torch._dynamo.config.cache_size_limit = 32
15
+
16
+ def is_cuda():
17
+ return triton.runtime.driver.active.get_current_target().backend == "cuda"
18
+
19
+ def supports_tma():
20
+ return is_cuda() and torch.cuda.get_device_capability()[0] >= 9
21
+
22
+ HAS_TMA_DESC = "nv_tma_desc_type" in dir(tl)
23
+
24
+ if HAS_TMA_DESC:
25
+ print("TMA benchmarks will be running with experimental grid constant TMA descriptor.", )
26
+ else:
27
+ print("TMA benchmarks will be running without grid constant TMA descriptor.", )
28
+
29
+
30
+ # TmaAutoTuneHelper used in htyu's PR #5622
31
+ class TmaAutoTuneHelper:
32
+
33
+ # duck typing wrapper to implement the same interface as TmaDescKernelParam in Triton PR #4498
34
+ class KernelParamWrapper:
35
+
36
+ def __init__(self, desc):
37
+ self.desc = desc
38
+
39
+ def tma_desc_cpu_ptr(self):
40
+ return self.desc.data_ptr()
41
+
42
+ TMA_SIZE = 128
43
+
44
+ def __init__(self):
45
+ self.fill_1d_tma_descriptor_inner = (triton.runtime.driver.active.utils.fill_1d_tma_descriptor)
46
+ self.fill_2d_tma_descriptor_inner = (triton.runtime.driver.active.utils.fill_2d_tma_descriptor)
47
+ if HAS_TMA_DESC:
48
+ self.descriptors = {}
49
+ else:
50
+ self.cuda_descriptors = {}
51
+
52
+ # Call this method outside of the lambda function for grid size
53
+ def init_tma_descriptor(self, name):
54
+ if HAS_TMA_DESC:
55
+ self.descriptors[name] = torch.empty(TmaAutoTuneHelper.TMA_SIZE, device="cpu", dtype=torch.int8)
56
+ else:
57
+ self.cuda_descriptors[name] = torch.empty(TmaAutoTuneHelper.TMA_SIZE, device="cuda", dtype=torch.int8)
58
+
59
+ # Call this method inside the lambda function for grid size
60
+ def fill_1d_tma_descriptor(self, name, ptr, dim, block_dim, element_size):
61
+ if HAS_TMA_DESC:
62
+ desc_x = self.descriptors[name]
63
+ assert desc_x.data_ptr() % 64 == 0
64
+ self.fill_1d_tma_descriptor_inner(ptr, dim, block_dim, element_size, desc_x.data_ptr())
65
+ else:
66
+ desc_x = self.cuda_descriptors[name]
67
+ buf_x = torch.empty_like(desc_x, device="cpu", pin_memory=True)
68
+ self.fill_1d_tma_descriptor_inner(ptr, dim, block_dim, element_size, buf_x.data_ptr())
69
+ desc_x.copy_(buf_x, non_blocking=True)
70
+
71
+ # Call this method inside the lambda function for grid size
72
+ def fill_2d_tma_descriptor(self, name, ptr, dim1, dim0, block_dim1, block_dim0, element_size):
73
+ if HAS_TMA_DESC:
74
+ desc_x = self.descriptors[name]
75
+ assert desc_x.data_ptr() % 64 == 0
76
+ self.fill_2d_tma_descriptor_inner(ptr, dim1, dim0, block_dim1, block_dim0, element_size, desc_x.data_ptr())
77
+ else:
78
+ desc_x = self.cuda_descriptors[name]
79
+ buf_x = torch.empty_like(desc_x, device="cpu", pin_memory=True)
80
+ self.fill_2d_tma_descriptor_inner(ptr, dim1, dim0, block_dim1, block_dim0, element_size, buf_x.data_ptr())
81
+ desc_x.copy_(buf_x, non_blocking=True)
82
+
83
+ def get_tma_descriptor_kernel_param(self, name):
84
+ if HAS_TMA_DESC:
85
+ assert self.descriptors[name] is not None
86
+ return self.KernelParamWrapper(self.descriptors[name])
87
+ else:
88
+ assert self.cuda_descriptors[name] is not None
89
+ return self.cuda_descriptors[name]
90
+
91
+
92
+ @triton.jit
93
+ def create_mask_from_indices_kernel(
94
+ block_indices,
95
+ block_mask,
96
+ stride_bz, stride_bh, stride_bm, stride_bs,
97
+ stride_mz, stride_mh, stride_mm, stride_mn,
98
+ H,
99
+ ):
100
+ i_zh, i_m, i_s = tl.program_id(0), tl.program_id(1), tl.program_id(2)
101
+ i_z, i_h = i_zh // H, i_zh % H
102
+
103
+ off_b = i_z.to(tl.int64) * stride_bz + i_h.to(tl.int64) * stride_bh + i_m.to(tl.int64) * stride_bm + i_s.to(tl.int64) * stride_bs
104
+
105
+ b_i = tl.load(block_indices + off_b)
106
+
107
+ off_m = i_z.to(tl.int64) * stride_mz + i_h.to(tl.int64) * stride_mh + i_m.to(tl.int64) * stride_mm + b_i.to(tl.int64) * stride_mn
108
+
109
+ b_m = 1
110
+ tl.store(block_mask + off_m, b_m.to(block_mask.dtype.element_ty))
111
+
112
+ def create_mask_from_indices_triton(
113
+ block_indices,
114
+ N_cols
115
+ ):
116
+ B, H, N_rows, S = block_indices.shape
117
+ block_mask = torch.zeros((B, H, N_rows, N_cols), dtype=torch.bool, device=block_indices.device)
118
+ create_mask_from_indices_kernel[(B * H, N_rows, S)](
119
+ block_indices,
120
+ block_mask,
121
+ block_indices.stride(0), block_indices.stride(1), block_indices.stride(2), block_indices.stride(3),
122
+ block_mask.stride(0), block_mask.stride(1), block_mask.stride(2), block_mask.stride(3),
123
+ H,
124
+ )
125
+ return block_mask
126
+
127
+ @torch.compile
128
+ def create_mask_from_indices_varlen(block_indices, N_cols_mask):
129
+
130
+ B, H, M, _ = block_indices.shape
131
+ device = block_indices.device
132
+
133
+ mask = torch.zeros((B, H, M, N_cols_mask), dtype=torch.bool, device=device)
134
+
135
+ valid = block_indices < N_cols_mask
136
+
137
+ b_idx = torch.arange(B, device=device)[:, None, None, None].expand_as(block_indices)
138
+ h_idx = torch.arange(H, device=device)[None, :, None, None].expand_as(block_indices)
139
+ m_idx = torch.arange(M, device=device)[None, None, :, None].expand_as(block_indices)
140
+
141
+ valid_coords = (b_idx[valid], h_idx[valid], m_idx[valid], block_indices[valid])
142
+
143
+ mask[valid_coords] = True
144
+
145
+ return mask
146
+
147
+ @torch.compile
148
+ def create_indices_k_from_indices_q_varlen(
149
+ block_indices,
150
+ N_cols_mask # indicate the number of the last dimension of the bool mask, since this information cannot be determined by block_indices, which may contain invalid elements
151
+ ):
152
+ block_mask_qk = create_mask_from_indices_varlen(block_indices, N_cols_mask)
153
+ B, H, M, N = block_mask_qk.shape
154
+ block_mask_kq = block_mask_qk.permute(0, 1, 3, 2)
155
+ indices = torch.arange(M, device=block_indices.device).view(1, 1, 1, -1).expand_as(block_mask_kq)
156
+ block_indices_k = torch.where(block_mask_kq, indices, M)
157
+ block_indices_k, _ = torch.sort(block_indices_k, dim=-1)
158
+
159
+ block_indices_k_lens = (block_indices_k < M).sum(dim=-1)
160
+
161
+ return block_indices_k, block_indices_k_lens
162
+
163
+
164
+ @torch.compile
165
+ def mean_pooling_compression(
166
+ x: torch.Tensor,
167
+ block_size: int
168
+ ) -> torch.Tensor:
169
+ B, H, S = x.shape[:3]
170
+ num_block = math.ceil(S / block_size)
171
+ if S % block_size != 0:
172
+ x = F.pad(x, (0, 0, 0, num_block * block_size - S))
173
+ x_cmp = x.view(B, H, num_block, block_size, -1).mean(dim=3)
174
+ return x_cmp
175
+
176
+
177
+ @torch.compile
178
+ def masked_mean_pooling_compression(
179
+ x: torch.Tensor,
180
+ block_size: int,
181
+ valid_mask: torch.Tensor,
182
+ ) -> torch.Tensor:
183
+ """Mean pooling that ignores padded positions marked False in valid_mask.
184
+
185
+ Handles both cases:
186
+ - S divisible by block_size (after 3D padding + block rearrangement)
187
+ - S not divisible by block_size (pads the tail and extends valid_mask)
188
+ """
189
+ B, H, S, D = x.shape
190
+ num_block = math.ceil(S / block_size)
191
+ if S % block_size != 0:
192
+ pad_len = num_block * block_size - S
193
+ x = F.pad(x, (0, 0, 0, pad_len))
194
+ valid_mask = F.pad(valid_mask, (0, pad_len), value=False)
195
+
196
+ x_blocks = x.view(B, H, num_block, block_size, D)
197
+ mask_blocks = valid_mask.view(num_block, block_size)
198
+ mask_f = mask_blocks.float().unsqueeze(0).unsqueeze(0).unsqueeze(-1)
199
+ counts = mask_f.sum(dim=3, keepdim=True).clamp(min=1.0)
200
+ x_cmp = (x_blocks * mask_f).sum(dim=3) / counts.squeeze(3)
201
+ return x_cmp
202
+
203
+ @torch.compile
204
+ def cal_score(q, k):
205
+ k_transposed = k.transpose(-1, -2) # [b, h, d, s_k]
206
+ score = torch.matmul(q, k_transposed) # [b, h, s_q, s_k]
207
+ return score
208
+
209
+ def cal_score_triton(q, k):
210
+ B, H, s_q, D = q.shape
211
+ s_k = k.shape[2]
212
+
213
+ score = torch.empty(B, H, s_q, s_k, device=q.device, dtype=q.dtype)
214
+
215
+ kernel_config = {} if os.environ.get('TRITON_AUTOTUNE_ENBALE', '0') == '1' else configs_gating_preset['default']
216
+
217
+ grid = lambda args: (triton.cdiv(s_q, args["BLOCK_M"]), B * H, 1)
218
+ _attn_fwd_gating[grid](
219
+ q, k, score,
220
+ q.stride(0), q.stride(1), q.stride(2), q.stride(3),
221
+ k.stride(0), k.stride(1), k.stride(2), k.stride(3),
222
+ score.stride(0), score.stride(1), score.stride(2), score.stride(3),
223
+ H, s_q, s_k,
224
+ HEAD_DIM=D,
225
+ **kernel_config
226
+ )
227
+ return score
228
+
229
+ @torch.compile
230
+ def get_select_indices_topk(q, k, sparsity):
231
+ score = cal_score(q, k)
232
+ block_indices, block_indices_lens = get_select_indices_topk_from_score(score, sparsity)
233
+ return block_indices, block_indices_lens
234
+
235
+ @torch.compile
236
+ def get_select_indices_topk_from_score(score, sparsity):
237
+ num_selected = int((1 - sparsity) * score.shape[-1])
238
+ block_indices = torch.topk(score, num_selected)[1]
239
+ block_indices, _ = torch.sort(block_indices, dim=-1)
240
+
241
+ block_indices_lens = torch.full(
242
+ (block_indices.shape[0], block_indices.shape[1], block_indices.shape[2]),
243
+ num_selected,
244
+ dtype=torch.int32,
245
+ device=block_indices.device
246
+ )
247
+
248
+ return block_indices, block_indices_lens
249
+
250
+ @torch.compile
251
+ def get_select_indices_cdf(q, k, cdf_threshold):
252
+ score = cal_score(q, k)
253
+ head_dim = q.shape[-1]
254
+ block_indices, block_indices_lens = get_select_indices_cdf_from_score(score, cdf_threshold, 1 / head_dim**0.5)
255
+ return block_indices, block_indices_lens
256
+
257
+ @torch.compile
258
+ def get_select_indices_cdf_from_score(score, cdf_threshold, sm_scale):
259
+ weights = torch.softmax(score * sm_scale, dim=-1)
260
+
261
+ B, H, Sq, Sk = weights.shape
262
+ upper_bound = min(Sk, int(cdf_threshold * Sk) + 1)
263
+ topk_vals, topk_idx = torch.topk(weights, k=upper_bound, dim=-1, largest=True, sorted=True)
264
+ cdf = torch.cumsum(topk_vals, dim=-1)
265
+ # Standard nucleus (top-p): pick the smallest set whose cumulative prob >= threshold,
266
+ # i.e. INCLUDE the block that crosses the threshold. (cdf < p).sum() counts the prefix
267
+ # blocks still strictly below p; +1 adds the crossing block.
268
+ num_selected = (cdf < cdf_threshold).to(torch.int32).sum(dim=-1, keepdim=True) + 1
269
+ num_selected = num_selected.clamp(min=1, max=Sk)
270
+
271
+ block_indices = topk_idx.contiguous()
272
+ pos = torch.arange(upper_bound, device=block_indices.device)
273
+ block_indices = block_indices.masked_fill(pos >= num_selected, Sk)
274
+ block_indices, _ = torch.sort(block_indices, dim=-1)
275
+ return block_indices, num_selected.squeeze(-1).to(torch.int32)
276
+
277
+ @torch.compile
278
+ def get_select_indices_cdf_topk(q, k, sparsity, cdf_threshold):
279
+ score = cal_score(q, k)
280
+ head_dim = q.shape[-1]
281
+ block_indices, block_indices_lens = get_select_indices_cdf_topk_from_score(score, sparsity, cdf_threshold, 1 / head_dim**0.5)
282
+ return block_indices, block_indices_lens
283
+
284
+ @torch.compile
285
+ def get_select_indices_cdf_topk_from_score(score, sparsity, cdf_threshold, sm_scale):
286
+ weights = torch.softmax(score * sm_scale, dim=-1)
287
+
288
+ B, H, Sq, Sk = weights.shape
289
+ num_selected_topk = max(1, int((1 - sparsity) * Sk))
290
+ upper_bound = min(Sk, max(num_selected_topk, int(cdf_threshold * Sk) + 1))
291
+
292
+ topk_vals, topk_idx = torch.topk(weights, k=upper_bound, dim=-1, largest=True, sorted=True)
293
+ cdf = torch.cumsum(topk_vals, dim=-1)
294
+ # Standard nucleus (top-p): pick the smallest set whose cumulative prob >= threshold,
295
+ # i.e. INCLUDE the block that crosses the threshold. (cdf < p).sum() counts the prefix
296
+ # blocks still strictly below p; +1 adds the crossing block.
297
+ num_selected = (cdf < cdf_threshold).to(torch.int32).sum(dim=-1, keepdim=True) + 1
298
+ num_selected = num_selected.clamp(min=num_selected_topk, max=Sk)
299
+
300
+ block_indices = topk_idx.contiguous()
301
+ pos = torch.arange(upper_bound, device=block_indices.device)
302
+ block_indices = block_indices.masked_fill(pos >= num_selected, Sk)
303
+ block_indices, _ = torch.sort(block_indices, dim=-1)
304
+ return block_indices, num_selected.squeeze(-1).to(torch.int32)
305
+
306
+ def get_select_indices(q, k, sparsity, cdf_threshold):
307
+ if sparsity is not None and cdf_threshold is None:
308
+ block_indices, block_indices_lens = get_select_indices_topk(q, k, sparsity)
309
+ elif sparsity is None and cdf_threshold is not None:
310
+ block_indices, block_indices_lens = get_select_indices_cdf(q, k, cdf_threshold)
311
+ elif sparsity is not None and cdf_threshold is not None:
312
+ block_indices, block_indices_lens = get_select_indices_cdf_topk(q, k, sparsity, cdf_threshold)
313
+ else:
314
+ raise ValueError
315
+ return block_indices, block_indices_lens
316
+
317
+ def get_select_indices_from_score(score, sparsity, cdf_threshold):
318
+ if sparsity is not None and cdf_threshold is None:
319
+ block_indices, block_indices_lens = get_select_indices_topk_from_score(score, sparsity)
320
+ elif sparsity is None and cdf_threshold is not None:
321
+ block_indices, block_indices_lens = get_select_indices_cdf_from_score(score, cdf_threshold)
322
+ elif sparsity is not None and cdf_threshold is not None:
323
+ block_indices, block_indices_lens = get_select_indices_cdf_topk_from_score(score, sparsity, cdf_threshold)
324
+ else:
325
+ raise ValueError
326
+ return block_indices, block_indices_lens
327
+
328
+ def attn_fwd_bsa_varlen_triton(
329
+ q,
330
+ k,
331
+ v,
332
+ sm_scale,
333
+ block_indices,
334
+ block_indices_lens,
335
+ chunk_size_q,
336
+ chunk_size_k,
337
+ sparsity,
338
+ kv_valid_mask=None,
339
+ ):
340
+
341
+ B, H, Seq, D = q.shape
342
+
343
+ o = torch.empty_like(q)
344
+ M = torch.empty((q.shape[0], q.shape[1], q.shape[2]), device=q.device, dtype=torch.float32)
345
+
346
+ grid = lambda args: (triton.cdiv(q.shape[2], args["BLOCK_M"]), q.shape[0] * q.shape[1], 1)
347
+
348
+ config_key = 'BLOCK_N_LG=64' if chunk_size_k == 64 else 'default'
349
+ if chunk_size_k > 128:
350
+ fwd_func = _attn_fwd_bsa_varlen
351
+ kernel_config = {} if os.environ.get('TRITON_AUTOTUNE_ENBALE', '0') == '1' else configs_fwd_bsa_varlen_preset[config_key]
352
+ else:
353
+ fwd_func = _attn_fwd_bsa_varlen_align
354
+ kernel_config = {} if os.environ.get('TRITON_AUTOTUNE_ENBALE', '0') == '1' else configs_fwd_bsa_varlen_align_preset[config_key]
355
+
356
+ block_indices = block_indices.contiguous()
357
+ block_indices_lens = block_indices_lens.contiguous()
358
+
359
+ has_kv_mask = 1 if kv_valid_mask is not None else 0
360
+ _kv_mask = kv_valid_mask if kv_valid_mask is not None else torch.empty(0, dtype=torch.bool, device=q.device)
361
+
362
+ fwd_func[grid](
363
+ q, k, v, sm_scale, M, o,
364
+ block_indices,
365
+ block_indices_lens,
366
+ _kv_mask,
367
+ q.stride(0), q.stride(1), q.stride(2), q.stride(3),
368
+ k.stride(0), k.stride(1), k.stride(2), k.stride(3),
369
+ v.stride(0), v.stride(1), v.stride(2), v.stride(3),
370
+ o.stride(0), o.stride(1), o.stride(2), o.stride(3),
371
+ block_indices.stride(0), block_indices.stride(1), block_indices.stride(2), block_indices.stride(3),
372
+ block_indices_lens.stride(0), block_indices_lens.stride(1), block_indices_lens.stride(2),
373
+ H, Seq,
374
+ D,
375
+ BLOCK_M=chunk_size_q,
376
+ BLOCK_N_LG=chunk_size_k,
377
+ SPARSITY=sparsity,
378
+ HAS_KV_MASK=has_kv_mask,
379
+ **kernel_config
380
+ )
381
+
382
+ LN2 = 0.6931471824645996
383
+ lse = M * LN2
384
+
385
+ return o, lse
386
+
387
+ def attn_bwd_bsa_varlen_triton(
388
+ do,
389
+ q,
390
+ k,
391
+ v,
392
+ o,
393
+ dq,
394
+ dk,
395
+ dv,
396
+ sm_scale,
397
+ M,
398
+ block_indices,
399
+ block_indices_lens,
400
+ chunk_size_q,
401
+ chunk_size_k,
402
+ sparsity,
403
+ kv_valid_mask=None,
404
+ ):
405
+ RCP_LN2 = 1.4426950408889634
406
+ M = M * RCP_LN2 # ln -> log2
407
+
408
+ do = do.contiguous()
409
+ # assert q.stride() == k.stride() == v.stride() == o.stride() == do.stride()
410
+
411
+ BATCH, N_HEAD, N_CTX, HEAD_DIM = q.shape
412
+ N_CTX_KV = k.shape[-2]
413
+
414
+ RCP_LN2 = 1.4426950408889634 # = 1.0 / ln(2) # reciprocal
415
+ arg_k = k
416
+ arg_k = arg_k * (sm_scale * RCP_LN2)
417
+
418
+ if min(chunk_size_q, chunk_size_k) >= 128:
419
+ PRE_BLOCK = 128
420
+ else:
421
+ PRE_BLOCK = min(chunk_size_q, chunk_size_k)
422
+
423
+ assert N_CTX % PRE_BLOCK == 0
424
+ pre_grid = (N_CTX // PRE_BLOCK, BATCH * N_HEAD)
425
+ delta = torch.empty_like(M)
426
+ _attn_bwd_preprocess[pre_grid](
427
+ o, do,
428
+ delta,
429
+ N_CTX,
430
+ BLOCK_M=PRE_BLOCK,
431
+ HEAD_DIM=HEAD_DIM
432
+ )
433
+
434
+ _N_cols = N_CTX_KV // chunk_size_k
435
+ _Hn = block_indices.shape[1]
436
+ _ki_list, _kl_list = [], []
437
+ for _h in range(_Hn):
438
+ _ki_h, _kl_h = create_indices_k_from_indices_q_varlen(
439
+ block_indices=block_indices[:, _h:_h+1],
440
+ N_cols_mask=_N_cols,
441
+ )
442
+ _ki_list.append(_ki_h)
443
+ _kl_list.append(_kl_h)
444
+ del _ki_h, _kl_h
445
+ block_indices_k = torch.cat(_ki_list, dim=1)
446
+ block_indices_k_lens = torch.cat(_kl_list, dim=1)
447
+ del _ki_list, _kl_list
448
+
449
+ block_indices = block_indices.contiguous()
450
+ block_indices_lens = block_indices_lens.contiguous()
451
+ block_indices_k = block_indices_k.contiguous()
452
+ block_indices_k_lens = block_indices_k_lens.contiguous()
453
+
454
+ has_kv_mask = 1 if kv_valid_mask is not None else 0
455
+ _kv_mask = kv_valid_mask if kv_valid_mask is not None else torch.empty(0, dtype=torch.bool, device=q.device)
456
+
457
+ config_key = 'BLOCK_N_DQ_LG=64' if chunk_size_k == 64 else 'default'
458
+ kernel_config = {} if os.environ.get('TRITON_AUTOTUNE_ENBALE', '0') == '1' else configs_bwd_dkdv_bsa_varlen_preset[config_key]
459
+
460
+ grid_dkdv = lambda args: (triton.cdiv(arg_k.shape[2], args["BLOCK_N"]), 1, arg_k.shape[0] * arg_k.shape[1])
461
+ _attn_bwd_dkdv_bsa_varlen_wrapper[grid_dkdv](
462
+ q, arg_k, v, sm_scale,
463
+ do,
464
+ dk, dv,
465
+ M,
466
+ delta,
467
+ block_indices_k,
468
+ block_indices_k_lens,
469
+ _kv_mask,
470
+ q.stride(0), q.stride(1), q.stride(2), q.stride(3),
471
+ k.stride(0), k.stride(1), k.stride(2), k.stride(3),
472
+ v.stride(0), v.stride(1), v.stride(2), v.stride(3),
473
+ dk.stride(0), dk.stride(1), dk.stride(2), dk.stride(3),
474
+ dv.stride(0), dv.stride(1), dv.stride(2), dv.stride(3),
475
+ do.stride(0), do.stride(1), do.stride(2), do.stride(3),
476
+ M.stride(0), M.stride(1), M.stride(2),
477
+ delta.stride(0), delta.stride(1), delta.stride(2),
478
+ block_indices_k.stride(0), block_indices_k.stride(1), block_indices_k.stride(2), block_indices_k.stride(3),
479
+ block_indices_k_lens.stride(0), block_indices_k_lens.stride(1), block_indices_k_lens.stride(2),
480
+ N_HEAD, N_CTX,
481
+ BLOCK_M=chunk_size_q,
482
+ BLOCK_N_DQ_LG=chunk_size_k,
483
+ HEAD_DIM=HEAD_DIM,
484
+ SPARSITY=sparsity,
485
+ HAS_KV_MASK=has_kv_mask,
486
+ **kernel_config
487
+ )
488
+
489
+ config_key = 'BLOCK_N_DQ_LG=64' if chunk_size_k == 64 else 'default'
490
+ if chunk_size_k > 128:
491
+ bwd_dq_func = _attn_bwd_dq_bsa_varlen_wrapper
492
+ kernel_config = {} if os.environ.get('TRITON_AUTOTUNE_ENBALE', '0') == '1' else configs_bwd_dq_bsa_varlen_preset[config_key]
493
+ else:
494
+ bwd_dq_func = _attn_bwd_dq_bsa_varlen_align_wrapper
495
+ kernel_config = {} if os.environ.get('TRITON_AUTOTUNE_ENBALE', '0') == '1' else configs_bwd_dq_bsa_varlen_align_preset[config_key]
496
+
497
+ grid_dq = lambda args: (triton.cdiv(q.shape[2], args["BLOCK_M"]), 1, q.shape[0] * q.shape[1])
498
+ bwd_dq_func[grid_dq](
499
+ q, arg_k, v,
500
+ do,
501
+ dq,
502
+ M,
503
+ delta,
504
+ block_indices,
505
+ block_indices_lens,
506
+ _kv_mask,
507
+ q.stride(0), q.stride(1), q.stride(2), q.stride(3),
508
+ k.stride(0), k.stride(1), k.stride(2), k.stride(3),
509
+ v.stride(0), v.stride(1), v.stride(2), v.stride(3),
510
+ dq.stride(0), dq.stride(1), dq.stride(2), dq.stride(3),
511
+ do.stride(0), do.stride(1), do.stride(2), do.stride(3),
512
+ M.stride(0), M.stride(1), M.stride(2),
513
+ delta.stride(0), delta.stride(1), delta.stride(2),
514
+ block_indices.stride(0), block_indices.stride(1), block_indices.stride(2), block_indices.stride(3),
515
+ block_indices_lens.stride(0), block_indices_lens.stride(1), block_indices_lens.stride(2),
516
+ N_HEAD, N_CTX,
517
+ BLOCK_M=chunk_size_q,
518
+ BLOCK_N_DQ_LG=chunk_size_k,
519
+ HEAD_DIM=HEAD_DIM,
520
+ SPARSITY=sparsity,
521
+ HAS_KV_MASK=has_kv_mask,
522
+ **kernel_config
523
+ )
524
+
525
+ @torch.compile
526
+ def make_block_indices_varlen_cp_list(block_indices, cp_size, num_blocks_k_full):
527
+ """
528
+ Args:
529
+ block_indices: [B, H, num_blocks_q_per_cp_rank, num_blocks_k_full]
530
+
531
+ Return:
532
+ a list of [block_indices, block_indices_lens] for k from each cp_rank
533
+ - each block_indices starts from zero
534
+ - block_indices_lens indicates the valid number of elements in the last dimension of block_indices
535
+ """
536
+ res = []
537
+ num_blocks_per_rank = num_blocks_k_full // cp_size
538
+ for i in range(cp_size):
539
+ block_indices_tmp = block_indices.clone()
540
+ min_block_idx = i * num_blocks_per_rank
541
+ block_indices_tmp -= min_block_idx
542
+ block_indices_tmp[block_indices_tmp < 0] = num_blocks_per_rank # block_indices_tmp < 0 indicate invalid indices, set them to num_blocks_per_rank in order to sort them to the tail, so that the first N elements of the block_indices indicated by block_indices_lens are valid
543
+
544
+ block_indices_tmp, _ = torch.sort(block_indices_tmp, dim=-1)
545
+
546
+ block_indices_tmp_lens = (block_indices_tmp < num_blocks_per_rank).sum(dim=-1)
547
+
548
+ res.append([block_indices_tmp, block_indices_tmp_lens])
549
+
550
+ return res
551
+
552
+ @torch.compile
553
+ def flash_attn_fwd_softmax_lse_correction(
554
+ softmax_lse: torch.Tensor,
555
+ softmax_lse_per_step: torch.Tensor,
556
+ ):
557
+ """Merge softmax stats of each step in Attention with context parallelism"""
558
+ max_scale = torch.max(softmax_lse, softmax_lse_per_step)
559
+ min_scale = torch.min(softmax_lse, softmax_lse_per_step)
560
+ lse_diff = min_scale - max_scale
561
+ lse_diff = lse_diff.nan_to_num(nan=0.) # handle cases: tensor(-inf) - tensor(-inf) = tensor(nan); In the current cp implementation, it is possible that lses of 2 cp ranks are both -inf, if no block is selected from both cp ranks. In such cases, the finally corrected lse should remain -inf.
562
+ new_scale = max_scale + torch.log1p(torch.exp(lse_diff)) # a + ln(1 + e^(b - a)) = ln(e^a) + ln(1 + e^(b - a)) = ln(e^a + e^b)
563
+ softmax_lse.copy_(new_scale)
564
+
565
+ @torch.compile
566
+ def flash_attn_fwd_out_correction_init(
567
+ out_init_step: torch.Tensor, # b h s d
568
+ softmax_lse: torch.Tensor, # b h s
569
+ softmax_lse_init_step: torch.Tensor,
570
+ ):
571
+ """Merge partial outputs of the first step in Attention with context parallelism"""
572
+ softmax_lse_corrected_exp = torch.exp(softmax_lse_init_step - softmax_lse)
573
+ softmax_lse_corrected_exp = softmax_lse_corrected_exp.unsqueeze(-1)
574
+ out_corrected = out_init_step * softmax_lse_corrected_exp
575
+ return out_corrected.to(out_init_step.dtype)
576
+
577
+
578
+ @torch.compile
579
+ def flash_attn_fwd_out_correction(
580
+ out: torch.Tensor,
581
+ out_per_step: torch.Tensor,
582
+ softmax_lse: torch.Tensor,
583
+ softmax_lse_per_step: torch.Tensor,
584
+ ):
585
+ """Merge partial outputs of each step in Attention with context parallelism"""
586
+ softmax_lse_corrected_exp = torch.exp(softmax_lse_per_step - softmax_lse)
587
+ softmax_lse_corrected_exp = softmax_lse_corrected_exp.unsqueeze(-1)
588
+ out_corrected = out_per_step * softmax_lse_corrected_exp
589
+ out.add_(out_corrected)
590
+
591
+ @torch.compile
592
+ def topk_sort(score, num_chunks_selected):
593
+ block_indices = torch.topk(score, num_chunks_selected)[1]
594
+ block_indices, _ = torch.sort(block_indices, dim=-1)
595
+ return block_indices
596
+
597
+ class _attention_bsa(torch.autograd.Function):
598
+
599
+ @staticmethod
600
+ def forward(ctx, q, k, v, chunk_size_q, chunk_size_k, sparsity, cdf_threshold, sm_scale,
601
+ kv_valid_mask=None, q_valid_mask=None, score_modifier=None):
602
+ HEAD_DIM_Q, HEAD_DIM_K = q.shape[-1], k.shape[-1]
603
+ HEAD_DIM_V = v.shape[-1]
604
+ assert HEAD_DIM_Q == HEAD_DIM_K and HEAD_DIM_K == HEAD_DIM_V
605
+ assert HEAD_DIM_K in {16, 32, 64, 128, 256}
606
+
607
+ # ---------------------- gating ----------------------
608
+ if q_valid_mask is not None:
609
+ q_cmp = masked_mean_pooling_compression(q, chunk_size_q, q_valid_mask)
610
+ elif kv_valid_mask is not None and q.shape[2] == k.shape[2]:
611
+ q_cmp = masked_mean_pooling_compression(q, chunk_size_q, kv_valid_mask)
612
+ else:
613
+ q_cmp = mean_pooling_compression(q, chunk_size_q)
614
+
615
+ if kv_valid_mask is not None:
616
+ k_cmp = masked_mean_pooling_compression(k, chunk_size_k, kv_valid_mask)
617
+ else:
618
+ k_cmp = mean_pooling_compression(k, chunk_size_k)
619
+
620
+ score = cal_score(q_cmp, k_cmp)
621
+
622
+ if score_modifier is not None:
623
+ score = score * score_modifier
624
+
625
+ if sparsity is not None and cdf_threshold is None:
626
+ block_indices, block_indices_lens = get_select_indices_topk_from_score(score, sparsity)
627
+ elif sparsity is None and cdf_threshold is not None:
628
+ block_indices, block_indices_lens = get_select_indices_cdf_from_score(score, cdf_threshold, sm_scale)
629
+ elif sparsity is not None and cdf_threshold is not None:
630
+ block_indices, block_indices_lens = get_select_indices_cdf_topk_from_score(score, sparsity, cdf_threshold, sm_scale)
631
+ else:
632
+ raise ValueError("Either sparsity or cdf_threshold must be provided")
633
+
634
+ # ---------------------- bsa ----------------------
635
+
636
+ o, lse = attn_fwd_bsa_varlen_triton(
637
+ q, k, v,
638
+ sm_scale, block_indices, block_indices_lens,
639
+ chunk_size_q, chunk_size_k,
640
+ sparsity,
641
+ kv_valid_mask=kv_valid_mask,
642
+ )
643
+
644
+ ctx.save_for_backward(q, k, v, o, lse, block_indices, block_indices_lens)
645
+ ctx.sm_scale = sm_scale
646
+ ctx.HEAD_DIM = HEAD_DIM_K
647
+ ctx.chunk_size_q = chunk_size_q
648
+ ctx.chunk_size_k = chunk_size_k
649
+ ctx.sparsity = sparsity
650
+ ctx.kv_valid_mask = kv_valid_mask
651
+
652
+ return o
653
+
654
+ @staticmethod
655
+ def backward(ctx, do):
656
+ q, k, v, o, lse, block_indices, block_indices_lens = ctx.saved_tensors
657
+
658
+ dq = torch.empty_like(q)
659
+ dk = torch.empty_like(k)
660
+ dv = torch.empty_like(v)
661
+
662
+ attn_bwd_bsa_varlen_triton(
663
+ do,
664
+ q,
665
+ k,
666
+ v,
667
+ o,
668
+ dq,
669
+ dk,
670
+ dv,
671
+ ctx.sm_scale,
672
+ lse,
673
+ block_indices,
674
+ block_indices_lens,
675
+ ctx.chunk_size_q,
676
+ ctx.chunk_size_k,
677
+ ctx.sparsity,
678
+ kv_valid_mask=ctx.kv_valid_mask,
679
+ )
680
+
681
+ return dq, dk, dv, None, None, None, None, None, None, None, None
682
+
683
+ flash_attn_bsa = _attention_bsa.apply
684
+
685
+
686
+ class _attention_bsa_audio_guided(torch.autograd.Function):
687
+ """BSA with audio-guided score modulation (Gate 2: Audio Spatial Concentration Gate).
688
+
689
+ The audio saliency modulates the block-level QK scores BEFORE topk selection,
690
+ but the actual sparse attention Triton kernel remains unchanged. This ensures
691
+ Triton-level efficiency for the compute-heavy attention, while the lightweight
692
+ score modulation uses standard PyTorch ops (torch.compile friendly).
693
+ """
694
+
695
+ @staticmethod
696
+ def forward(ctx, q, k, v, chunk_size_q, chunk_size_k, sparsity, cdf_threshold, sm_scale,
697
+ audio_block_saliency, audio_gate_value, audio_boost_gamma,
698
+ kv_valid_mask=None, q_valid_mask=None,
699
+ audio_norms_block_order=None, audio_weighted_lambda=0.0,
700
+ score_modifier=None):
701
+ """
702
+ Args:
703
+ audio_block_saliency: [N_blocks_k] or [B, 1, N_blocks_k] — per-key-block saliency in [0,1]
704
+ audio_gate_value: scalar in [0,1] — Audio Spatial Concentration Gate g_a
705
+ audio_boost_gamma: scalar >= 0 — boost strength γ
706
+ audio_norms_block_order: [S] — per-token norms in block-rearranged order (for Path B)
707
+ audio_weighted_lambda: float — Path B weighting strength (0=standard pooling)
708
+ score_modifier: optional broadcastable tensor — external additive boost term
709
+ (e.g., γ_var * ρ from variance guidance). Combined with audio INSIDE
710
+ one boost bracket: score *= (1 + γ_audio * S̃ + score_modifier).
711
+ """
712
+ HEAD_DIM_Q, HEAD_DIM_K = q.shape[-1], k.shape[-1]
713
+ HEAD_DIM_V = v.shape[-1]
714
+ assert HEAD_DIM_Q == HEAD_DIM_K and HEAD_DIM_K == HEAD_DIM_V
715
+ assert HEAD_DIM_K in {16, 32, 64, 128, 256}
716
+
717
+ B, H, Sq, D = q.shape
718
+ Sk = k.shape[2]
719
+
720
+ # ---------------------- gating (block-level scoring) ----------------------
721
+ if q_valid_mask is not None:
722
+ q_cmp = masked_mean_pooling_compression(q, chunk_size_q, q_valid_mask)
723
+ elif kv_valid_mask is not None and q.shape[2] == k.shape[2]:
724
+ q_cmp = masked_mean_pooling_compression(q, chunk_size_q, kv_valid_mask)
725
+ else:
726
+ q_cmp = mean_pooling_compression(q, chunk_size_q)
727
+
728
+ use_path_b = (audio_norms_block_order is not None and audio_weighted_lambda > 0)
729
+ if use_path_b:
730
+ k_cmp = audio_weighted_mean_pooling_compression(
731
+ k, chunk_size_k, audio_norms_block_order,
732
+ valid_mask=kv_valid_mask, lambda_w=audio_weighted_lambda,
733
+ )
734
+ elif kv_valid_mask is not None:
735
+ k_cmp = masked_mean_pooling_compression(k, chunk_size_k, kv_valid_mask)
736
+ else:
737
+ k_cmp = mean_pooling_compression(k, chunk_size_k)
738
+
739
+ score = cal_score(q_cmp, k_cmp) # [B, H, N_blocks_q, N_blocks_k]
740
+
741
+ # ---------------------- Unified Additive Boost (md 3.5.1.2) ----------------------
742
+ # I_{i,j} = P^QK * (1 + γ_audio * S̃ + additive_boost_from_external)
743
+ # All boost terms are ADDITIVE inside one bracket, then multiply P^QK.
744
+ N_blocks_k = score.shape[-1]
745
+ boost = torch.zeros(1, device=score.device, dtype=score.dtype)
746
+
747
+ has_audio = (audio_block_saliency is not None
748
+ and audio_block_saliency.numel() > 0
749
+ and audio_boost_gamma > 0)
750
+ if has_audio:
751
+ if audio_block_saliency.dim() == 1:
752
+ saliency = audio_block_saliency.view(1, 1, 1, -1)
753
+ else:
754
+ saliency = audio_block_saliency.unsqueeze(-2)
755
+ uniform_val = 1.0 / N_blocks_k
756
+ gated_saliency = audio_gate_value * saliency + (1.0 - audio_gate_value) * uniform_val
757
+ boost = boost + audio_boost_gamma * gated_saliency
758
+
759
+ if score_modifier is not None:
760
+ boost = boost + score_modifier
761
+
762
+ score = score * (1.0 + boost)
763
+
764
+ # ---------------------- topk selection from modulated scores ----------------------
765
+ if sparsity is not None and cdf_threshold is None:
766
+ block_indices, block_indices_lens = get_select_indices_topk_from_score(score, sparsity)
767
+ elif sparsity is None and cdf_threshold is not None:
768
+ block_indices, block_indices_lens = get_select_indices_cdf_from_score(score, cdf_threshold, sm_scale)
769
+ elif sparsity is not None and cdf_threshold is not None:
770
+ block_indices, block_indices_lens = get_select_indices_cdf_topk_from_score(score, sparsity, cdf_threshold, sm_scale)
771
+ else:
772
+ raise ValueError("Either sparsity or cdf_threshold must be provided")
773
+
774
+ # ---------------------- bsa (Triton kernel unchanged) ----------------------
775
+ o, lse = attn_fwd_bsa_varlen_triton(
776
+ q, k, v,
777
+ sm_scale, block_indices, block_indices_lens,
778
+ chunk_size_q, chunk_size_k,
779
+ sparsity,
780
+ kv_valid_mask=kv_valid_mask,
781
+ )
782
+
783
+ ctx.save_for_backward(q, k, v, o, lse, block_indices, block_indices_lens)
784
+ ctx.sm_scale = sm_scale
785
+ ctx.HEAD_DIM = HEAD_DIM_K
786
+ ctx.chunk_size_q = chunk_size_q
787
+ ctx.chunk_size_k = chunk_size_k
788
+ ctx.sparsity = sparsity
789
+ ctx.kv_valid_mask = kv_valid_mask
790
+
791
+ return o
792
+
793
+ @staticmethod
794
+ def backward(ctx, do):
795
+ q, k, v, o, lse, block_indices, block_indices_lens = ctx.saved_tensors
796
+
797
+ dq = torch.empty_like(q)
798
+ dk = torch.empty_like(k)
799
+ dv = torch.empty_like(v)
800
+
801
+ attn_bwd_bsa_varlen_triton(
802
+ do,
803
+ q,
804
+ k,
805
+ v,
806
+ o,
807
+ dq,
808
+ dk,
809
+ dv,
810
+ ctx.sm_scale,
811
+ lse,
812
+ block_indices,
813
+ block_indices_lens,
814
+ ctx.chunk_size_q,
815
+ ctx.chunk_size_k,
816
+ ctx.sparsity,
817
+ kv_valid_mask=ctx.kv_valid_mask,
818
+ )
819
+
820
+ # Order: q, k, v, chunk_size_q, chunk_size_k, sparsity, cdf_threshold, sm_scale,
821
+ # audio_block_saliency, audio_gate_value, audio_boost_gamma,
822
+ # kv_valid_mask, q_valid_mask, audio_norms_block_order, audio_weighted_lambda,
823
+ # score_modifier
824
+ return dq, dk, dv, None, None, None, None, None, None, None, None, None, None, None, None, None
825
+
826
+
827
+ flash_attn_bsa_audio_guided = _attention_bsa_audio_guided.apply
828
+
829
+
830
+ @torch.compile
831
+ def compute_timestep_reliability_gate(
832
+ timestep_ratio: torch.Tensor,
833
+ mode: str = "power",
834
+ power: float = 1.5,
835
+ scale: float = 10.0,
836
+ threshold: float = 0.7,
837
+ ) -> torch.Tensor:
838
+ """Compute Gate 1: Timestep Reliability Gate r_t (non-learnable).
839
+
840
+ At high noise (t→1): r_t→0 (don't trust audio guidance, bridge residual is noise-driven).
841
+ At low noise (t→0): r_t→1 (trust audio guidance, bridge residual reflects real saliency).
842
+
843
+ Two modes:
844
+ - "power": r_t = (1 - t)^power. Physically motivated by flow matching signal decay.
845
+ power=1.5 is recommended (moderate decay, default).
846
+ - "sigmoid": r_t = sigmoid(scale * (threshold - t)). Sharp transition.
847
+
848
+ Args:
849
+ timestep_ratio: scalar or [B] — normalized timestep in [0, 1] (0=clean, 1=noise)
850
+ mode: "power" (default) or "sigmoid"
851
+ power: exponent for power-law mode (default 1.5)
852
+ scale: sharpness for sigmoid mode (default 10.0)
853
+ threshold: midpoint for sigmoid mode (default 0.7)
854
+
855
+ Returns:
856
+ r_t: scalar in [0, 1]
857
+ """
858
+ if mode == "power":
859
+ r_t = (1.0 - timestep_ratio.clamp(0.0, 1.0)) ** power
860
+ else:
861
+ r_t = torch.sigmoid(scale * (threshold - timestep_ratio))
862
+ return r_t
863
+
864
+
865
+ @torch.compile
866
+ def compute_audio_spatial_concentration_gate(
867
+ token_norms: torch.Tensor,
868
+ valid_mask: torch.Tensor = None,
869
+ alpha: float = 5.0,
870
+ beta: float = 2.0,
871
+ ) -> torch.Tensor:
872
+ """Compute Gate 2: Audio Spatial Concentration Gate g_a from per-token L2 norms.
873
+
874
+ g_a = sigmoid(alpha * CV - beta)
875
+ where CV = std(norms) / (mean(norms) + eps)
876
+
877
+ Args:
878
+ token_norms: [N_tokens] — L2 norms of bridge residual per video token
879
+ valid_mask: [N_tokens] bool — True for real tokens, False for padded
880
+ alpha, beta: fixed hyperparameters (non-learnable)
881
+
882
+ Returns:
883
+ g_a: scalar in [0, 1]
884
+ """
885
+ eps = 1e-6
886
+ if valid_mask is not None:
887
+ mask_f = valid_mask.float()
888
+ count = mask_f.sum().clamp(min=1.0)
889
+ mean_a = (token_norms * mask_f).sum() / count
890
+ var_a = ((token_norms - mean_a).pow(2) * mask_f).sum() / count
891
+ else:
892
+ mean_a = token_norms.mean()
893
+ var_a = token_norms.var(unbiased=False)
894
+
895
+ std_a = var_a.sqrt()
896
+ cv = std_a / (mean_a + eps)
897
+ g_a = torch.sigmoid(alpha * cv - beta)
898
+ return g_a
899
+
900
+
901
+ @torch.compile
902
+ def audio_weighted_mean_pooling_compression(
903
+ x: torch.Tensor,
904
+ block_size: int,
905
+ token_norms: torch.Tensor,
906
+ valid_mask: torch.Tensor = None,
907
+ lambda_w: float = 1.0,
908
+ ) -> torch.Tensor:
909
+ """Audio-weighted mean pooling for K-side block representatives (Path B).
910
+
911
+ w_m = 1 + lambda_w * (a_m / max(a_m' in block) + eps)
912
+ K_bar_j = sum(w_m * K_m) / sum(w_m)
913
+
914
+ Automatically degrades to standard mean pooling when:
915
+ - silence: all a_m ≈ 0 → w_m ≈ 1
916
+ - high noise: all a_m similar → a_m/max ≈ 1 → uniform weighting
917
+ - lambda_w = 0: w_m = 1 exactly
918
+
919
+ Args:
920
+ x: [B, H, S, D] — K or V tokens (already in 3D-block rearranged order)
921
+ block_size: number of tokens per block (e.g., 64)
922
+ token_norms: [S] — per-token L2 norms of bridge residual (block-rearranged order)
923
+ valid_mask: [S] bool — True for real tokens, False for padded
924
+ lambda_w: weighting strength (default 1.0; 0=standard mean pooling)
925
+
926
+ Returns:
927
+ x_cmp: [B, H, N_blocks, D] — weighted block representatives
928
+ """
929
+ B, H, S, D = x.shape
930
+ eps = 1e-6
931
+ num_blocks = math.ceil(S / block_size)
932
+ if S % block_size != 0:
933
+ pad_len = num_blocks * block_size - S
934
+ x = F.pad(x, (0, 0, 0, pad_len))
935
+ token_norms = F.pad(token_norms, (0, pad_len), value=0.0)
936
+ if valid_mask is not None:
937
+ valid_mask = F.pad(valid_mask, (0, pad_len), value=False)
938
+
939
+ # Reshape to [num_blocks, block_size]
940
+ norms_blocks = token_norms.view(num_blocks, block_size)
941
+
942
+ # Intra-block normalized weighting: w_m = 1 + λ * (a_m / max_in_block)
943
+ if valid_mask is not None:
944
+ mask_blocks = valid_mask.view(num_blocks, block_size).float()
945
+ # Mask out padded positions for max computation
946
+ norms_masked = norms_blocks * mask_blocks + (-1e9) * (1 - mask_blocks)
947
+ block_max = norms_masked.max(dim=1, keepdim=True).values.clamp(min=eps)
948
+ # Reset negatives from masking
949
+ norms_for_weight = norms_blocks * mask_blocks
950
+ else:
951
+ block_max = norms_blocks.max(dim=1, keepdim=True).values.clamp(min=eps)
952
+ norms_for_weight = norms_blocks
953
+ mask_blocks = None
954
+
955
+ # w_m = 1 + λ * (a_m / max)
956
+ weights = 1.0 + lambda_w * (norms_for_weight / block_max) # [num_blocks, block_size]
957
+
958
+ if mask_blocks is not None:
959
+ weights = weights * mask_blocks # zero out padded positions
960
+
961
+ # Reshape x for block-wise weighted mean: [B, H, num_blocks, block_size, D]
962
+ x_blocks = x.view(B, H, num_blocks, block_size, D)
963
+
964
+ # weights: [num_blocks, block_size] → [1, 1, num_blocks, block_size, 1] for broadcasting
965
+ w = weights.unsqueeze(0).unsqueeze(0).unsqueeze(-1) # [1, 1, num_blocks, block_size, 1]
966
+ w_sum = w.sum(dim=3, keepdim=True).clamp(min=1.0) # [1, 1, num_blocks, 1, 1]
967
+
968
+ x_cmp = (x_blocks * w).sum(dim=3) / w_sum.squeeze(3) # [B, H, num_blocks, D]
969
+ return x_cmp
970
+
971
+
972
+ @torch.compile
973
+ def compute_channel_variance_density(
974
+ x: torch.Tensor,
975
+ block_size: int,
976
+ bt: int, bh: int, bw: int,
977
+ valid_mask: torch.Tensor = None,
978
+ ) -> torch.Tensor:
979
+ """Compute per-block channel-variance information density (non-learnable).
980
+
981
+ Averages across all (local) heads for head-agnostic block-level statistics
982
+ (different heads project into different subspaces — single-head would bias
983
+ selection toward that head's variance pattern). Mean pooling stays in native
984
+ dtype (bf16) to avoid large float32 copies; only the tiny [num_blocks, D]
985
+ result converts to float32 for numerically precise variance computation.
986
+
987
+ Note (SP): under sequence parallelism each rank owns a head shard, so ρ is
988
+ computed from local heads only. This keeps the per-layer cost minimal (no
989
+ extra collective). ρ is a coarse per-block density signal whose estimate is
990
+ stable across head subsets, and each head's attention is computed on its own
991
+ rank, so the per-rank ρ is correct for that rank's heads.
992
+
993
+ Args:
994
+ x: [B, H, S, D] V tensor in 3D-block-rearranged order (V = x @ W_V, no RMSNorm/RoPE).
995
+ block_size: bt * bh * bw tokens per block.
996
+ bt, bh, bw: block shape in temporal, height, width.
997
+ valid_mask: [S] bool — True for real tokens, False for padding.
998
+
999
+ Returns:
1000
+ rho: [num_blocks] ∈ [0, 1] — normalized variance density per block.
1001
+ """
1002
+ S, D = x.shape[2], x.shape[3]
1003
+ num_blocks = S // block_size
1004
+ spatial_size = bh * bw
1005
+
1006
+ # Average across heads in native dtype (bf16) — no large float32 copy.
1007
+ # x[0]: [H, S, D] → .mean(dim=0): [S, D]. bf16 mean is numerically stable.
1008
+ x_avg = x[0].mean(dim=0) # [S, D], bf16, head-agnostic
1009
+ x_blocks = x_avg.view(num_blocks, block_size, D) # [num_blocks, block_size, D]
1010
+
1011
+ if valid_mask is not None:
1012
+ mask_blocks = valid_mask.view(num_blocks, block_size)
1013
+ mask_f = mask_blocks.unsqueeze(-1).to(x_blocks.dtype) # [num_blocks, block_size, 1]
1014
+ counts = mask_f.sum(dim=1).clamp(min=1.0) # [num_blocks, 1]
1015
+ else:
1016
+ mask_f = None
1017
+ counts = None
1018
+
1019
+ # --- Spatial Channel Variance σ²_ch-s ---
1020
+ # Per-block mean per channel (bf16), then variance across channels (float32)
1021
+ if mask_f is not None:
1022
+ channel_means = (x_blocks * mask_f).sum(dim=1) / counts # [num_blocks, D], bf16
1023
+ else:
1024
+ channel_means = x_blocks.mean(dim=1) # [num_blocks, D], bf16
1025
+
1026
+ cm_f = channel_means.float() # [num_blocks, D] — only this is float32 (~2.5MB)
1027
+ global_cm = cm_f.mean(dim=-1, keepdim=True) # [num_blocks, 1]
1028
+ spatial_var = ((cm_f - global_cm) ** 2).mean(dim=-1) # [num_blocks]
1029
+
1030
+ # --- Temporal Channel Variance σ²_ch-t ---
1031
+ if bt > 1:
1032
+ x_temporal = x_blocks.view(num_blocks, bt, spatial_size, D)
1033
+
1034
+ if valid_mask is not None:
1035
+ mask_t_3d = mask_blocks.view(num_blocks, bt, spatial_size)
1036
+ mask_t = mask_t_3d.unsqueeze(-1).to(x_blocks.dtype) # [num_blocks, bt, spatial_size, 1]
1037
+ fc = mask_t.sum(dim=2).clamp(min=1.0) # [num_blocks, bt, 1]
1038
+ frame_means = (x_temporal * mask_t).sum(dim=2) / fc # [num_blocks, bt, D], bf16
1039
+
1040
+ # Frame-level validity: only diff between pairs where BOTH frames have ≥1 valid token
1041
+ frame_valid = mask_t_3d.any(dim=2) # [num_blocks, bt]
1042
+ both_valid = frame_valid[:, 1:] & frame_valid[:, :-1] # [num_blocks, bt-1]
1043
+ bv_f = both_valid.float().unsqueeze(-1) # [num_blocks, bt-1, 1]
1044
+ valid_pairs = bv_f.sum(dim=1).clamp(min=1.0) # [num_blocks, 1]
1045
+
1046
+ fm_f = frame_means.float()
1047
+ frame_diffs = fm_f[:, 1:, :] - fm_f[:, :-1, :] # [num_blocks, bt-1, D]
1048
+ temporal_var = ((frame_diffs.pow(2) * bv_f).sum(dim=1) / valid_pairs).mean(dim=-1)
1049
+ else:
1050
+ frame_means = x_temporal.mean(dim=2) # [num_blocks, bt, D], bf16
1051
+ fm_f = frame_means.float()
1052
+ frame_diffs = fm_f[:, 1:, :] - fm_f[:, :-1, :] # [num_blocks, bt-1, D]
1053
+ temporal_var = frame_diffs.pow(2).mean(dim=1).mean(dim=-1)
1054
+ else:
1055
+ temporal_var = torch.zeros(num_blocks, device=x.device, dtype=torch.float32)
1056
+
1057
+ # --- Combine and normalize to [0, 1] ---
1058
+ combined = spatial_var + temporal_var
1059
+ rho = combined / combined.max().clamp(min=1e-6)
1060
+
1061
+ return rho
1062
+
1063
+
1064
+ @torch.compile
1065
+ def compute_block_audio_saliency(
1066
+ token_norms: torch.Tensor,
1067
+ chunk_size: int,
1068
+ valid_mask: torch.Tensor = None,
1069
+ ) -> torch.Tensor:
1070
+ """Compute per-block audio saliency from per-token L2 norms.
1071
+
1072
+ For each block j: S_j = mean(norms in block j) / max(mean norms across all blocks)
1073
+ Then clamp to [0, 1].
1074
+
1075
+ Args:
1076
+ token_norms: [S] — per-token L2 norms (already in 3D-block rearranged order)
1077
+ chunk_size: block size (e.g., 64 for 4x4x4)
1078
+ valid_mask: [S] bool — True for real tokens, False for padded
1079
+
1080
+ Returns:
1081
+ block_saliency: [N_blocks] in [0, 1]
1082
+ """
1083
+ S = token_norms.shape[0]
1084
+ num_blocks = math.ceil(S / chunk_size)
1085
+ if S % chunk_size != 0:
1086
+ pad_len = num_blocks * chunk_size - S
1087
+ token_norms = F.pad(token_norms, (0, pad_len), value=0.0)
1088
+ if valid_mask is not None:
1089
+ valid_mask = F.pad(valid_mask, (0, pad_len), value=False)
1090
+
1091
+ norms_blocks = token_norms.view(num_blocks, chunk_size)
1092
+
1093
+ if valid_mask is not None:
1094
+ mask_blocks = valid_mask.view(num_blocks, chunk_size).float()
1095
+ counts = mask_blocks.sum(dim=1).clamp(min=1.0)
1096
+ block_means = (norms_blocks * mask_blocks).sum(dim=1) / counts
1097
+ else:
1098
+ block_means = norms_blocks.mean(dim=1)
1099
+
1100
+ max_val = block_means.max().clamp(min=1e-6)
1101
+ block_saliency = block_means / max_val
1102
+ return block_saliency
1103
+
1104
+
1105
+ def rearrange_THW_to_3d_block(x, Nt, Nh, Nw, t, h, w, D):
1106
+ B, H, _, D = x.shape
1107
+ x = x.view(B, H, Nt, t, Nh, h, Nw, w, D)
1108
+ x = x.permute(0, 1, 2, 4, 6, 3, 5, 7, 8) # B H Nt Nh Nw t h w D
1109
+ return x.contiguous().view(B, H, Nt * Nh * Nw * t * h * w, D)
1110
+
1111
+ def rearrange_3d_block_to_THW(x, Nt, Nh, Nw, t, h, w, D):
1112
+ B, H, _, D = x.shape
1113
+ x = x.view(B, H, Nt, Nh, Nw, t, h, w, D)
1114
+ x = x.permute(0, 1, 2, 5, 3, 6, 4, 7, 8) # B H Nt t Nh h Nw w D
1115
+ return x.contiguous().view(B, H, Nt * t * Nh * h * Nw * w, D)
1116
+
1117
+ def rearrange_THW_to_3d_block_1d(mask, Nt, Nh, Nw, t, h, w):
1118
+ """Rearrange a 1D bool mask from THW order to 3D block order (same permutation as tokens)."""
1119
+ mask = mask.view(Nt, t, Nh, h, Nw, w)
1120
+ mask = mask.permute(0, 2, 4, 1, 3, 5) # Nt Nh Nw t h w
1121
+ return mask.contiguous().view(-1)
1122
+
1123
+ def flash_attn_bsa_3d(
1124
+ q: torch.Tensor, # [B, H, Sq, D]
1125
+ k: torch.Tensor, # [B, H, Skv, D]
1126
+ v: torch.Tensor, # [B, H, Skv, D]
1127
+ latent_shape_q,
1128
+ latent_shape_k,
1129
+ # bsa_params
1130
+ sparsity=0.875,
1131
+ cdf_threshold=None,
1132
+ chunk_3d_shape_q=[4, 4, 8],
1133
+ chunk_3d_shape_k=[4, 4, 8],
1134
+ valid_mask=None, # [Sq] bool, True=valid token, False=padded
1135
+ ) -> torch.Tensor:
1136
+ _, _, Sq, head_dim_q = q.shape
1137
+ _, _, Sk, head_dim_k = k.shape
1138
+
1139
+ assert head_dim_q == head_dim_k
1140
+ head_dim = head_dim_q
1141
+
1142
+ Tq, Hq, Wq = latent_shape_q
1143
+ Tk, Hk, Wk = latent_shape_k
1144
+
1145
+ assert Tq * Hq * Wq == Sq
1146
+ assert Tk * Hk * Wk == Sk
1147
+
1148
+ tq, hq, wq = chunk_3d_shape_q
1149
+ tk, hk, wk = chunk_3d_shape_k
1150
+
1151
+ assert Tq % tq == 0 and Hq % hq == 0 and Wq % wq == 0
1152
+ assert Tk % tk == 0 and Hk % hk == 0 and Wk % wk == 0
1153
+
1154
+ Ntq = Tq // tq
1155
+ Nhq = Hq // hq
1156
+ Nwq = Wq // wq
1157
+
1158
+ Ntk = Tk // tk
1159
+ Nhk = Hk // hk
1160
+ Nwk = Wk // wk
1161
+
1162
+ q = rearrange_THW_to_3d_block(q, Ntq, Nhq, Nwq, tq, hq, wq, q.shape[-1])
1163
+ k = rearrange_THW_to_3d_block(k, Ntk, Nhk, Nwk, tk, hk, wk, k.shape[-1])
1164
+ v = rearrange_THW_to_3d_block(v, Ntk, Nhk, Nwk, tk, hk, wk, v.shape[-1])
1165
+
1166
+ kv_valid_mask = None
1167
+ if valid_mask is not None:
1168
+ kv_valid_mask = rearrange_THW_to_3d_block_1d(valid_mask, Ntk, Nhk, Nwk, tk, hk, wk)
1169
+
1170
+ chunk_size_q = tq * hq * wq
1171
+ chunk_size_k = tk * hk * wk
1172
+
1173
+ output = flash_attn_bsa(q, k, v, chunk_size_q, chunk_size_k, sparsity, cdf_threshold, 1 / head_dim**0.5, kv_valid_mask)
1174
+
1175
+ output = rearrange_3d_block_to_THW(output, Ntq, Nhq, Nwq, tq, hq, wq, output.shape[-1])
1176
+ return output
1177
+
1178
+
1179
+ def flash_attn_bsa_3d_variance_guided(
1180
+ q: torch.Tensor,
1181
+ k: torch.Tensor,
1182
+ v: torch.Tensor,
1183
+ latent_shape_q,
1184
+ latent_shape_k,
1185
+ variance_boost_gamma: float = 1.0,
1186
+ sparsity=0.875,
1187
+ cdf_threshold=None,
1188
+ chunk_3d_shape_q=[4, 4, 4],
1189
+ chunk_3d_shape_k=[4, 4, 4],
1190
+ valid_mask=None,
1191
+ ) -> torch.Tensor:
1192
+ """BSA with channel-variance information density score modulation.
1193
+
1194
+ Completely independent from audio guidance — no shared state or code.
1195
+
1196
+ Computes ρ_j on V blocks (key/value side only), then applies:
1197
+ score_{i,j} *= (1 + γ * ρ_j)
1198
+
1199
+ Query-awareness comes from P^QK being per-query (zero-gate effect):
1200
+ - Sky query → lip key: P^QK ≈ 0, boost = 1.85, final ≈ 0 → not selected
1201
+ - Face query → lip key: P^QK ≈ 0.12, boost = 1.85, final = 0.222 → selected
1202
+ Same ρ_j, different P^QK → different top-k per query block.
1203
+
1204
+ Why V (not K or hidden states)?
1205
+ V = x @ W_V — raw linear projection, no RMSNorm, no RoPE distortion.
1206
+ V channel variance directly predicts information contribution I(V_block; Output):
1207
+ low variance → V rows near-parallel → output gain ≈ 0 regardless of attention weight.
1208
+ """
1209
+ _, _, Sq, head_dim_q = q.shape
1210
+ _, _, Sk, head_dim_k = k.shape
1211
+
1212
+ assert head_dim_q == head_dim_k
1213
+ head_dim = head_dim_q
1214
+
1215
+ Tq, Hq, Wq = latent_shape_q
1216
+ Tk, Hk, Wk = latent_shape_k
1217
+
1218
+ assert Tq * Hq * Wq == Sq
1219
+ assert Tk * Hk * Wk == Sk
1220
+
1221
+ tq, hq, wq = chunk_3d_shape_q
1222
+ tk, hk, wk = chunk_3d_shape_k
1223
+
1224
+ assert Tq % tq == 0 and Hq % hq == 0 and Wq % wq == 0
1225
+ assert Tk % tk == 0 and Hk % hk == 0 and Wk % wk == 0
1226
+
1227
+ Ntq = Tq // tq
1228
+ Nhq = Hq // hq
1229
+ Nwq = Wq // wq
1230
+
1231
+ Ntk = Tk // tk
1232
+ Nhk = Hk // hk
1233
+ Nwk = Wk // wk
1234
+
1235
+ q = rearrange_THW_to_3d_block(q, Ntq, Nhq, Nwq, tq, hq, wq, q.shape[-1])
1236
+ k = rearrange_THW_to_3d_block(k, Ntk, Nhk, Nwk, tk, hk, wk, k.shape[-1])
1237
+ v = rearrange_THW_to_3d_block(v, Ntk, Nhk, Nwk, tk, hk, wk, v.shape[-1])
1238
+
1239
+ kv_valid_mask = None
1240
+ if valid_mask is not None:
1241
+ kv_valid_mask = rearrange_THW_to_3d_block_1d(valid_mask, Ntk, Nhk, Nwk, tk, hk, wk)
1242
+
1243
+ chunk_size_q = tq * hq * wq
1244
+ chunk_size_k = tk * hk * wk
1245
+
1246
+ with torch.no_grad():
1247
+ rho = compute_channel_variance_density(v, chunk_size_k, tk, hk, wk, kv_valid_mask)
1248
+
1249
+ # Multiplicative modifier for variance-only path: score *= (1 + γ * ρ)
1250
+ score_modifier = (
1251
+ 1.0 + variance_boost_gamma * rho.to(q.dtype).view(1, 1, 1, -1)
1252
+ ).contiguous()
1253
+
1254
+ output = flash_attn_bsa(
1255
+ q, k, v, chunk_size_q, chunk_size_k, sparsity, cdf_threshold,
1256
+ 1 / head_dim**0.5, kv_valid_mask, None, score_modifier,
1257
+ )
1258
+
1259
+ output = rearrange_3d_block_to_THW(output, Ntq, Nhq, Nwq, tq, hq, wq, output.shape[-1])
1260
+ return output
1261
+
1262
+
1263
+ def _pad_1d_for_bsa(x, chunk_size):
1264
+ """Pad a 1D sequence [B, H, S, D] to be divisible by chunk_size. Returns (padded_x, valid_mask_or_None, orig_len)."""
1265
+ B, H, S, D = x.shape
1266
+ remainder = S % chunk_size
1267
+ if remainder == 0:
1268
+ return x, None, S
1269
+ pad_len = chunk_size - remainder
1270
+ x_padded = F.pad(x, (0, 0, 0, pad_len)).contiguous()
1271
+ valid_mask = torch.zeros(S + pad_len, dtype=torch.bool, device=x.device)
1272
+ valid_mask[:S] = True
1273
+ return x_padded, valid_mask, S
1274
+
1275
+
1276
+ def flash_attn_bsa_cross(
1277
+ q: torch.Tensor,
1278
+ k: torch.Tensor,
1279
+ v: torch.Tensor,
1280
+ q_structure: str,
1281
+ k_structure: str,
1282
+ q_grid_size=None,
1283
+ k_grid_size=None,
1284
+ sparsity=0.875,
1285
+ cdf_threshold=None,
1286
+ chunk_size_q=64,
1287
+ chunk_size_k=64,
1288
+ chunk_3d_shape_q=None,
1289
+ chunk_3d_shape_k=None,
1290
+ ) -> torch.Tensor:
1291
+ """Cross-modal BSA supporting asymmetric Q/K structures (3D video vs 1D audio).
1292
+
1293
+ Args:
1294
+ q: [B, H, Sq, D]
1295
+ k: [B, H, Sk, D]
1296
+ v: [B, H, Sk, D]
1297
+ q_structure: "3d" (video with grid T,H,W) or "1d" (audio sequential)
1298
+ k_structure: "3d" or "1d"
1299
+ q_grid_size: (T, H, W) when q_structure="3d"
1300
+ k_grid_size: (T, H, W) when k_structure="3d"
1301
+ sparsity: fraction of K blocks to skip
1302
+ chunk_size_q: block size for 1D Q (must be multiple of 64)
1303
+ chunk_size_k: block size for 1D K (must be multiple of 64)
1304
+ chunk_3d_shape_q: [t,h,w] for 3D Q blocking
1305
+ chunk_3d_shape_k: [t,h,w] for 3D K blocking
1306
+ """
1307
+ _, _, Sq, head_dim = q.shape
1308
+ _, _, Sk, _ = k.shape
1309
+
1310
+ q_valid_mask = None
1311
+ k_valid_mask = None
1312
+ q_orig_len = Sq
1313
+ k_orig_len = Sk
1314
+
1315
+ if q_structure == "3d":
1316
+ assert q_grid_size is not None
1317
+ Tq, Hq, Wq = q_grid_size
1318
+ tq, hq, wq = chunk_3d_shape_q or [4, 4, 4]
1319
+ pad_tq = (tq - Tq % tq) % tq
1320
+ pad_hq = (hq - Hq % hq) % hq
1321
+ pad_wq = (wq - Wq % wq) % wq
1322
+
1323
+ if pad_tq > 0 or pad_hq > 0 or pad_wq > 0:
1324
+ B, H, _, D = q.shape
1325
+ Tq_p, Hq_p, Wq_p = Tq + pad_tq, Hq + pad_hq, Wq + pad_wq
1326
+ idx_t = torch.arange(Tq_p, device=q.device)
1327
+ idx_h = torch.arange(Hq_p, device=q.device)
1328
+ idx_w = torch.arange(Wq_p, device=q.device)
1329
+ q_valid_mask = ((idx_t[:, None, None] < Tq) & (idx_h[None, :, None] < Hq) & (idx_w[None, None, :] < Wq)).reshape(-1).contiguous()
1330
+ q_3d = q.view(B, H, Tq, Hq, Wq, D)
1331
+ q = F.pad(q_3d, (0, 0, 0, pad_wq, 0, pad_hq, 0, pad_tq)).reshape(B, H, -1, D).contiguous()
1332
+ Tq, Hq, Wq = Tq_p, Hq_p, Wq_p
1333
+
1334
+ Ntq, Nhq, Nwq = Tq // tq, Hq // hq, Wq // wq
1335
+ q = rearrange_THW_to_3d_block(q, Ntq, Nhq, Nwq, tq, hq, wq, q.shape[-1])
1336
+ if q_valid_mask is not None:
1337
+ q_valid_mask = rearrange_THW_to_3d_block_1d(q_valid_mask, Ntq, Nhq, Nwq, tq, hq, wq)
1338
+ actual_chunk_size_q = tq * hq * wq
1339
+ else:
1340
+ q, q_valid_mask, q_orig_len = _pad_1d_for_bsa(q, chunk_size_q)
1341
+ actual_chunk_size_q = chunk_size_q
1342
+ Ntq = Nhq = Nwq = tq = hq = wq = None
1343
+
1344
+ if k_structure == "3d":
1345
+ assert k_grid_size is not None
1346
+ Tk, Hk, Wk = k_grid_size
1347
+ tk, hk, wk = chunk_3d_shape_k or [4, 4, 4]
1348
+ pad_tk = (tk - Tk % tk) % tk
1349
+ pad_hk = (hk - Hk % hk) % hk
1350
+ pad_wk = (wk - Wk % wk) % wk
1351
+
1352
+ if pad_tk > 0 or pad_hk > 0 or pad_wk > 0:
1353
+ B, H, _, D = k.shape
1354
+ Tk_p, Hk_p, Wk_p = Tk + pad_tk, Hk + pad_hk, Wk + pad_wk
1355
+ idx_t = torch.arange(Tk_p, device=k.device)
1356
+ idx_h = torch.arange(Hk_p, device=k.device)
1357
+ idx_w = torch.arange(Wk_p, device=k.device)
1358
+ k_valid_mask = ((idx_t[:, None, None] < Tk) & (idx_h[None, :, None] < Hk) & (idx_w[None, None, :] < Wk)).reshape(-1).contiguous()
1359
+ B, H, _, D = k.shape
1360
+ k_3d = k.view(B, H, Tk, Hk, Wk, D)
1361
+ k = F.pad(k_3d, (0, 0, 0, pad_wk, 0, pad_hk, 0, pad_tk)).reshape(B, H, -1, D).contiguous()
1362
+ v_3d = v.view(B, H, Tk, Hk, Wk, D)
1363
+ v = F.pad(v_3d, (0, 0, 0, pad_wk, 0, pad_hk, 0, pad_tk)).reshape(B, H, -1, D).contiguous()
1364
+ Tk, Hk, Wk = Tk_p, Hk_p, Wk_p
1365
+
1366
+ Ntk, Nhk, Nwk = Tk // tk, Hk // hk, Wk // wk
1367
+ k = rearrange_THW_to_3d_block(k, Ntk, Nhk, Nwk, tk, hk, wk, k.shape[-1])
1368
+ v = rearrange_THW_to_3d_block(v, Ntk, Nhk, Nwk, tk, hk, wk, v.shape[-1])
1369
+ if k_valid_mask is not None:
1370
+ k_valid_mask = rearrange_THW_to_3d_block_1d(k_valid_mask, Ntk, Nhk, Nwk, tk, hk, wk)
1371
+ actual_chunk_size_k = tk * hk * wk
1372
+ else:
1373
+ k, k_valid_mask_k, k_orig_len = _pad_1d_for_bsa(k, chunk_size_k)
1374
+ v, _, _ = _pad_1d_for_bsa(v, chunk_size_k)
1375
+ if k_valid_mask_k is not None:
1376
+ k_valid_mask = k_valid_mask_k
1377
+ actual_chunk_size_k = chunk_size_k
1378
+ Ntk = Nhk = Nwk = tk = hk = wk = None
1379
+
1380
+ output = flash_attn_bsa(q, k, v, actual_chunk_size_q, actual_chunk_size_k, sparsity, cdf_threshold, 1 / head_dim**0.5, k_valid_mask, q_valid_mask)
1381
+
1382
+ if q_structure == "3d":
1383
+ output = rearrange_3d_block_to_THW(output, Ntq, Nhq, Nwq, tq, hq, wq, output.shape[-1])
1384
+ if pad_tq > 0 or pad_hq > 0 or pad_wq > 0:
1385
+ B, H, _, D = output.shape
1386
+ Tq_orig, Hq_orig, Wq_orig = q_grid_size
1387
+ output = output.view(B, H, Tq, Hq, Wq, D)[:, :, :Tq_orig, :Hq_orig, :Wq_orig, :].contiguous().reshape(B, H, q_orig_len, D)
1388
+ else:
1389
+ if q_orig_len != output.shape[2]:
1390
+ output = output[:, :, :q_orig_len, :].contiguous()
1391
+
1392
+ return output
1393
+
1394
+
1395
+ def flash_attn_bsa_3d_audio_guided(
1396
+ q: torch.Tensor, # [B, H, Sq, D]
1397
+ k: torch.Tensor, # [B, H, Skv, D]
1398
+ v: torch.Tensor, # [B, H, Skv, D]
1399
+ latent_shape_q,
1400
+ latent_shape_k,
1401
+ audio_token_norms: torch.Tensor = None, # [Sq] per-token L2 norms of A->V bridge residual
1402
+ audio_gate_value: torch.Tensor = None, # scalar gate g_a in [0,1]
1403
+ audio_boost_gamma: torch.Tensor = None, # scalar boost γ >= 0
1404
+ sparsity=0.875,
1405
+ cdf_threshold=None,
1406
+ chunk_3d_shape_q=[4, 4, 4],
1407
+ chunk_3d_shape_k=[4, 4, 4],
1408
+ valid_mask=None, # [Sq] bool, True=valid, False=padded
1409
+ audio_weighted_lambda: float = 0.0, # Path B: weighted pooling strength (0=disabled)
1410
+ score_modifier: torch.Tensor = None, # external additive boost (e.g., γ_var * ρ from variance guidance)
1411
+ variance_boost_gamma: float = 0.0, # if > 0, compute ρ INSIDE on rearranged V (avoids double rearrange)
1412
+ ) -> torch.Tensor:
1413
+ """BSA with audio saliency score modulation.
1414
+
1415
+ Audio-specific: gated saliency boost + optional audio-weighted K pooling.
1416
+
1417
+ Combine modes for additional boosts (all additive inside one bracket):
1418
+ score *= (1 + γ_audio * S̃ + γ_var * ρ + score_modifier)
1419
+
1420
+ For audio + variance combination, prefer passing `variance_boost_gamma > 0`
1421
+ instead of `score_modifier`: that lets us compute ρ on the **already
1422
+ rearranged** V tensor, avoiding a second `rearrange_THW_to_3d_block(v)`
1423
+ on the caller side. Both produce identical results, but the in-function
1424
+ path saves one V permute+contiguous (≈30-80 MB memory bandwidth on 2K).
1425
+ """
1426
+ _, _, Sq, head_dim_q = q.shape
1427
+ _, _, Sk, head_dim_k = k.shape
1428
+
1429
+ assert head_dim_q == head_dim_k
1430
+ head_dim = head_dim_q
1431
+
1432
+ Tq, Hq, Wq = latent_shape_q
1433
+ Tk, Hk, Wk = latent_shape_k
1434
+
1435
+ assert Tq * Hq * Wq == Sq
1436
+ assert Tk * Hk * Wk == Sk
1437
+
1438
+ tq, hq, wq = chunk_3d_shape_q
1439
+ tk, hk, wk = chunk_3d_shape_k
1440
+
1441
+ assert Tq % tq == 0 and Hq % hq == 0 and Wq % wq == 0
1442
+ assert Tk % tk == 0 and Hk % hk == 0 and Wk % wk == 0
1443
+
1444
+ Ntq = Tq // tq
1445
+ Nhq = Hq // hq
1446
+ Nwq = Wq // wq
1447
+
1448
+ Ntk = Tk // tk
1449
+ Nhk = Hk // hk
1450
+ Nwk = Wk // wk
1451
+
1452
+ q = rearrange_THW_to_3d_block(q, Ntq, Nhq, Nwq, tq, hq, wq, q.shape[-1])
1453
+ k = rearrange_THW_to_3d_block(k, Ntk, Nhk, Nwk, tk, hk, wk, k.shape[-1])
1454
+ v = rearrange_THW_to_3d_block(v, Ntk, Nhk, Nwk, tk, hk, wk, v.shape[-1])
1455
+
1456
+ kv_valid_mask = None
1457
+ if valid_mask is not None:
1458
+ kv_valid_mask = rearrange_THW_to_3d_block_1d(valid_mask, Ntk, Nhk, Nwk, tk, hk, wk)
1459
+
1460
+ chunk_size_q = tq * hq * wq
1461
+ chunk_size_k = tk * hk * wk
1462
+
1463
+ # Optional in-function variance density (avoids the caller re-rearranging V).
1464
+ # Folded additively into score_modifier so audio + variance share one boost
1465
+ # bracket — no cross-term, exactly matches the standalone variance path.
1466
+ if variance_boost_gamma is not None and variance_boost_gamma > 0:
1467
+ with torch.no_grad():
1468
+ _rho = compute_channel_variance_density(v, chunk_size_k, tk, hk, wk, kv_valid_mask)
1469
+ _var_term = (variance_boost_gamma * _rho.to(q.dtype).view(1, 1, 1, -1)).contiguous()
1470
+ score_modifier = (score_modifier + _var_term) if score_modifier is not None else _var_term
1471
+
1472
+ # Audio saliency computation (only when audio norms are provided)
1473
+ has_audio_norms = (audio_token_norms is not None and audio_token_norms.numel() > 0)
1474
+ if has_audio_norms:
1475
+ audio_norms_block_order = rearrange_THW_to_3d_block_1d(
1476
+ audio_token_norms, Ntk, Nhk, Nwk, tk, hk, wk
1477
+ )
1478
+ block_saliency = compute_block_audio_saliency(
1479
+ audio_norms_block_order, chunk_size_k, valid_mask=kv_valid_mask
1480
+ )
1481
+ else:
1482
+ block_saliency = None
1483
+ audio_norms_block_order = None
1484
+ if audio_gate_value is None:
1485
+ audio_gate_value = torch.ones(1, device=q.device, dtype=q.dtype)
1486
+ if audio_boost_gamma is None:
1487
+ audio_boost_gamma = torch.zeros(1, device=q.device, dtype=q.dtype)
1488
+
1489
+ output = flash_attn_bsa_audio_guided(
1490
+ q, k, v, chunk_size_q, chunk_size_k, sparsity, cdf_threshold,
1491
+ 1 / head_dim**0.5,
1492
+ block_saliency, audio_gate_value, audio_boost_gamma,
1493
+ kv_valid_mask,
1494
+ None, # q_valid_mask (same grid in self-attn)
1495
+ audio_norms_block_order if audio_weighted_lambda > 0 else None,
1496
+ audio_weighted_lambda,
1497
+ score_modifier,
1498
+ )
1499
+
1500
+ output = rearrange_3d_block_to_THW(output, Ntq, Nhq, Nwq, tq, hq, wq, output.shape[-1])
1501
+ return output
1502
+
1503
+
hymm/models/modules/block_sparse_attention/common.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import triton
2
+ import triton.language as tl
3
+ import os
4
+
5
+ if os.environ.get('TRITON_AUTOTUNE_ENBALE', '0') == '1':
6
+ autotune = triton.autotune
7
+ else:
8
+ def autotune(*args, **kwargs):
9
+ def decorator(func):
10
+ return func
11
+ return decorator
12
+
13
+ configs_gating_preset = {
14
+ 'default': {
15
+ 'BLOCK_M': 64,
16
+ 'BLOCK_N': 64,
17
+ 'num_stages': 3,
18
+ 'num_warps': 8,
19
+ }
20
+ }
21
+
22
+ configs_gating = [
23
+ triton.Config({'BLOCK_M': BM, 'BLOCK_N': BN}, num_stages=s, num_warps=w) \
24
+ for BM in [64, 128] \
25
+ for BN in [32, 64] \
26
+ for s in [2, 3, 4, 5] \
27
+ for w in [4, 8] \
28
+ ]
29
+
30
+ gating_reevaluate_keys = ["M", "N"] if os.environ.get('TRITON_REEVALUATE_KEY', '0') == '1' else []
31
+ @autotune(configs_gating, key=gating_reevaluate_keys)
32
+ @triton.jit
33
+ def _attn_fwd_gating(
34
+ Q, K, Out,
35
+ stride_qz, stride_qh, stride_qm, stride_qk,
36
+ stride_kz, stride_kh, stride_kn, stride_kk,
37
+ stride_oz, stride_oh, stride_om, stride_on,
38
+ H, M, N,
39
+ HEAD_DIM: tl.constexpr,
40
+ BLOCK_M: tl.constexpr,
41
+ BLOCK_N: tl.constexpr,
42
+ ):
43
+
44
+ tl.static_assert(BLOCK_N <= HEAD_DIM)
45
+ start_m = tl.program_id(0)
46
+ off_hz = tl.program_id(1)
47
+ off_z = off_hz // H
48
+ off_h = off_hz % H
49
+ q_offset = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh
50
+ k_offset = off_z.to(tl.int64) * stride_kz + off_h.to(tl.int64) * stride_kh
51
+ o_offset = off_z.to(tl.int64) * stride_oz + off_h.to(tl.int64) * stride_oh
52
+
53
+ # block pointers
54
+ Q_block_ptr = tl.make_block_ptr(
55
+ base=Q + q_offset,
56
+ shape=(M, HEAD_DIM),
57
+ strides=(stride_qm, stride_qk),
58
+ offsets=(start_m * BLOCK_M, 0),
59
+ block_shape=(BLOCK_M, HEAD_DIM),
60
+ order=(1, 0),
61
+ )
62
+
63
+ K_block_ptr = tl.make_block_ptr(
64
+ base=K + k_offset,
65
+ shape=(HEAD_DIM, N),
66
+ strides=(stride_kk, stride_kn),
67
+ offsets=(0, 0),
68
+ block_shape=(HEAD_DIM, BLOCK_N),
69
+ order=(0, 1),
70
+ )
71
+ O_block_ptr = tl.make_block_ptr(
72
+ base=Out + o_offset,
73
+ shape=(M, N),
74
+ strides=(stride_om, stride_on),
75
+ offsets=(start_m * BLOCK_M, 0),
76
+ block_shape=(BLOCK_M, BLOCK_N),
77
+ order=(1, 0),
78
+ )
79
+
80
+ # load q: it will stay in SRAM throughout
81
+ q = tl.load(Q_block_ptr, boundary_check=(0,))
82
+ for start_n in range(0, N, BLOCK_N):
83
+ start_n = tl.multiple_of(start_n, BLOCK_N)
84
+ # -- compute qk ----
85
+ k = tl.load(K_block_ptr, boundary_check=(1,))
86
+ qk = tl.dot(q, k)
87
+
88
+ tl.store(O_block_ptr, qk.to(Out.type.element_ty), boundary_check=(0, 1))
89
+
90
+ K_block_ptr = tl.advance(K_block_ptr, (0, BLOCK_N))
91
+ O_block_ptr = tl.advance(O_block_ptr, (0, BLOCK_N))
92
+
93
+
94
+ @triton.jit
95
+ def _attn_bwd_preprocess(
96
+ O, DO,
97
+ Delta, # output
98
+ N_CTX,
99
+ BLOCK_M: tl.constexpr,
100
+ HEAD_DIM: tl.constexpr
101
+ ):
102
+
103
+ off_m = tl.program_id(0) * BLOCK_M + tl.arange(0, BLOCK_M)
104
+ off_hz = tl.program_id(1)
105
+ off_n = tl.arange(0, HEAD_DIM)
106
+ # load
107
+ o = tl.load(O + off_hz * HEAD_DIM * N_CTX + off_m[:, None] * HEAD_DIM + off_n[None, :])
108
+ do = tl.load(DO + off_hz * HEAD_DIM * N_CTX + off_m[:, None] * HEAD_DIM + off_n[None, :]).to(tl.float32)
109
+ delta = tl.sum(o * do, axis=1)
110
+ # write-back
111
+ tl.store(Delta + off_hz * N_CTX + off_m, delta)
hymm/models/modules/block_sparse_attention/dynamic_block_attention.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from typing import Optional, Tuple
3
+
4
+ import torch
5
+
6
+ # NOTE: `bsa_interface` pulls in Triton. We import its symbols lazily (inside the
7
+ # functions that launch kernels) so that the pure-PyTorch geometry helpers in
8
+ # this module remain importable / unit-testable on machines without Triton.
9
+ from .dynamic_block_shape import (
10
+ ALL_CANDIDATE_SHAPES,
11
+ ZONE_SIZE,
12
+ select_zone_shapes,
13
+ )
14
+
15
+ _TILE = 64 # kernel tile size (Section 8.7: unified BLOCK_N=64)
16
+
17
+
18
+ # =====================================================================
19
+ # Per-shape local permutation precompute (constant; cached per device).
20
+ # =====================================================================
21
+ def _precompute_local_orders(device):
22
+ """For each candidate shape, the [512] permutation mapping a rearranged
23
+ within-zone position -> local flat coord (lt*64 + lh*8 + lw).
24
+
25
+ Also returns, per shape, (product, tiles_per_block). All cached.
26
+ """
27
+ cache = _precompute_local_orders._cache
28
+ if device in cache:
29
+ return cache[device]
30
+
31
+ zs = ZONE_SIZE
32
+ n = zs * zs * zs # 512
33
+ lf = torch.arange(n, device=device)
34
+ lt = lf // (zs * zs)
35
+ lh = (lf // zs) % zs
36
+ lw = lf % zs
37
+
38
+ orders = []
39
+ prods = []
40
+ tpb = []
41
+ for (bt, bh, bw) in ALL_CANDIDATE_SHAPES:
42
+ bsz = bt * bh * bw
43
+ Nh, Nw = zs // bh, zs // bw
44
+ nt, it = lt // bt, lt % bt
45
+ nh, ih = lh // bh, lh % bh
46
+ nw, iw = lw // bw, lw % bw
47
+ blk = (nt * Nh + nh) * Nw + nw # block index within zone
48
+ off = (it * bh + ih) * bw + iw # offset within block
49
+ rearr_pos = blk * bsz + off # [512]: local_flat -> rearr pos
50
+ order = torch.empty(n, dtype=torch.long, device=device)
51
+ order[rearr_pos] = lf # invert -> rearr pos -> local_flat
52
+ orders.append(order)
53
+ prods.append(bsz)
54
+ tpb.append(bsz // _TILE)
55
+
56
+ local_orders = torch.stack(orders, dim=0) # [C, 512]
57
+ products = torch.tensor(prods, dtype=torch.long, device=device) # [C]
58
+ tiles_per_block = torch.tensor(tpb, dtype=torch.long, device=device) # [C]
59
+ cache[device] = (local_orders, products, tiles_per_block)
60
+ return cache[device]
61
+
62
+
63
+ _precompute_local_orders._cache = {}
64
+
65
+
66
+ # =====================================================================
67
+ # Pure BSA Triton wrapper (forward + backward through the kernel only).
68
+ # Mirrors bias_rectification._bsa_kernel_only but lives here to keep the
69
+ # dynamic path fully isolated. Selection / pooling stay in PyTorch outside.
70
+ # =====================================================================
71
+ class _dyn_bsa_kernel(torch.autograd.Function):
72
+ @staticmethod
73
+ def forward(ctx, q, k, v, sm_scale, block_indices, block_indices_lens,
74
+ chunk_size_q, chunk_size_k, sparsity, kv_valid_mask):
75
+ from .bsa_interface import attn_fwd_bsa_varlen_triton
76
+ o, lse = attn_fwd_bsa_varlen_triton(
77
+ q, k, v, sm_scale, block_indices, block_indices_lens,
78
+ chunk_size_q, chunk_size_k, sparsity, kv_valid_mask=kv_valid_mask,
79
+ )
80
+ ctx.save_for_backward(q, k, v, o, lse, block_indices, block_indices_lens)
81
+ ctx.sm_scale = sm_scale
82
+ ctx.chunk_size_q = chunk_size_q
83
+ ctx.chunk_size_k = chunk_size_k
84
+ ctx.sparsity = sparsity
85
+ ctx.kv_valid_mask = kv_valid_mask
86
+ return o
87
+
88
+ @staticmethod
89
+ def backward(ctx, do):
90
+ from .bsa_interface import attn_bwd_bsa_varlen_triton
91
+ q, k, v, o, lse, block_indices, block_indices_lens = ctx.saved_tensors
92
+ dq = torch.empty_like(q)
93
+ dk = torch.empty_like(k)
94
+ dv = torch.empty_like(v)
95
+ attn_bwd_bsa_varlen_triton(
96
+ do.contiguous(), q, k, v, o, dq, dk, dv,
97
+ ctx.sm_scale, lse, block_indices, block_indices_lens,
98
+ ctx.chunk_size_q, ctx.chunk_size_k, ctx.sparsity,
99
+ kv_valid_mask=ctx.kv_valid_mask,
100
+ )
101
+ return dq, dk, dv, None, None, None, None, None, None, None
102
+
103
+
104
+ # =====================================================================
105
+ # Geometry helpers (no grad).
106
+ # =====================================================================
107
+ def _build_rearrange_index(shape_ids, ZT, ZH, ZW, H, W, device):
108
+ """Return src_idx[S], inv_idx[S] for the per-zone anisotropic rearrange.
109
+
110
+ src_idx[rearr_pos] = original_thw_index ; inv_idx = its inverse permutation.
111
+ Both contiguous int64.
112
+ """
113
+ zs = ZONE_SIZE
114
+ Nz = ZT * ZH * ZW
115
+ local_orders, _, _ = _precompute_local_orders(device)
116
+ lo = local_orders[shape_ids] # [Nz, 512] local flat per rearr pos
117
+ lt = lo // (zs * zs)
118
+ lh = (lo // zs) % zs
119
+ lw = lo % zs
120
+
121
+ zidx = torch.arange(Nz, device=device)
122
+ zt = zidx // (ZH * ZW)
123
+ zh = (zidx // ZW) % ZH
124
+ zw = zidx % ZW
125
+ gt = zt[:, None] * zs + lt
126
+ gh = zh[:, None] * zs + lh
127
+ gw = zw[:, None] * zs + lw
128
+ global_thw = (gt * H + gh) * W + gw # [Nz, 512]
129
+ src_idx = global_thw.reshape(-1).contiguous() # [S]
130
+ # src_idx is a permutation, so its inverse is an O(S) scatter (cheaper than
131
+ # an O(S log S) argsort): inv_idx[src_idx[r]] = r.
132
+ S = src_idx.shape[0]
133
+ inv_idx = torch.empty_like(src_idx)
134
+ inv_idx[src_idx] = torch.arange(S, device=device)
135
+ return src_idx, inv_idx
136
+
137
+
138
+ def _build_block_tile_maps(shape_ids, ZT, ZH, ZW, device):
139
+ """Logical-block <-> tile bookkeeping for the rearranged sequence.
140
+
141
+ Returns:
142
+ block_id_of_tile [N_tiles] : logical-block id of each 64-tile
143
+ kblock_tiles [N_b+1, 2] : the (up-to-2) tile ids of each logical block
144
+ (row N_b is the sentinel = N_tiles, used for
145
+ invalid/sentinel selection slots)
146
+ N_b (int), N_tiles (int)
147
+ """
148
+ Nz = ZT * ZH * ZW
149
+ _, products, tiles_per_block = _precompute_local_orders(device)
150
+ tpb = tiles_per_block[shape_ids] # [Nz] in {1,2}
151
+ nbz = (512 // products[shape_ids]) # blocks per zone [Nz] in {8,4}
152
+ N_tiles = Nz * (512 // _TILE) # = Nz * 8
153
+ N_b = int(nbz.sum().item())
154
+
155
+ # exclusive cumsum of blocks-per-zone -> per-zone block offset
156
+ block_offset = torch.zeros(Nz, dtype=torch.long, device=device)
157
+ if Nz > 1:
158
+ block_offset[1:] = torch.cumsum(nbz, dim=0)[:-1]
159
+
160
+ tile_g = torch.arange(N_tiles, device=device)
161
+ z_of_tile = tile_g // 8
162
+ tl_local = tile_g % 8
163
+ tpb_t = tpb[z_of_tile]
164
+ block_local = tl_local // tpb_t
165
+ pos_in_block = tl_local % tpb_t
166
+ block_id_of_tile = block_offset[z_of_tile] + block_local # [N_tiles]
167
+
168
+ kblock_tiles = torch.full((N_b + 1, 2), N_tiles, dtype=torch.long, device=device)
169
+ kblock_tiles[block_id_of_tile, pos_in_block] = tile_g
170
+ return block_id_of_tile, kblock_tiles, N_b, N_tiles
171
+
172
+
173
+ def _logical_block_means(x_tile, tile_counts, block_id_of_tile, N_b):
174
+ """Aggregate masked tile-means -> logical-block masked means.
175
+
176
+ block_mean = sum(tile_mean * tile_valid_count) / sum(tile_valid_count),
177
+ correctly averaging only valid tokens across the 1-or-2 tiles of a block.
178
+
179
+ x_tile: [B,H,N_tiles,D]; tile_counts: [N_tiles]; returns ([B,H,N_b,D], block_count[N_b]).
180
+ """
181
+ B, Hn, N_tiles, D = x_tile.shape
182
+ w = x_tile * tile_counts.view(1, 1, N_tiles, 1).to(x_tile.dtype)
183
+ block_sum = x_tile.new_zeros(B, Hn, N_b, D)
184
+ block_sum.index_add_(2, block_id_of_tile, w)
185
+ block_count = tile_counts.new_zeros(N_b)
186
+ block_count.index_add_(0, block_id_of_tile, tile_counts.to(block_count.dtype))
187
+ block_mean = block_sum / block_count.clamp(min=1.0).view(1, 1, N_b, 1).to(x_tile.dtype)
188
+ return block_mean, block_count
189
+
190
+
191
+ # =====================================================================
192
+ # Public entry point.
193
+ # =====================================================================
194
+ def flash_attn_bsa_3d_dynamic(
195
+ q: torch.Tensor, # [B, H, S, D] THW order (already padded to mult of ZONE_SIZE)
196
+ k: torch.Tensor,
197
+ v: torch.Tensor,
198
+ grid_padded: Tuple[int, int, int],
199
+ *,
200
+ method: str, # "ivpq" or "penalty"
201
+ sparsity: Optional[float] = 0.9375,
202
+ cdf_threshold: Optional[float] = None,
203
+ audio_token_norms: Optional[torch.Tensor] = None, # [S] THW order, or None
204
+ valid_mask: Optional[torch.Tensor] = None, # [S] bool THW order, or None
205
+ lambda_a: float = 0.5,
206
+ tau_128: float = 0.15,
207
+ lambda_128: float = 1.0,
208
+ shape_ids: Optional[torch.Tensor] = None,
209
+ return_shape_ids: bool = False,
210
+ ) -> torch.Tensor:
211
+ """Dynamic-block-shape BSA for video self-attention. Returns [B,H,S,D] THW order.
212
+
213
+ shape_ids: if given, skip the (non-deterministic) shape selection and use it
214
+ verbatim — used to keep the checkpoint recompute identical to the forward.
215
+ return_shape_ids: if True, also return the shape_ids used (for caching).
216
+ """
217
+ from .bsa_interface import (
218
+ mean_pooling_compression,
219
+ masked_mean_pooling_compression,
220
+ cal_score,
221
+ get_select_indices_topk_from_score,
222
+ get_select_indices_cdf_from_score,
223
+ get_select_indices_cdf_topk_from_score,
224
+ )
225
+ B, Hn, S, D = q.shape
226
+ T, H, W = grid_padded
227
+ assert T * H * W == S
228
+ assert T % ZONE_SIZE == 0 and H % ZONE_SIZE == 0 and W % ZONE_SIZE == 0
229
+ device = q.device
230
+ sm_scale = 1.0 / (D ** 0.5)
231
+ ZT, ZH, ZW = T // ZONE_SIZE, H // ZONE_SIZE, W // ZONE_SIZE
232
+
233
+ # ---- (1) per-zone shape decision (head-agnostic, SP-consistent) ----
234
+ with torch.no_grad():
235
+ if shape_ids is None:
236
+ v_headavg = v[0].mean(dim=0).view(T, H, W, D) # [T,H,W,D]
237
+ shape_ids = select_zone_shapes(
238
+ v_headavg, audio_token_norms, grid_padded, method,
239
+ lambda_a=lambda_a, tau_128=tau_128, lambda_128=lambda_128,
240
+ valid_mask_thw=valid_mask, sp_reduce=True,
241
+ ) # [Nz] int64
242
+
243
+ # ---- (2) build the rearrange + tile/block bookkeeping ----
244
+ src_idx, inv_idx = _build_rearrange_index(shape_ids, ZT, ZH, ZW, H, W, device)
245
+ block_id_of_tile, kblock_tiles, N_b, N_tiles = _build_block_tile_maps(
246
+ shape_ids, ZT, ZH, ZW, device
247
+ )
248
+
249
+ # ---- (3) rearrange q/k/v into the tile-contiguous layout (differentiable) ----
250
+ q_re = q.index_select(2, src_idx).contiguous()
251
+ k_re = k.index_select(2, src_idx).contiguous()
252
+ v_re = v.index_select(2, src_idx).contiguous()
253
+ valid_re = valid_mask[src_idx].contiguous() if valid_mask is not None else None
254
+
255
+ # ---- (4) logical-block scoring + selection (no grad — selection only) ----
256
+ with torch.no_grad():
257
+ if valid_re is not None:
258
+ q_tile = masked_mean_pooling_compression(q_re, _TILE, valid_re)
259
+ k_tile = masked_mean_pooling_compression(k_re, _TILE, valid_re)
260
+ tile_counts = valid_re.view(N_tiles, _TILE).sum(dim=-1).float()
261
+ else:
262
+ q_tile = mean_pooling_compression(q_re, _TILE)
263
+ k_tile = mean_pooling_compression(k_re, _TILE)
264
+ tile_counts = torch.full((N_tiles,), float(_TILE), device=device)
265
+
266
+ q_cmp, _ = _logical_block_means(q_tile, tile_counts, block_id_of_tile, N_b)
267
+ k_cmp, block_count = _logical_block_means(k_tile, tile_counts, block_id_of_tile, N_b)
268
+ block_valid = block_count > 0 # [N_b]
269
+ del q_tile, k_tile
270
+
271
+ has_invalid = not bool(block_valid.all())
272
+ invalid_mask = ~block_valid.view(1, 1, 1, -1) if has_invalid else None
273
+ del block_valid, block_count
274
+
275
+ sel_blocks_list = []
276
+ for _h in range(Hn):
277
+ score_h = cal_score(q_cmp[:, _h:_h+1], k_cmp[:, _h:_h+1])
278
+ if invalid_mask is not None:
279
+ score_h = score_h.masked_fill(invalid_mask, float('-inf'))
280
+ if sparsity is not None and cdf_threshold is None:
281
+ sel_h, _ = get_select_indices_topk_from_score(score_h, sparsity)
282
+ elif sparsity is None and cdf_threshold is not None:
283
+ sel_h, _ = get_select_indices_cdf_from_score(score_h, cdf_threshold, sm_scale)
284
+ elif sparsity is not None and cdf_threshold is not None:
285
+ sel_h, _ = get_select_indices_cdf_topk_from_score(score_h, sparsity, cdf_threshold, sm_scale)
286
+ else:
287
+ raise ValueError("Either sparsity or cdf_threshold must be provided")
288
+ sel_blocks_list.append(sel_h)
289
+ del score_h, sel_h
290
+ del q_cmp, k_cmp, invalid_mask
291
+
292
+ sel_blocks = torch.cat(sel_blocks_list, dim=1)
293
+ del sel_blocks_list
294
+ # sel_blocks values in [0, N_b]; N_b is the sentinel (cdf/varlen padding).
295
+ sel_blocks = sel_blocks.clamp(max=N_b) # safety (int64 index)
296
+
297
+ # ---- (5) expand selected logical blocks -> tile-level indices ----
298
+ kblock_tiles = kblock_tiles.to(torch.int32)
299
+ t0 = kblock_tiles[:, 0][sel_blocks] # [B,H,N_bq,r] int32
300
+ t1 = kblock_tiles[:, 1][sel_blocks]
301
+ del sel_blocks, kblock_tiles
302
+ tile_sel = torch.cat([t0, t1], dim=-1) # [B,H,N_bq,2r] int32; values in [0, N_tiles]
303
+ del t0, t1
304
+
305
+ if valid_re is not None:
306
+ tile_is_valid = tile_counts > 0 # [N_tiles] bool
307
+ in_range = tile_sel < N_tiles
308
+ sel_valid = in_range & tile_is_valid[tile_sel.clamp(max=N_tiles - 1)]
309
+ tile_sel = torch.where(sel_valid, tile_sel, torch.full_like(tile_sel, N_tiles))
310
+ del tile_counts
311
+
312
+ tile_sel, _ = torch.sort(tile_sel, dim=-1) # int32 sort; valid first, sentinel(N_tiles) at tail
313
+ tile_lens_block = (tile_sel < N_tiles).sum(dim=-1).to(torch.int32)
314
+
315
+ # map q-block rows -> q-tile rows (a 128 q-block shares its row across its 2 tiles).
316
+ # tile_sel is already int32, so index_select yields the int32 block indices directly.
317
+ tile_block_indices = tile_sel.index_select(2, block_id_of_tile).contiguous()
318
+ tile_block_indices_lens = tile_lens_block.index_select(2, block_id_of_tile).contiguous()
319
+ del tile_sel, tile_lens_block, block_id_of_tile
320
+
321
+ # ---- (6) BSA Triton kernel at chunk=64 (autograd through q_re/k_re/v_re) ----
322
+ o_re = _dyn_bsa_kernel.apply(
323
+ q_re, k_re, v_re, sm_scale,
324
+ tile_block_indices, tile_block_indices_lens,
325
+ _TILE, _TILE, sparsity, valid_re,
326
+ )
327
+
328
+ # ---- (7) scatter back to original THW order ----
329
+ out = o_re.index_select(2, inv_idx).contiguous()
330
+ if return_shape_ids:
331
+ return out, shape_ids
332
+ return out
333
+
334
+
335
+ __all__ = ["flash_attn_bsa_3d_dynamic"]
hymm/models/modules/block_sparse_attention/dynamic_block_shape.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from typing import List, Optional, Tuple
3
+
4
+ import torch
5
+
6
+ CANDIDATE_SHAPES_64: List[Tuple[int, int, int]] = [
7
+ (4, 4, 4), # isotropic, default
8
+ (8, 2, 4), # long-T, narrow-H (lip/face)
9
+ (8, 4, 2), # long-T, narrow-W
10
+ (2, 4, 8), # short-T, wide-W (fast motion)
11
+ (2, 8, 4), # short-T, wide-H
12
+ (4, 2, 8), # mid-T, narrow-H wide-W
13
+ (4, 8, 2), # mid-T, wide-H narrow-W
14
+ ]
15
+
16
+ CANDIDATE_SHAPES_128: List[Tuple[int, int, int]] = [
17
+ (2, 8, 8), # thin-T, large spatial
18
+ (8, 2, 8), # long-T, narrow-H wide-W
19
+ (8, 8, 2), # long-T, wide-H narrow-W
20
+ (4, 4, 8), # mid-T, wide-W
21
+ (4, 8, 4), # mid-T, wide-H
22
+ (8, 4, 4), # long-T, mid spatial (static sky)
23
+ ]
24
+
25
+ ALL_CANDIDATE_SHAPES: List[Tuple[int, int, int]] = (
26
+ CANDIDATE_SHAPES_64 + CANDIDATE_SHAPES_128
27
+ )
28
+ NUM_CANDIDATES_64 = len(CANDIDATE_SHAPES_64)
29
+ NUM_CANDIDATES = len(ALL_CANDIDATE_SHAPES)
30
+
31
+ ZONE_SIZE = 8 # macro-zone edge length (Section 8.4.1); lcm(2,4,8)
32
+
33
+ _EPS = 1e-6
34
+
35
+
36
+ def candidate_tensors(device, dtype=torch.float32):
37
+ """Return (shapes[NUM_CANDIDATES,3], log2_shapes[NUM_CANDIDATES,3],
38
+ is_128[NUM_CANDIDATES], product[NUM_CANDIDATES]) on `device`.
39
+
40
+ Cached per (device, dtype) to avoid re-allocation each layer/step.
41
+ """
42
+ key = (device, dtype)
43
+ cache = candidate_tensors._cache
44
+ if key not in cache:
45
+ shapes = torch.tensor(ALL_CANDIDATE_SHAPES, device=device, dtype=dtype) # [C,3]
46
+ log2_shapes = torch.log2(shapes)
47
+ prod = shapes.prod(dim=-1)
48
+ is_128 = prod > 64.5
49
+ cache[key] = (shapes, log2_shapes, is_128, prod)
50
+ return cache[key]
51
+
52
+
53
+ candidate_tensors._cache = {}
54
+
55
+ def _zone_reshape(x_thw_d: torch.Tensor, ZT: int, ZH: int, ZW: int) -> torch.Tensor:
56
+ """[T,H,W,D] -> [num_zones, ZS,ZS,ZS, D] with zone order zt-major.
57
+
58
+ Memory-contiguous output (one permute+contiguous). T,H,W must be multiples
59
+ of ZONE_SIZE.
60
+ """
61
+ T, H, W, D = x_thw_d.shape
62
+ zs = ZONE_SIZE
63
+ x = x_thw_d.reshape(ZT, zs, ZH, zs, ZW, zs, D)
64
+ x = x.permute(0, 2, 4, 1, 3, 5, 6).contiguous() # [ZT,ZH,ZW, zs,zs,zs, D]
65
+ return x.view(ZT * ZH * ZW, zs, zs, zs, D)
66
+
67
+
68
+ def compute_zone_aggregates(
69
+ v_headavg_thw_d: torch.Tensor,
70
+ ZT: int, ZH: int, ZW: int,
71
+ valid_mask_thw: Optional[torch.Tensor] = None,
72
+ ):
73
+ """Compute the per-zone *masked mean* aggregates that are linear in V.
74
+
75
+ Padded tokens (valid_mask == False) are excluded from every mean. Because
76
+ the valid mask is identical on every SP rank (it is a token-level grid
77
+ mask, not a head shard), the per-zone valid token *counts* are
78
+ rank-independent — so a masked-mean computed per rank and then AVG-all-
79
+ reduced across the SP group yields the exact global (head- and token-)
80
+ masked mean. This keeps the SP reduction a single cheap collective.
81
+
82
+ Returns (packed[num_zones, D + 3*ZS*D], frame_valid[num_zones, ZS]).
83
+ Packed last-dim layout:
84
+ [ 0 : D ] channel_means (masked mean over all tokens)
85
+ [ D : D + ZS*D ] frame_means_T (masked mean over H,W per t)
86
+ [ D + ZS*D : D + 2ZS*D] axis_means_H (masked mean over T,W per h)
87
+ [ D +2ZS*D : D + 3ZS*D] axis_means_W (masked mean over T,H per w)
88
+ `frame_valid[z,t]` marks t-frames that contain >=1 valid token (used to
89
+ exclude padded frames from the temporal-variance diff downstream).
90
+ """
91
+ zs = ZONE_SIZE
92
+ D = v_headavg_thw_d.shape[-1]
93
+ xz = _zone_reshape(v_headavg_thw_d, ZT, ZH, ZW).float() # [Nz, t,h,w, D]
94
+ Nz = xz.shape[0]
95
+
96
+ if valid_mask_thw is not None:
97
+ mz = _zone_reshape(valid_mask_thw.reshape(*v_headavg_thw_d.shape[:3], 1).float(),
98
+ ZT, ZH, ZW) # [Nz, t,h,w, 1]
99
+ c_all = mz.sum(dim=(1, 2, 3)).clamp(min=1.0) # [Nz,1]
100
+ channel_means = (xz * mz).sum(dim=(1, 2, 3)) / c_all
101
+ cT = mz.sum(dim=(2, 3)).clamp(min=1.0) # [Nz, t, 1]
102
+ frame_means_T = (xz * mz).sum(dim=(2, 3)) / cT
103
+ cH = mz.sum(dim=(1, 3)).clamp(min=1.0)
104
+ axis_means_H = (xz * mz).sum(dim=(1, 3)) / cH
105
+ cW = mz.sum(dim=(1, 2)).clamp(min=1.0)
106
+ axis_means_W = (xz * mz).sum(dim=(1, 2)) / cW
107
+ m = mz.squeeze(-1)
108
+ t_valid = m.sum(dim=(2, 3)) > 0 # [Nz, t]
109
+ h_valid = m.sum(dim=(1, 3)) > 0 # [Nz, h]
110
+ w_valid = m.sum(dim=(1, 2)) > 0 # [Nz, w]
111
+ axis_valid = (t_valid, h_valid, w_valid)
112
+ else:
113
+ channel_means = xz.mean(dim=(1, 2, 3)) # [Nz, D]
114
+ frame_means_T = xz.mean(dim=(2, 3)) # [Nz, t, D]
115
+ axis_means_H = xz.mean(dim=(1, 3)) # [Nz, h, D]
116
+ axis_means_W = xz.mean(dim=(1, 2)) # [Nz, w, D]
117
+ ones = torch.ones(Nz, zs, dtype=torch.bool, device=xz.device)
118
+ axis_valid = (ones, ones, ones)
119
+
120
+ packed = torch.cat(
121
+ [
122
+ channel_means,
123
+ frame_means_T.reshape(Nz, zs * D),
124
+ axis_means_H.reshape(Nz, zs * D),
125
+ axis_means_W.reshape(Nz, zs * D),
126
+ ],
127
+ dim=-1,
128
+ ).contiguous()
129
+ return packed, axis_valid
130
+
131
+
132
+ def _maybe_sp_average(packed: torch.Tensor) -> torch.Tensor:
133
+ """AVG-all-reduce the packed aggregates across the SP group (no-op if SP off).
134
+
135
+ Mirrors the FSDP/SP convention used by bias_rectification: only the SP path
136
+ splits heads across ranks, so the head-agnostic shape decision needs this
137
+ reduction to be globally consistent. FSDP-only (no SP) sees identical data
138
+ on all ranks already → no collective.
139
+ """
140
+ # Not wrapped in try/except on purpose: a swallowed import error would let one
141
+ # rank return early while the rest block inside the all_reduce, turning a
142
+ # loud failure into a silent NCCL hang.
143
+ import torch.distributed as dist
144
+ from hymm.utils.parallel_states import nccl_info, get_sequence_parallel_state
145
+
146
+ if (not get_sequence_parallel_state()) or nccl_info.sp_size <= 1:
147
+ return packed
148
+ out = packed.contiguous()
149
+ dist.all_reduce(out, op=dist.ReduceOp.SUM, group=nccl_info.sp_group)
150
+ out /= float(nccl_info.sp_size)
151
+ return out
152
+
153
+
154
+ def _unpack_aggregates(packed: torch.Tensor, D: int):
155
+ zs = ZONE_SIZE
156
+ Nz = packed.shape[0]
157
+ channel_means = packed[:, :D]
158
+ off = D
159
+ frame_means_T = packed[:, off:off + zs * D].reshape(Nz, zs, D); off += zs * D
160
+ axis_means_H = packed[:, off:off + zs * D].reshape(Nz, zs, D); off += zs * D
161
+ axis_means_W = packed[:, off:off + zs * D].reshape(Nz, zs, D); off += zs * D
162
+ return channel_means, frame_means_T, axis_means_H, axis_means_W
163
+
164
+
165
+ def _axis_var(axis_means: torch.Tensor, valid: Optional[torch.Tensor] = None) -> torch.Tensor:
166
+ """Variance across the ZS axis positions, averaged over channels. [Nz,ZS,D]->[Nz].
167
+
168
+ If `valid` [Nz,ZS] is given, only valid positions contribute (padded
169
+ axis positions are excluded so they cannot fabricate variance).
170
+ """
171
+ if valid is None:
172
+ var_per_channel = axis_means.var(dim=1, unbiased=False) # [Nz, D]
173
+ return var_per_channel.mean(dim=-1) # [Nz]
174
+ vf = valid.unsqueeze(-1).to(axis_means.dtype) # [Nz,ZS,1]
175
+ n = vf.sum(dim=1).clamp(min=1.0) # [Nz,1]
176
+ mean = (axis_means * vf).sum(dim=1, keepdim=True) / n.unsqueeze(1)
177
+ var_per_channel = ((axis_means - mean) ** 2 * vf).sum(dim=1) / n
178
+ return var_per_channel.mean(dim=-1) # [Nz]
179
+
180
+
181
+ def _masked_var_1d(vals: torch.Tensor, valid: torch.Tensor) -> torch.Tensor:
182
+ """Variance over the last axis restricted to valid positions. [Nz,L]->[Nz]."""
183
+ vf = valid.to(vals.dtype)
184
+ n = vf.sum(dim=-1).clamp(min=1.0)
185
+ mean = (vals * vf).sum(dim=-1) / n
186
+ return ((vals - mean.unsqueeze(-1)) ** 2 * vf).sum(dim=-1) / n
187
+
188
+
189
+ def compute_audio_directional(
190
+ audio_norms_thw: Optional[torch.Tensor],
191
+ ZT: int, ZH: int, ZW: int,
192
+ valid_mask_thw: Optional[torch.Tensor] = None,
193
+ device: Optional[torch.device] = None,
194
+ ):
195
+ """Per-zone audio descriptor (Section 8.4.3).
196
+
197
+ Returns (a_bar[Nz], v_hat_T[Nz], v_hat_H[Nz], v_hat_W[Nz]) where a_bar is
198
+ the (max-normalized) mean audio-absorption and v_hat_d is the normalized
199
+ directional variance along axis d. All in float32.
200
+
201
+ `audio_norms_thw` is identical on every SP rank (all-gathered upstream), so
202
+ no collective is needed here.
203
+ """
204
+ zs = ZONE_SIZE
205
+ Nz = ZT * ZH * ZW
206
+ if device is None:
207
+ device = (audio_norms_thw.device if audio_norms_thw is not None
208
+ else (valid_mask_thw.device if valid_mask_thw is not None else 'cpu'))
209
+ if audio_norms_thw is None:
210
+ z = torch.zeros(Nz, device=device, dtype=torch.float32)
211
+ return z, z.clone(), z.clone(), z.clone()
212
+
213
+ a = audio_norms_thw.reshape(ZT, zs, ZH, zs, ZW, zs).float()
214
+ a = a.permute(0, 2, 4, 1, 3, 5).reshape(Nz, zs, zs, zs) # [Nz,t,h,w]
215
+
216
+ if valid_mask_thw is not None:
217
+ m = valid_mask_thw.reshape(ZT, zs, ZH, zs, ZW, zs).float()
218
+ m = m.permute(0, 2, 4, 1, 3, 5).reshape(Nz, zs, zs, zs)
219
+ cnt = m.sum(dim=(1, 2, 3)).clamp(min=1.0)
220
+ a_bar = (a * m).sum(dim=(1, 2, 3)) / cnt
221
+ # Per-axis masked mean over the other two axes, then masked variance over
222
+ # the axis. Padded positions are excluded so they cannot fabricate audio
223
+ # directional structure (mirrors the V-feature masking).
224
+ cT = m.sum(dim=(2, 3)).clamp(min=1.0); aT = (a * m).sum(dim=(2, 3)) / cT # [Nz,t]
225
+ cH = m.sum(dim=(1, 3)).clamp(min=1.0); aH = (a * m).sum(dim=(1, 3)) / cH
226
+ cW = m.sum(dim=(1, 2)).clamp(min=1.0); aW = (a * m).sum(dim=(1, 2)) / cW
227
+ t_valid = m.sum(dim=(2, 3)) > 0
228
+ h_valid = m.sum(dim=(1, 3)) > 0
229
+ w_valid = m.sum(dim=(1, 2)) > 0
230
+ var_T = _masked_var_1d(aT, t_valid)
231
+ var_H = _masked_var_1d(aH, h_valid)
232
+ var_W = _masked_var_1d(aW, w_valid)
233
+ else:
234
+ a_bar = a.mean(dim=(1, 2, 3))
235
+ var_T = a.mean(dim=(2, 3)).var(dim=1, unbiased=False) # [Nz]
236
+ var_H = a.mean(dim=(1, 3)).var(dim=1, unbiased=False)
237
+ var_W = a.mean(dim=(1, 2)).var(dim=1, unbiased=False)
238
+
239
+ denom = (var_T + var_H + var_W).clamp(min=_EPS)
240
+ v_hat_T = var_T / denom
241
+ v_hat_H = var_H / denom
242
+ v_hat_W = var_W / denom
243
+
244
+ a_bar_norm = a_bar / a_bar.max().clamp(min=_EPS)
245
+ return a_bar_norm, v_hat_T, v_hat_H, v_hat_W
246
+
247
+
248
+ def compute_effective_gradient(
249
+ v_headavg_thw_d: torch.Tensor,
250
+ audio_norms_thw: Optional[torch.Tensor],
251
+ ZT: int, ZH: int, ZW: int,
252
+ lambda_a: float = 0.5,
253
+ valid_mask_thw: Optional[torch.Tensor] = None,
254
+ sp_reduce: bool = True,
255
+ ):
256
+ """Compute (g[Nz,3], rho[Nz]) — Section 8.4.4 effective gradient + density.
257
+
258
+ g_d = aniso_ratio_d + lambda_a * a_bar_z * v_hat_d^audio (d in T,H,W)
259
+ rho = normalized (spatial + temporal) channel-variance information density.
260
+ """
261
+ D = v_headavg_thw_d.shape[-1]
262
+ packed, axis_valid = compute_zone_aggregates(
263
+ v_headavg_thw_d, ZT, ZH, ZW, valid_mask_thw=valid_mask_thw
264
+ )
265
+ if sp_reduce:
266
+ packed = _maybe_sp_average(packed)
267
+ t_valid, h_valid, w_valid = axis_valid
268
+ channel_means, frame_means_T, axis_means_H, axis_means_W = _unpack_aggregates(packed, D)
269
+
270
+ # --- channel-variance information density rho (Section 3.3 @ zone level) ---
271
+ global_cm = channel_means.mean(dim=-1, keepdim=True)
272
+ spatial_var = ((channel_means - global_cm) ** 2).mean(dim=-1) # [Nz]
273
+ # Temporal diff only between adjacent frames that are BOTH valid (mirrors
274
+ # compute_channel_variance_density) so padded frames cannot fabricate motion.
275
+ both_valid = (t_valid[:, 1:] & t_valid[:, :-1]).unsqueeze(-1).to(frame_means_T.dtype) # [Nz,ZS-1,1]
276
+ n_pairs = both_valid.sum(dim=1).clamp(min=1.0) # [Nz,1]
277
+ frame_diffs = frame_means_T[:, 1:, :] - frame_means_T[:, :-1, :] # [Nz,ZS-1,D]
278
+ temporal_var = ((frame_diffs.pow(2) * both_valid).sum(dim=1) / n_pairs).mean(dim=-1) # [Nz]
279
+ rho_raw = spatial_var + temporal_var
280
+ rho = rho_raw / rho_raw.max().clamp(min=_EPS) # [Nz] in [0,1]
281
+
282
+ # --- anisotropy ratios (Section 8.4.2), padded axis positions excluded ---
283
+ var_T = _axis_var(frame_means_T, t_valid)
284
+ var_H = _axis_var(axis_means_H, h_valid)
285
+ var_W = _axis_var(axis_means_W, w_valid)
286
+ asum = (var_T + var_H + var_W).clamp(min=_EPS)
287
+ aniso_T = var_T / asum
288
+ aniso_H = var_H / asum
289
+ aniso_W = var_W / asum
290
+
291
+ # --- audio directional guidance (Section 8.4.3) ---
292
+ a_bar, v_hat_T, v_hat_H, v_hat_W = compute_audio_directional(
293
+ audio_norms_thw, ZT, ZH, ZW, valid_mask_thw,
294
+ device=v_headavg_thw_d.device,
295
+ )
296
+
297
+ g_T = aniso_T + lambda_a * a_bar * v_hat_T
298
+ g_H = aniso_H + lambda_a * a_bar * v_hat_H
299
+ g_W = aniso_W + lambda_a * a_bar * v_hat_W
300
+ g = torch.stack([g_T, g_H, g_W], dim=-1).clamp(min=1e-4) # [Nz,3]
301
+ return g, rho
302
+
303
+ def select_shapes_ivpq(
304
+ g: torch.Tensor, # [Nz, 3]
305
+ rho: torch.Tensor, # [Nz]
306
+ tau_128: float = 0.15,
307
+ ) -> torch.Tensor:
308
+ """IVPQ (Section 8.5): b_d* ∝ g_d^{-2/3}; nearest candidate in log2-space.
309
+
310
+ The candidate pool gating (rho vs tau_128) restricts high-info zones to the
311
+ 7 64-token shapes and lets low-info zones also consider the 6 128-token
312
+ shapes (Section 8.5.3). Returns shape_id [Nz] (index into ALL_CANDIDATE_SHAPES).
313
+ """
314
+ device = g.device
315
+ _, log2_shapes, is_128, _ = candidate_tensors(device, dtype=g.dtype) # [C,3], [C]
316
+
317
+ log2_ratio = (-2.0 / 3.0) * torch.log2(g) # [Nz, 3], ratio only
318
+ log2_ideal = log2_ratio + (6.0 - log2_ratio.sum(dim=-1, keepdim=True)) / 3.0
319
+
320
+ # log-space squared distance to each candidate: [Nz, C]
321
+ dist = ((log2_shapes.unsqueeze(0) - log2_ideal.unsqueeze(1)) ** 2).sum(dim=-1)
322
+
323
+ # Pool gating: forbid 128-token candidates for high-info zones (rho >= tau_128).
324
+ forbid = (rho.unsqueeze(1) >= tau_128) & is_128.unsqueeze(0) # [Nz, C]
325
+ dist = dist.masked_fill(forbid, float('inf'))
326
+
327
+ return dist.argmin(dim=-1).to(torch.int64)
328
+
329
+
330
+ def select_shapes_penalty(
331
+ g: torch.Tensor, # [Nz, 3]
332
+ rho: torch.Tensor, # [Nz]
333
+ tau_128: float = 0.15,
334
+ lambda_128: float = 1.0,
335
+ ) -> torch.Tensor:
336
+ """Penalty Matching (Section 8.6): argmin_c sum_d g_d * b_d (+128 bonus).
337
+
338
+ 128-token candidates receive a density bonus ``-lambda_128 * (1 - rho)`` so
339
+ low-info zones can fairly select them; high-info zones (rho >= tau_128) are
340
+ still restricted to the 64-token pool. Returns shape_id [Nz].
341
+ """
342
+ device = g.device
343
+ shapes, _, is_128, _ = candidate_tensors(device, dtype=g.dtype) # [C,3], [C]
344
+
345
+ penalty = (g.unsqueeze(1) * shapes.unsqueeze(0)).sum(dim=-1) # [Nz, C]
346
+
347
+ # 128-token density bonus (lowers penalty for low-info zones).
348
+ bonus = (lambda_128 * (1.0 - rho)).unsqueeze(1) * is_128.unsqueeze(0).to(g.dtype)
349
+ penalty = penalty - bonus
350
+
351
+ # Pool gating identical to IVPQ.
352
+ forbid = (rho.unsqueeze(1) >= tau_128) & is_128.unsqueeze(0)
353
+ penalty = penalty.masked_fill(forbid, float('inf'))
354
+
355
+ return penalty.argmin(dim=-1).to(torch.int64)
356
+
357
+
358
+ def select_zone_shapes(
359
+ v_headavg_thw_d: torch.Tensor,
360
+ audio_norms_thw: Optional[torch.Tensor],
361
+ grid_padded: Tuple[int, int, int],
362
+ method: str,
363
+ lambda_a: float = 0.5,
364
+ tau_128: float = 0.15,
365
+ lambda_128: float = 1.0,
366
+ valid_mask_thw: Optional[torch.Tensor] = None,
367
+ sp_reduce: bool = True,
368
+ ) -> torch.Tensor:
369
+ """End-to-end: features -> g_d -> shape_id per zone. Returns int64 [num_zones].
370
+
371
+ Args:
372
+ v_headavg_thw_d: [T,H,W,D] head-averaged V (already padded). float ok.
373
+ audio_norms_thw: [T*H*W] per-token L2 norm of A->V bridge residual, or None.
374
+ grid_padded: (T,H,W), each a multiple of ZONE_SIZE.
375
+ method: "ivpq" or "penalty".
376
+ """
377
+ T, H, W = grid_padded
378
+ assert T % ZONE_SIZE == 0 and H % ZONE_SIZE == 0 and W % ZONE_SIZE == 0, (
379
+ f"grid {grid_padded} must be multiple of zone size {ZONE_SIZE}"
380
+ )
381
+ ZT, ZH, ZW = T // ZONE_SIZE, H // ZONE_SIZE, W // ZONE_SIZE
382
+
383
+ g, rho = compute_effective_gradient(
384
+ v_headavg_thw_d, audio_norms_thw, ZT, ZH, ZW,
385
+ lambda_a=lambda_a, valid_mask_thw=valid_mask_thw, sp_reduce=sp_reduce,
386
+ )
387
+ if method == "ivpq":
388
+ return select_shapes_ivpq(g, rho, tau_128=tau_128)
389
+ elif method == "penalty":
390
+ return select_shapes_penalty(g, rho, tau_128=tau_128, lambda_128=lambda_128)
391
+ raise ValueError(f"unknown dynamic-block method: {method!r}")
392
+
393
+
394
+ __all__ = [
395
+ "CANDIDATE_SHAPES_64",
396
+ "CANDIDATE_SHAPES_128",
397
+ "ALL_CANDIDATE_SHAPES",
398
+ "NUM_CANDIDATES_64",
399
+ "NUM_CANDIDATES",
400
+ "ZONE_SIZE",
401
+ "candidate_tensors",
402
+ "compute_effective_gradient",
403
+ "select_shapes_ivpq",
404
+ "select_shapes_penalty",
405
+ "select_zone_shapes",
406
+ ]
hymm/models/modules/block_sparse_attention/flash_attn_bsa_varlen_mask.py ADDED
@@ -0,0 +1,1014 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import triton
2
+ import triton.language as tl
3
+ import os
4
+
5
+ from .common import autotune
6
+ """
7
+ TRITON_REEVALUATE_KEY=1
8
+ - autotune whenever params in reevaluate keys change
9
+ - use in benchmark script to fine the best config
10
+
11
+ TRITON_AUTOTUNE_ENBALE=1
12
+ - if set to 0, autotune will not work, and the related params must be passed to the function call.
13
+ """
14
+
15
+ configs_fwd_bsa_varlen_preset = {
16
+ 'default': {
17
+ 'BLOCK_N': 64,
18
+ 'num_stages': 3,
19
+ 'num_warps': 8,
20
+ },
21
+ 'BLOCK_N_LG=64': {
22
+ 'BLOCK_N': 64,
23
+ 'num_stages': 3,
24
+ 'num_warps': 4,
25
+ },
26
+ }
27
+ configs_fwd_bsa_varlen = [
28
+ triton.Config({'BLOCK_N': BN}, num_stages=s, num_warps=w) \
29
+ for BN in [32, 64, 128] \
30
+ for s in [2, 3, 4, 5] \
31
+ for w in [4, 8] \
32
+ ]
33
+
34
+ fwd_bsa_reevaluate_varlen_keys = ['N_CTX', 'BLOCK_M', 'BLOCK_N_LG', 'SPARSITY'] if os.environ.get('TRITON_REEVALUATE_KEY', '0') == '1' else []
35
+ @autotune(list(configs_fwd_bsa_varlen), key=fwd_bsa_reevaluate_varlen_keys)
36
+ @triton.jit
37
+ def _attn_fwd_bsa_varlen(
38
+ Q, K, V, sm_scale, M, Out,
39
+ block_indices, # [B, H, M_COMPRESS, S_MAX]
40
+ block_indices_lens, # [B, H, M_COMPRESS]
41
+ kv_valid_mask, # [N_CTX] bool, True=valid (shared across B,H)
42
+ stride_qz, stride_qh, stride_qm, stride_qk,
43
+ stride_kz, stride_kh, stride_kn, stride_kk,
44
+ stride_vz, stride_vh, stride_vn, stride_vk,
45
+ stride_oz, stride_oh, stride_om, stride_ok,
46
+ stride_bz, stride_bh, stride_bm, stride_bs,
47
+ stride_lz, stride_lh, stride_lm,
48
+ H, N_CTX,
49
+ HEAD_DIM: tl.constexpr,
50
+ BLOCK_M: tl.constexpr,
51
+ BLOCK_N_LG: tl.constexpr,
52
+ BLOCK_N: tl.constexpr,
53
+ SPARSITY: tl.constexpr,
54
+ HAS_KV_MASK: tl.constexpr,
55
+ ):
56
+
57
+ start_m = tl.program_id(0)
58
+ off_hz = tl.program_id(1)
59
+ off_z = off_hz // H
60
+ off_h = off_hz % H
61
+
62
+ q_offset = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh
63
+ k_offset = off_z.to(tl.int64) * stride_kz + off_h.to(tl.int64) * stride_kh
64
+ v_offset = off_z.to(tl.int64) * stride_vz + off_h.to(tl.int64) * stride_vh
65
+ o_offset = off_z.to(tl.int64) * stride_oz + off_h.to(tl.int64) * stride_oh
66
+ b_offset = off_z.to(tl.int64) * stride_bz + off_h.to(tl.int64) * stride_bh
67
+ l_offset = off_z.to(tl.int64) * stride_lz + off_h.to(tl.int64) * stride_lh
68
+
69
+ # block pointers
70
+ Q_block_ptr = tl.make_block_ptr(
71
+ base=Q + q_offset,
72
+ shape=(N_CTX, HEAD_DIM),
73
+ strides=(stride_qm, stride_qk),
74
+ offsets=(start_m * BLOCK_M, 0),
75
+ block_shape=(BLOCK_M, HEAD_DIM),
76
+ order=(1, 0),
77
+ )
78
+ V_block_ptr = tl.make_block_ptr(
79
+ base=V + v_offset,
80
+ shape=(N_CTX, HEAD_DIM),
81
+ strides=(stride_vn, stride_vk),
82
+ offsets=(0, 0),
83
+ block_shape=(BLOCK_N, HEAD_DIM),
84
+ order=(1, 0),
85
+ )
86
+ KT_block_ptr = tl.make_block_ptr(
87
+ base=K + k_offset,
88
+ shape=(HEAD_DIM, N_CTX),
89
+ strides=(stride_kk, stride_kn),
90
+ offsets=(0, 0),
91
+ block_shape=(HEAD_DIM, BLOCK_N),
92
+ order=(0, 1),
93
+ )
94
+ O_block_ptr = tl.make_block_ptr(
95
+ base=Out + o_offset,
96
+ shape=(N_CTX, HEAD_DIM),
97
+ strides=(stride_om, stride_ok),
98
+ offsets=(start_m * BLOCK_M, 0),
99
+ block_shape=(BLOCK_M, HEAD_DIM),
100
+ order=(1, 0),
101
+ )
102
+ block_indices += b_offset + start_m * stride_bm
103
+ block_indices_lens += l_offset + start_m * stride_lm
104
+ # initialize offsets
105
+ offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M)
106
+
107
+ # initialize pointer to m and l
108
+ m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf")
109
+ l_i = tl.zeros([BLOCK_M], dtype=tl.float32) + 1.0
110
+ acc = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32)
111
+ # load scales
112
+ qk_scale = sm_scale
113
+ qk_scale *= 1.44269504 # 1/ln2
114
+ # load q: it will stay in SRAM throughout
115
+ q = tl.load(Q_block_ptr)
116
+ S = tl.load(block_indices_lens)
117
+ for i in range(S):
118
+ block_id = tl.load(block_indices + i * stride_bs).to(tl.int32)
119
+ lo, hi = block_id * BLOCK_N_LG, (block_id + 1) * BLOCK_N_LG
120
+ lo = tl.multiple_of(lo, BLOCK_N)
121
+ KT_block_ptr_i = tl.advance(KT_block_ptr, (0, lo))
122
+ V_block_ptr_i = tl.advance(V_block_ptr, (lo, 0))
123
+ mask_offset = lo
124
+
125
+ # loop over k, v and update accumulator
126
+ for start_n in range(lo, hi, BLOCK_N):
127
+ start_n = tl.multiple_of(start_n, BLOCK_N)
128
+ # -- compute qk ----
129
+ kT = tl.load(KT_block_ptr_i)
130
+ qkT = tl.dot(q, kT)
131
+
132
+ if HAS_KV_MASK:
133
+ offs_n = mask_offset + tl.arange(0, BLOCK_N)
134
+ k_valid = tl.load(kv_valid_mask + offs_n)
135
+ qkT = tl.where(k_valid[None, :], qkT, float('-inf'))
136
+
137
+ m_ij = tl.maximum(m_i, tl.max(qkT, 1) * qk_scale)
138
+ qkT = qkT * qk_scale - m_ij[:, None]
139
+ p = tl.math.exp2(qkT)
140
+
141
+ if HAS_KV_MASK:
142
+ p = tl.where(k_valid[None, :], p, 0.0)
143
+
144
+ # -- update m_i and l_i
145
+ # Guard the all-(-inf) block case: if a selected block is fully padded
146
+ # (every key masked) AND it is the first processed block, m_i and m_ij
147
+ # are both -inf, so (m_i - m_ij) = -inf-(-inf) = NaN. Such a block
148
+ # contributes nothing (p is all-zero via the kv-mask above), so alpha
149
+ # must be 1.0 to leave acc / l_i untouched (alpha=0 would zero l_i and
150
+ # produce 0/0 = NaN in the epilogue for an all-padding-selection query).
151
+ # When any selected block has >=1 valid key (the common case) m_ij is
152
+ # finite and this is numerically identical to the original.
153
+ alpha = tl.where(m_ij == float("-inf"), 1.0, tl.math.exp2(m_i - m_ij))
154
+ l_ij = tl.sum(p, 1)
155
+ # -- update output accumulator --
156
+ acc = acc * alpha[:, None]
157
+ # update acc
158
+ v = tl.load(V_block_ptr_i)
159
+ acc = tl.dot(p.to(v.dtype), v, acc)
160
+ # update m_i and l_i
161
+ l_i = l_i * alpha + l_ij
162
+ m_i = m_ij
163
+ V_block_ptr_i = tl.advance(V_block_ptr_i, (BLOCK_N, 0))
164
+ KT_block_ptr_i = tl.advance(KT_block_ptr_i, (0, BLOCK_N))
165
+ mask_offset += BLOCK_N
166
+
167
+ # epilogue
168
+ m_i += tl.math.log2(l_i)
169
+ acc = acc / l_i[:, None]
170
+ m_ptrs = M + off_hz * N_CTX + offs_m
171
+ tl.store(m_ptrs, m_i)
172
+ tl.store(O_block_ptr, acc.to(Out.type.element_ty))
173
+
174
+ configs_fwd_bsa_varlen_align_preset = {
175
+ 'default': {
176
+ 'num_stages': 3,
177
+ 'num_warps': 8,
178
+ },
179
+ 'BLOCK_N_LG=64': {
180
+ 'num_stages': 3,
181
+ 'num_warps': 4,
182
+ },
183
+ }
184
+ configs_fwd_bsa_varlen_align = [
185
+ triton.Config({}, num_stages=s, num_warps=w) \
186
+ for s in [2, 3, 4, 5] \
187
+ for w in [4, 8] \
188
+ ]
189
+
190
+ fwd_bsa_reevaluate_varlen_align_keys = ['N_CTX', 'BLOCK_M', 'BLOCK_N_LG', 'SPARSITY'] if os.environ.get('TRITON_REEVALUATE_KEY', '0') == '1' else []
191
+ @autotune(list(configs_fwd_bsa_varlen_align), key=fwd_bsa_reevaluate_varlen_align_keys)
192
+ @triton.jit
193
+ def _attn_fwd_bsa_varlen_align(
194
+ Q, K, V, sm_scale, M, Out,
195
+ block_indices, # [B, H, M_COMPRESS, S_MAX]
196
+ block_indices_lens, # [B, H, M_COMPRESS]
197
+ kv_valid_mask, # [N_CTX] bool
198
+ stride_qz, stride_qh, stride_qm, stride_qk,
199
+ stride_kz, stride_kh, stride_kn, stride_kk,
200
+ stride_vz, stride_vh, stride_vn, stride_vk,
201
+ stride_oz, stride_oh, stride_om, stride_on,
202
+ stride_bz, stride_bh, stride_bm, stride_bs,
203
+ stride_lz, stride_lh, stride_lm,
204
+ H, N_CTX,
205
+ HEAD_DIM: tl.constexpr,
206
+ BLOCK_M: tl.constexpr,
207
+ BLOCK_N_LG: tl.constexpr,
208
+ SPARSITY: tl.constexpr,
209
+ HAS_KV_MASK: tl.constexpr,
210
+ ):
211
+
212
+ start_m = tl.program_id(0)
213
+ off_hz = tl.program_id(1)
214
+ off_z = off_hz // H
215
+ off_h = off_hz % H
216
+
217
+ q_offset = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh
218
+ k_offset = off_z.to(tl.int64) * stride_kz + off_h.to(tl.int64) * stride_kh
219
+ v_offset = off_z.to(tl.int64) * stride_vz + off_h.to(tl.int64) * stride_vh
220
+ o_offset = off_z.to(tl.int64) * stride_oz + off_h.to(tl.int64) * stride_oh
221
+ b_offset = off_z.to(tl.int64) * stride_bz + off_h.to(tl.int64) * stride_bh
222
+ l_offset = off_z.to(tl.int64) * stride_lz + off_h.to(tl.int64) * stride_lh
223
+
224
+ # block pointers
225
+ Q_block_ptr = tl.make_block_ptr(
226
+ base=Q + q_offset,
227
+ shape=(N_CTX, HEAD_DIM),
228
+ strides=(stride_qm, stride_qk),
229
+ offsets=(start_m * BLOCK_M, 0),
230
+ block_shape=(BLOCK_M, HEAD_DIM),
231
+ order=(1, 0),
232
+ )
233
+ V_block_ptr = tl.make_block_ptr(
234
+ base=V + v_offset,
235
+ shape=(N_CTX, HEAD_DIM),
236
+ strides=(stride_vn, stride_vk),
237
+ offsets=(0, 0),
238
+ block_shape=(BLOCK_N_LG, HEAD_DIM),
239
+ order=(1, 0),
240
+ )
241
+ KT_block_ptr = tl.make_block_ptr(
242
+ base=K + k_offset,
243
+ shape=(HEAD_DIM, N_CTX),
244
+ strides=(stride_kk, stride_kn),
245
+ offsets=(0, 0),
246
+ block_shape=(HEAD_DIM, BLOCK_N_LG),
247
+ order=(0, 1),
248
+ )
249
+ O_block_ptr = tl.make_block_ptr(
250
+ base=Out + o_offset,
251
+ shape=(N_CTX, HEAD_DIM),
252
+ strides=(stride_om, stride_on),
253
+ offsets=(start_m * BLOCK_M, 0),
254
+ block_shape=(BLOCK_M, HEAD_DIM),
255
+ order=(1, 0),
256
+ )
257
+ block_indices += b_offset + start_m * stride_bm
258
+ block_indices_lens += l_offset + start_m * stride_lm
259
+ # initialize offsets
260
+ offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M)
261
+
262
+ # initialize pointer to m and l
263
+ m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf")
264
+ l_i = tl.zeros([BLOCK_M], dtype=tl.float32) + 1.0
265
+ acc = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32)
266
+ # load scales
267
+ qk_scale = sm_scale
268
+ qk_scale *= 1.44269504 # 1/ln2
269
+ # load q: it will stay in SRAM throughout
270
+ q = tl.load(Q_block_ptr)
271
+ S = tl.load(block_indices_lens)
272
+ for i in range(S):
273
+ block_id = tl.load(block_indices + i * stride_bs).to(tl.int32)
274
+ lo = block_id * BLOCK_N_LG
275
+ lo = tl.multiple_of(lo, BLOCK_N_LG)
276
+ KT_block_ptr_i = tl.advance(KT_block_ptr, (0, lo))
277
+ V_block_ptr_i = tl.advance(V_block_ptr, (lo, 0))
278
+
279
+ # -- compute qk ----
280
+ kT = tl.load(KT_block_ptr_i)
281
+ qkT = tl.dot(q, kT)
282
+
283
+ if HAS_KV_MASK:
284
+ offs_n = lo + tl.arange(0, BLOCK_N_LG)
285
+ k_valid = tl.load(kv_valid_mask + offs_n)
286
+ qkT = tl.where(k_valid[None, :], qkT, float('-inf'))
287
+
288
+ m_ij = tl.maximum(m_i, tl.max(qkT, 1) * qk_scale)
289
+ qkT = qkT * qk_scale - m_ij[:, None]
290
+ p = tl.math.exp2(qkT)
291
+
292
+ if HAS_KV_MASK:
293
+ p = tl.where(k_valid[None, :], p, 0.0)
294
+
295
+ # -- update m_i and l_i
296
+ # Guard the all-(-inf) block case: a fully-padded selected block processed
297
+ # first leaves m_i == m_ij == -inf, so (m_i - m_ij) = NaN. Such a block
298
+ # contributes nothing (p is all-zero via the kv-mask above); alpha must be
299
+ # 1.0 to leave acc / l_i untouched (alpha=0 would zero l_i -> 0/0 = NaN in
300
+ # the epilogue for an all-padding-selection query). Identical to the
301
+ # original whenever any selected block has >=1 valid key (the common case).
302
+ alpha = tl.where(m_ij == float("-inf"), 1.0, tl.math.exp2(m_i - m_ij))
303
+ l_ij = tl.sum(p, 1)
304
+ # -- update output accumulator --
305
+ acc = acc * alpha[:, None]
306
+ # update acc
307
+ v = tl.load(V_block_ptr_i)
308
+ acc = tl.dot(p.to(v.dtype), v, acc)
309
+ # update m_i and l_i
310
+ l_i = l_i * alpha + l_ij
311
+ m_i = m_ij
312
+
313
+
314
+ # epilogue
315
+ m_i += tl.math.log2(l_i)
316
+ acc = acc / l_i[:, None]
317
+ m_ptrs = M + off_hz * N_CTX + offs_m
318
+ tl.store(m_ptrs, m_i)
319
+ tl.store(O_block_ptr, acc.to(Out.type.element_ty))
320
+
321
+
322
+ # The main inner-loop logic for computing dK and dV.
323
+ @triton.jit
324
+ def _attn_bwd_dkdv_bsa_varlen(
325
+ dk, dv,
326
+ k, v,
327
+ Q, DO,
328
+ M, D,
329
+ block_indices,
330
+ block_indices_lens,
331
+ # shared by Q/K/V/DO.
332
+ stride_qm, stride_qk,
333
+ stride_dom, stride_dok,
334
+ stride_mm,
335
+ stride_dm,
336
+ stride_bm,
337
+ N_CTX,
338
+ kv_valid_mask,
339
+ k_start_pos,
340
+ BLOCK_N_K: tl.constexpr,
341
+ BLOCK_M1: tl.constexpr,
342
+ HEAD_DIM: tl.constexpr,
343
+ HAS_KV_MASK: tl.constexpr,
344
+ ):
345
+
346
+ if HAS_KV_MASK:
347
+ offs_k = k_start_pos + tl.arange(0, BLOCK_N_K)
348
+ k_valid = tl.load(kv_valid_mask + offs_k)
349
+
350
+ QT_block_ptr = tl.make_block_ptr(
351
+ base=Q,
352
+ shape=(HEAD_DIM, N_CTX),
353
+ strides=(stride_qk, stride_qm),
354
+ offsets=(0, 0),
355
+ block_shape=(HEAD_DIM, BLOCK_M1),
356
+ order=(0, 1),
357
+ )
358
+
359
+ DO_block_ptr = tl.make_block_ptr(
360
+ base=DO,
361
+ shape=(N_CTX, HEAD_DIM),
362
+ strides=(stride_dom, stride_dok),
363
+ offsets=(0, 0),
364
+ block_shape=(BLOCK_M1, HEAD_DIM),
365
+ order=(1, 0),
366
+ )
367
+
368
+ S = tl.load(block_indices_lens)
369
+ for i in range(S):
370
+ block_id = tl.load(block_indices + i * stride_bm).to(tl.int32)
371
+ start_m = block_id * BLOCK_M1
372
+ start_m = tl.multiple_of(start_m, BLOCK_M1)
373
+
374
+ QT_block_ptr_i = tl.advance(QT_block_ptr, (0, start_m))
375
+ DO_block_ptr_i = tl.advance(DO_block_ptr, (start_m, 0))
376
+
377
+ qT = tl.load(QT_block_ptr_i)
378
+ offs_m = start_m + tl.arange(0, BLOCK_M1) * stride_mm
379
+ m = tl.load(M + offs_m)
380
+ kqT = tl.dot(k, qT)
381
+ if HAS_KV_MASK:
382
+ kqT = tl.where(k_valid[:, None], kqT, float('-inf'))
383
+ pT = tl.math.exp2(kqT - m[None, :])
384
+ if HAS_KV_MASK:
385
+ pT = tl.where(k_valid[:, None], pT, 0.0)
386
+
387
+ do = tl.load(DO_block_ptr_i)
388
+ # Compute dV.
389
+ ppT = pT
390
+ ppT = ppT.to(v.dtype)
391
+ dv += tl.dot(ppT, do)
392
+ # D (= delta) is pre-divided by ds_scale.
393
+ offs_d = start_m + tl.arange(0, BLOCK_M1) * stride_dm
394
+ Di = tl.load(D + offs_d)
395
+ # Compute dP and dS.
396
+ dpT = tl.dot(v, tl.trans(do)).to(tl.float32)
397
+ dsT = pT * (dpT - Di[None, :])
398
+ dsT = dsT.to(v.dtype)
399
+ dk += tl.dot(dsT, tl.trans(qT))
400
+
401
+
402
+ return dk, dv
403
+
404
+
405
+ # the main inner-loop logic for computing dQ
406
+ @triton.jit
407
+ def _attn_bwd_dq_bsa_varlen(
408
+ dq,
409
+ q, do,
410
+ m, d,
411
+ K, V,
412
+ N_CTX,
413
+ BLOCK_N2: tl.constexpr,
414
+ BLOCK_N_LG: tl.constexpr,
415
+ HEAD_DIM: tl.constexpr,
416
+ block_indices,
417
+ block_indices_lens,
418
+ stride_bn,
419
+ stride_kn, stride_kk,
420
+ stride_vn, stride_vk,
421
+ kv_valid_mask,
422
+ HAS_KV_MASK: tl.constexpr,
423
+ ):
424
+
425
+ VT_block_ptr = tl.make_block_ptr(
426
+ base=V,
427
+ shape=(HEAD_DIM, N_CTX),
428
+ strides=(stride_vk, stride_vn),
429
+ offsets=(0, 0),
430
+ block_shape=(HEAD_DIM, BLOCK_N2),
431
+ order=(0, 1),
432
+ )
433
+ KT_block_ptr = tl.make_block_ptr(
434
+ base=K,
435
+ shape=(HEAD_DIM, N_CTX),
436
+ strides=(stride_kk, stride_kn),
437
+ offsets=(0, 0),
438
+ block_shape=(HEAD_DIM, BLOCK_N2),
439
+ order=(0, 1),
440
+ )
441
+
442
+ S = tl.load(block_indices_lens)
443
+ for i in range(S):
444
+ block_id = tl.load(block_indices + i * stride_bn).to(tl.int32)
445
+ lo, hi = block_id * BLOCK_N_LG, (block_id + 1) * BLOCK_N_LG
446
+ lo = tl.multiple_of(lo, BLOCK_N2)
447
+
448
+ KT_block_ptr_i = tl.advance(KT_block_ptr, (0, lo))
449
+ VT_block_ptr_i = tl.advance(VT_block_ptr, (0, lo))
450
+ mask_offset = lo
451
+
452
+ for start_n in range(lo, hi, BLOCK_N2):
453
+ start_n = tl.multiple_of(start_n, BLOCK_N2)
454
+
455
+ kT = tl.load(KT_block_ptr_i)
456
+ vT = tl.load(VT_block_ptr_i)
457
+ qkT = tl.dot(q, kT)
458
+ if HAS_KV_MASK:
459
+ offs_n = mask_offset + tl.arange(0, BLOCK_N2)
460
+ k_valid = tl.load(kv_valid_mask + offs_n)
461
+ qkT = tl.where(k_valid[None, :], qkT, float('-inf'))
462
+ p = tl.math.exp2(qkT - m)
463
+ if HAS_KV_MASK:
464
+ p = tl.where(k_valid[None, :], p, 0.0)
465
+ # Compute dP and dS.
466
+ dp = tl.dot(do, vT).to(tl.float32)
467
+ ds = p * (dp - d)
468
+ ds = ds.to(kT.dtype)
469
+ # Compute dQ.
470
+ dq += tl.dot(ds, tl.trans(kT))
471
+ # Increment pointers.
472
+ KT_block_ptr_i = tl.advance(KT_block_ptr_i, (0, BLOCK_N2))
473
+ VT_block_ptr_i = tl.advance(VT_block_ptr_i, (0, BLOCK_N2))
474
+ mask_offset += BLOCK_N2
475
+
476
+ return dq
477
+
478
+ # the main inner-loop logic for computing dQ
479
+ @triton.jit
480
+ def _attn_bwd_dq_bsa_varlen_align(
481
+ dq,
482
+ q, do,
483
+ m, d,
484
+ K, V,
485
+ N_CTX,
486
+ BLOCK_N_LG: tl.constexpr,
487
+ HEAD_DIM: tl.constexpr,
488
+ block_indices,
489
+ block_indices_lens,
490
+ stride_bn,
491
+ stride_kn, stride_kk,
492
+ stride_vn, stride_vk,
493
+ kv_valid_mask,
494
+ HAS_KV_MASK: tl.constexpr,
495
+ ):
496
+
497
+ VT_block_ptr = tl.make_block_ptr(
498
+ base=V,
499
+ shape=(HEAD_DIM, N_CTX),
500
+ strides=(stride_vk, stride_vn),
501
+ offsets=(0, 0),
502
+ block_shape=(HEAD_DIM, BLOCK_N_LG),
503
+ order=(0, 1),
504
+ )
505
+ KT_block_ptr = tl.make_block_ptr(
506
+ base=K,
507
+ shape=(HEAD_DIM, N_CTX),
508
+ strides=(stride_kk, stride_kn),
509
+ offsets=(0, 0),
510
+ block_shape=(HEAD_DIM, BLOCK_N_LG),
511
+ order=(0, 1),
512
+ )
513
+
514
+ S = tl.load(block_indices_lens)
515
+ for i in range(S):
516
+ block_id = tl.load(block_indices + i * stride_bn).to(tl.int32)
517
+ lo = block_id * BLOCK_N_LG
518
+ lo = tl.multiple_of(lo, BLOCK_N_LG)
519
+
520
+ KT_block_ptr_i = tl.advance(KT_block_ptr, (0, lo))
521
+ VT_block_ptr_i = tl.advance(VT_block_ptr, (0, lo))
522
+
523
+ kT = tl.load(KT_block_ptr_i)
524
+ vT = tl.load(VT_block_ptr_i)
525
+ qkT = tl.dot(q, kT)
526
+ if HAS_KV_MASK:
527
+ offs_n = lo + tl.arange(0, BLOCK_N_LG)
528
+ k_valid = tl.load(kv_valid_mask + offs_n)
529
+ qkT = tl.where(k_valid[None, :], qkT, float('-inf'))
530
+ p = tl.math.exp2(qkT - m)
531
+ if HAS_KV_MASK:
532
+ p = tl.where(k_valid[None, :], p, 0.0)
533
+ # Compute dP and dS.
534
+ dp = tl.dot(do, vT).to(tl.float32)
535
+ ds = p * (dp - d)
536
+ ds = ds.to(kT.dtype)
537
+ # Compute dQ.
538
+ dq += tl.dot(ds, tl.trans(kT))
539
+
540
+
541
+ return dq
542
+
543
+ configs_bwd_dkdv_bsa_varlen_preset = {
544
+ 'default': {
545
+ 'BLOCK_N': 128,
546
+ 'num_stages': 2,
547
+ 'num_warps': 8,
548
+ },
549
+ 'BLOCK_N_DQ_LG=64': {
550
+ 'BLOCK_N': 64,
551
+ 'num_stages': 2,
552
+ 'num_warps': 4,
553
+ }
554
+ }
555
+ configs_bwd_dkdv_bsa_varlen = [
556
+ triton.Config({'BLOCK_N': BN}, num_stages=s, num_warps=w) \
557
+ for BN in [32, 64, 128] \
558
+ for s in [2, 3, 4, 5] \
559
+ for w in [4, 8] \
560
+ ]
561
+ bwd_dkdv_bsa_varlen_reevaluate_keys = ['N_CTX', 'BLOCK_M', 'BLOCK_N_DQ_LG', 'SPARSITY'] if os.environ.get('TRITON_REEVALUATE_KEY', '0') == '1' else []
562
+ @autotune(list(configs_bwd_dkdv_bsa_varlen), key=bwd_dkdv_bsa_varlen_reevaluate_keys)
563
+ @triton.jit
564
+ def _attn_bwd_dkdv_bsa_varlen_wrapper(
565
+ Q, K, V, sm_scale, # softmax scale
566
+ DO,
567
+ DK, DV,
568
+ M, # lse (log2)
569
+ D,
570
+ block_indices,
571
+ block_indices_lens,
572
+ kv_valid_mask,
573
+ # qkv
574
+ stride_qz, stride_qh, stride_qm, stride_qk,
575
+ stride_kz, stride_kh, stride_kn, stride_kk,
576
+ stride_vz, stride_vh, stride_vn, stride_vk,
577
+ # dk dv do
578
+ stride_dkz, stride_dkh, stride_dkn, stride_dkk,
579
+ stride_dvz, stride_dvh, stride_dvn, stride_dvk,
580
+ stride_doz, stride_doh, stride_dom, stride_dok,
581
+ # m, d
582
+ stride_mz, stride_mh, stride_mm,
583
+ stride_dz, stride_dh, stride_dm,
584
+ #
585
+ stride_bz, stride_bh, stride_bn, stride_bm, # block_indices
586
+ stride_lz, stride_lh, stride_ln, # block_indices_lens
587
+ #
588
+ H, N_CTX,
589
+ BLOCK_M: tl.constexpr,
590
+ BLOCK_N: tl.constexpr,
591
+ BLOCK_N_DQ_LG: tl.constexpr, # logical block size
592
+ HEAD_DIM: tl.constexpr,
593
+ SPARSITY: tl.constexpr,
594
+ HAS_KV_MASK: tl.constexpr,
595
+ ):
596
+
597
+ tl.static_assert(BLOCK_N_DQ_LG % BLOCK_N == 0)
598
+ start_n = tl.program_id(0)
599
+
600
+ off_hz = tl.program_id(2)
601
+ off_z = off_hz // H
602
+ off_h = off_hz % H
603
+
604
+ off_q = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh
605
+ off_k = off_z.to(tl.int64) * stride_kz + off_h.to(tl.int64) * stride_kh
606
+ off_v = off_z.to(tl.int64) * stride_vz + off_h.to(tl.int64) * stride_vh
607
+
608
+ off_dk = off_z.to(tl.int64) * stride_dkz + off_h.to(tl.int64) * stride_dkh
609
+ off_dv = off_z.to(tl.int64) * stride_dvz + off_h.to(tl.int64) * stride_dvh
610
+ off_do = off_z.to(tl.int64) * stride_doz + off_h.to(tl.int64) * stride_doh
611
+
612
+ off_m = off_z.to(tl.int64) * stride_mz + off_h.to(tl.int64) * stride_mh
613
+ off_d = off_z.to(tl.int64) * stride_dz + off_h.to(tl.int64) * stride_dh
614
+
615
+ off_block_incides = off_z.to(tl.int64) * stride_bz + off_h.to(tl.int64) * stride_bh
616
+ off_block_incides_lens = off_z.to(tl.int64) * stride_lz + off_h.to(tl.int64) * stride_lh
617
+
618
+ # offset pointers for batch/head
619
+ Q += off_q
620
+ K += off_k
621
+ V += off_v
622
+ DO += off_do
623
+ DK += off_dk
624
+ DV += off_dv
625
+
626
+ M += off_m
627
+ D += off_d
628
+ block_indices += off_block_incides
629
+ block_indices_lens += off_block_incides_lens
630
+
631
+ # ---------------------------------------- [DKDV] ----------------------------------------
632
+
633
+ dv = tl.zeros([BLOCK_N, HEAD_DIM], dtype=tl.float32)
634
+ dk = tl.zeros([BLOCK_N, HEAD_DIM], dtype=tl.float32)
635
+
636
+ # load K and V: they stay in SRAM throughout the inner loop.
637
+
638
+ K_block_ptr = tl.make_block_ptr(
639
+ base=K,
640
+ shape=(N_CTX, HEAD_DIM),
641
+ strides=(stride_kn, stride_kk),
642
+ offsets=(start_n * BLOCK_N, 0),
643
+ block_shape=(BLOCK_N, HEAD_DIM),
644
+ order=(1, 0),
645
+ )
646
+ V_block_ptr = tl.make_block_ptr(
647
+ base=V,
648
+ shape=(N_CTX, HEAD_DIM),
649
+ strides=(stride_vn, stride_vk),
650
+ offsets=(start_n * BLOCK_N, 0),
651
+ block_shape=(BLOCK_N, HEAD_DIM),
652
+ order=(1, 0),
653
+ )
654
+ DK_block_ptr = tl.make_block_ptr(
655
+ base=DK,
656
+ shape=(N_CTX, HEAD_DIM),
657
+ strides=(stride_dkn, stride_dkk),
658
+ offsets=(start_n * BLOCK_N, 0),
659
+ block_shape=(BLOCK_N, HEAD_DIM),
660
+ order=(1, 0),
661
+ )
662
+ DV_block_ptr = tl.make_block_ptr(
663
+ base=DV,
664
+ shape=(N_CTX, HEAD_DIM),
665
+ strides=(stride_dvn, stride_dvk),
666
+ offsets=(start_n * BLOCK_N, 0),
667
+ block_shape=(BLOCK_N, HEAD_DIM),
668
+ order=(1, 0),
669
+ )
670
+
671
+ k = tl.load(K_block_ptr)
672
+ v = tl.load(V_block_ptr)
673
+
674
+ k_compress_idx = start_n * BLOCK_N // BLOCK_N_DQ_LG
675
+ block_indices_i = block_indices + k_compress_idx * stride_bn
676
+ block_indices_lens_i = block_indices_lens + k_compress_idx * stride_ln
677
+
678
+ dk, dv = _attn_bwd_dkdv_bsa_varlen(
679
+ dk, dv,
680
+ k, v,
681
+ Q, DO,
682
+ M, D,
683
+ block_indices_i,
684
+ block_indices_lens_i,
685
+ stride_qm, stride_qk,
686
+ stride_dom, stride_dok,
687
+ stride_mm,
688
+ stride_dm,
689
+ stride_bm,
690
+ N_CTX,
691
+ kv_valid_mask,
692
+ start_n * BLOCK_N,
693
+ BLOCK_N,
694
+ BLOCK_M,
695
+ HEAD_DIM,
696
+ HAS_KV_MASK,
697
+ )
698
+
699
+ # Write back dk
700
+ dk *= sm_scale # S = scale * QKT; dK = scale * QdST
701
+ tl.store(DK_block_ptr, dk.to(k.dtype))
702
+
703
+ # Write back dv
704
+ tl.store(DV_block_ptr, dv.to(v.dtype))
705
+
706
+ configs_bwd_dq_bsa_varlen_preset = {
707
+ 'default': {
708
+ 'BLOCK_N_DQ': 64,
709
+ 'num_stages': 2,
710
+ 'num_warps': 8,
711
+ },
712
+ 'BLOCK_N_DQ_LG=64': {
713
+ 'BLOCK_N_DQ': 64,
714
+ 'num_stages': 2,
715
+ 'num_warps': 4,
716
+ },
717
+ }
718
+ configs_bwd_dq_bsa_varlen = [
719
+ triton.Config({'BLOCK_N_DQ': BN}, num_stages=s, num_warps=w) \
720
+ for BN in [32, 64, 128] \
721
+ for s in [2, 3, 4, 5] \
722
+ for w in [4, 8] \
723
+ ]
724
+ bwd_dq_bsa_varlen_reevaluate_keys = ['N_CTX', 'BLOCK_M', 'BLOCK_N_DQ_LG', 'SPARSITY'] if os.environ.get('TRITON_REEVALUATE_KEY', '0') == '1' else []
725
+ @autotune(list(configs_bwd_dq_bsa_varlen), key=bwd_dq_bsa_varlen_reevaluate_keys)
726
+ @triton.jit
727
+ def _attn_bwd_dq_bsa_varlen_wrapper(
728
+ Q, K, V,
729
+ DO,
730
+ DQ,
731
+ M, # lse (log2)
732
+ D,
733
+ block_indices,
734
+ block_indices_lens,
735
+ kv_valid_mask,
736
+ # qkv
737
+ stride_qz, stride_qh, stride_qm, stride_qk,
738
+ stride_kz, stride_kh, stride_kn, stride_kk,
739
+ stride_vz, stride_vh, stride_vn, stride_vk,
740
+ # dq do
741
+ stride_dqz, stride_dqh, stride_dqm, stride_dqk,
742
+ stride_doz, stride_doh, stride_dom, stride_dok,
743
+ # m, d
744
+ stride_mz, stride_mh, stride_mm,
745
+ stride_dz, stride_dh, stride_dm,
746
+ #
747
+ stride_bz, stride_bh, stride_bm, stride_bn, # block_indices
748
+ stride_lz, stride_lh, stride_lm, # block_indices_lens
749
+ #
750
+ H, N_CTX,
751
+ BLOCK_M: tl.constexpr,
752
+ BLOCK_N_DQ_LG: tl.constexpr, # logical block size
753
+ BLOCK_N_DQ: tl.constexpr,
754
+ HEAD_DIM: tl.constexpr,
755
+ SPARSITY: tl.constexpr,
756
+ HAS_KV_MASK: tl.constexpr,
757
+ ):
758
+
759
+ tl.static_assert(BLOCK_N_DQ_LG % BLOCK_N_DQ == 0)
760
+ tl.static_assert(BLOCK_N_DQ_LG % BLOCK_M == 0)
761
+
762
+ LN2: tl.constexpr = 0.6931471824645996 # = ln(2)
763
+
764
+ start_m = tl.program_id(0)
765
+
766
+ off_hz = tl.program_id(2)
767
+ off_z = off_hz // H
768
+ off_h = off_hz % H
769
+
770
+ off_q = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh
771
+ off_k = off_z.to(tl.int64) * stride_kz + off_h.to(tl.int64) * stride_kh
772
+ off_v = off_z.to(tl.int64) * stride_vz + off_h.to(tl.int64) * stride_vh
773
+
774
+ off_dq = off_z.to(tl.int64) * stride_dqz + off_h.to(tl.int64) * stride_dqh
775
+ off_do = off_z.to(tl.int64) * stride_doz + off_h.to(tl.int64) * stride_doh
776
+
777
+ off_m = off_z.to(tl.int64) * stride_mz + off_h.to(tl.int64) * stride_mh
778
+ off_d = off_z.to(tl.int64) * stride_dz + off_h.to(tl.int64) * stride_dh
779
+
780
+ off_block_incides = off_z.to(tl.int64) * stride_bz + off_h.to(tl.int64) * stride_bh
781
+ off_block_incides_lens = off_z.to(tl.int64) * stride_lz + off_h.to(tl.int64) * stride_lh
782
+
783
+ # offset pointers for batch/head
784
+ Q += off_q
785
+ K += off_k
786
+ V += off_v
787
+ DO += off_do
788
+ DQ += off_dq
789
+
790
+ M += off_m
791
+ D += off_d
792
+ block_indices += off_block_incides
793
+ block_indices_lens += off_block_incides_lens
794
+
795
+ # ---------------------------------------- [DQ] ----------------------------------------
796
+
797
+ Q_block_ptr = tl.make_block_ptr(
798
+ base=Q,
799
+ shape=(N_CTX, HEAD_DIM),
800
+ strides=(stride_qm, stride_qk),
801
+ offsets=(start_m * BLOCK_M, 0),
802
+ block_shape=(BLOCK_M, HEAD_DIM),
803
+ order=(1, 0),
804
+ )
805
+ DO_block_ptr = tl.make_block_ptr(
806
+ base=DO,
807
+ shape=(N_CTX, HEAD_DIM),
808
+ strides=(stride_dom, stride_dok),
809
+ offsets=(start_m * BLOCK_M, 0),
810
+ block_shape=(BLOCK_M, HEAD_DIM),
811
+ order=(1, 0),
812
+ )
813
+ DQ_block_ptr = tl.make_block_ptr(
814
+ base=DQ,
815
+ shape=(N_CTX, HEAD_DIM),
816
+ strides=(stride_dqm, stride_dqk),
817
+ offsets=(start_m * BLOCK_M, 0),
818
+ block_shape=(BLOCK_M, HEAD_DIM),
819
+ order=(1, 0),
820
+ )
821
+
822
+ q = tl.load(Q_block_ptr)
823
+ do = tl.load(DO_block_ptr)
824
+
825
+ dq = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32)
826
+
827
+ start_m = start_m * BLOCK_M
828
+
829
+ offs_m = start_m + tl.arange(0, BLOCK_M) * stride_mm
830
+ offs_d = start_m + tl.arange(0, BLOCK_M) * stride_dm
831
+
832
+ m = tl.load(M + offs_m)
833
+ m = m[:, None]
834
+
835
+ d = tl.load(D + offs_d)
836
+ d = d[:, None]
837
+
838
+ block_indices_m = block_indices + (start_m // BLOCK_M) * stride_bm
839
+ block_indices_lens_m = block_indices_lens + (start_m // BLOCK_M) * stride_lm
840
+
841
+ dq = _attn_bwd_dq_bsa_varlen(
842
+ dq,
843
+ q, do,
844
+ m, d,
845
+ K, V,
846
+ N_CTX,
847
+ BLOCK_N_DQ,
848
+ BLOCK_N_DQ_LG,
849
+ HEAD_DIM,
850
+ block_indices_m,
851
+ block_indices_lens_m,
852
+ stride_bn,
853
+ stride_kn, stride_kk,
854
+ stride_vn, stride_vk,
855
+ kv_valid_mask,
856
+ HAS_KV_MASK,
857
+ )
858
+
859
+ # Write back dQ.
860
+ dq *= LN2
861
+ tl.store(DQ_block_ptr, dq.to(q.dtype))
862
+
863
+ configs_bwd_dq_bsa_varlen_align_preset = {
864
+ 'default': {
865
+ 'num_stages': 2,
866
+ 'num_warps': 8,
867
+ },
868
+ 'BLOCK_N_DQ_LG=64': {
869
+ 'num_stages': 2,
870
+ 'num_warps': 4,
871
+ },
872
+ }
873
+ configs_bwd_dq_bsa_varlen_align = [
874
+ triton.Config({}, num_stages=s, num_warps=w) \
875
+ for s in [2, 3, 4, 5] \
876
+ for w in [4, 8] \
877
+ ]
878
+ bwd_dq_bsa_varlen_align_reevaluate_keys = ['N_CTX', 'BLOCK_M', 'BLOCK_N_DQ_LG', 'SPARSITY'] if os.environ.get('TRITON_REEVALUATE_KEY', '0') == '1' else []
879
+ @autotune(list(configs_bwd_dq_bsa_varlen_align), key=bwd_dq_bsa_varlen_align_reevaluate_keys)
880
+ @triton.jit
881
+ def _attn_bwd_dq_bsa_varlen_align_wrapper(
882
+ Q, K, V,
883
+ DO,
884
+ DQ,
885
+ M, # lse (log2)
886
+ D,
887
+ block_indices,
888
+ block_indices_lens,
889
+ kv_valid_mask,
890
+ # qkv
891
+ stride_qz, stride_qh, stride_qm, stride_qk,
892
+ stride_kz, stride_kh, stride_kn, stride_kk,
893
+ stride_vz, stride_vh, stride_vn, stride_vk,
894
+ # dq do
895
+ stride_dqz, stride_dqh, stride_dqm, stride_dqk,
896
+ stride_doz, stride_doh, stride_dom, stride_dok,
897
+ # m, d
898
+ stride_mz, stride_mh, stride_mm,
899
+ stride_dz, stride_dh, stride_dm,
900
+ #
901
+ stride_bz, stride_bh, stride_bm, stride_bn, # block_indices
902
+ stride_lz, stride_lh, stride_lm, # block_indices_lens
903
+ #
904
+ H, N_CTX,
905
+ BLOCK_M: tl.constexpr,
906
+ BLOCK_N_DQ_LG: tl.constexpr, # logical block size
907
+ HEAD_DIM: tl.constexpr,
908
+ SPARSITY: tl.constexpr,
909
+ HAS_KV_MASK: tl.constexpr,
910
+ ):
911
+
912
+ tl.static_assert(BLOCK_N_DQ_LG % BLOCK_N_DQ_LG == 0)
913
+ tl.static_assert(BLOCK_N_DQ_LG % BLOCK_M == 0)
914
+
915
+ LN2: tl.constexpr = 0.6931471824645996 # = ln(2)
916
+
917
+ start_m = tl.program_id(0)
918
+
919
+ off_hz = tl.program_id(2)
920
+ off_z = off_hz // H
921
+ off_h = off_hz % H
922
+
923
+ off_q = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh
924
+ off_k = off_z.to(tl.int64) * stride_kz + off_h.to(tl.int64) * stride_kh
925
+ off_v = off_z.to(tl.int64) * stride_vz + off_h.to(tl.int64) * stride_vh
926
+
927
+ off_dq = off_z.to(tl.int64) * stride_dqz + off_h.to(tl.int64) * stride_dqh
928
+ off_do = off_z.to(tl.int64) * stride_doz + off_h.to(tl.int64) * stride_doh
929
+
930
+ off_m = off_z.to(tl.int64) * stride_mz + off_h.to(tl.int64) * stride_mh
931
+ off_d = off_z.to(tl.int64) * stride_dz + off_h.to(tl.int64) * stride_dh
932
+
933
+ off_block_incides = off_z.to(tl.int64) * stride_bz + off_h.to(tl.int64) * stride_bh
934
+ off_block_incides_lens = off_z.to(tl.int64) * stride_lz + off_h.to(tl.int64) * stride_lh
935
+
936
+ # offset pointers for batch/head
937
+ Q += off_q
938
+ K += off_k
939
+ V += off_v
940
+ DO += off_do
941
+ DQ += off_dq
942
+
943
+ M += off_m
944
+ D += off_d
945
+ block_indices += off_block_incides
946
+ block_indices_lens += off_block_incides_lens
947
+
948
+ # ---------------------------------------- [DQ] ----------------------------------------
949
+
950
+ Q_block_ptr = tl.make_block_ptr(
951
+ base=Q,
952
+ shape=(N_CTX, HEAD_DIM),
953
+ strides=(stride_qm, stride_qk),
954
+ offsets=(start_m * BLOCK_M, 0),
955
+ block_shape=(BLOCK_M, HEAD_DIM),
956
+ order=(1, 0),
957
+ )
958
+ DO_block_ptr = tl.make_block_ptr(
959
+ base=DO,
960
+ shape=(N_CTX, HEAD_DIM),
961
+ strides=(stride_dom, stride_dok),
962
+ offsets=(start_m * BLOCK_M, 0),
963
+ block_shape=(BLOCK_M, HEAD_DIM),
964
+ order=(1, 0),
965
+ )
966
+ DQ_block_ptr = tl.make_block_ptr(
967
+ base=DQ,
968
+ shape=(N_CTX, HEAD_DIM),
969
+ strides=(stride_dqm, stride_dqk),
970
+ offsets=(start_m * BLOCK_M, 0),
971
+ block_shape=(BLOCK_M, HEAD_DIM),
972
+ order=(1, 0),
973
+ )
974
+
975
+ q = tl.load(Q_block_ptr)
976
+ do = tl.load(DO_block_ptr)
977
+
978
+ dq = tl.zeros([BLOCK_M, HEAD_DIM], dtype=tl.float32)
979
+
980
+ start_m = start_m * BLOCK_M
981
+
982
+ offs_m = start_m + tl.arange(0, BLOCK_M) * stride_mm
983
+ offs_d = start_m + tl.arange(0, BLOCK_M) * stride_dm
984
+
985
+ m = tl.load(M + offs_m)
986
+ m = m[:, None]
987
+
988
+ # D (= delta) is pre-divided by ds_scale.
989
+ d = tl.load(D + offs_d)
990
+ d = d[:, None]
991
+
992
+ block_indices_m = block_indices + (start_m // BLOCK_M) * stride_bm
993
+ block_indices_lens_m = block_indices_lens + (start_m // BLOCK_M) * stride_lm
994
+
995
+ dq = _attn_bwd_dq_bsa_varlen_align(
996
+ dq,
997
+ q, do,
998
+ m, d,
999
+ K, V,
1000
+ N_CTX,
1001
+ BLOCK_N_DQ_LG,
1002
+ HEAD_DIM,
1003
+ block_indices_m,
1004
+ block_indices_lens_m,
1005
+ stride_bn,
1006
+ stride_kn, stride_kk,
1007
+ stride_vn, stride_vk,
1008
+ kv_valid_mask,
1009
+ HAS_KV_MASK,
1010
+ )
1011
+
1012
+ # Write back dQ.
1013
+ dq *= LN2
1014
+ tl.store(DQ_block_ptr, dq.to(q.dtype))
hymm/models/modules/dac_vae.py ADDED
@@ -0,0 +1,1103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import math
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import List, Union
6
+
7
+ import numpy as np
8
+ import torch
9
+ import tqdm
10
+ from audiotools import AudioSignal
11
+ from audiotools.ml import BaseModel
12
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
13
+ from diffusers.models.modeling_utils import ModelMixin
14
+ from diffusers.utils.accelerate_utils import apply_forward_hook
15
+ from torch import nn
16
+ from torch.nn.utils import weight_norm
17
+
18
+ SUPPORTED_VERSIONS = ["1.0.0"]
19
+
20
+
21
+ @dataclass
22
+ class DACFile:
23
+ codes: torch.Tensor
24
+
25
+ # Metadata
26
+ chunk_length: int
27
+ original_length: int
28
+ input_db: float
29
+ channels: int
30
+ sample_rate: int
31
+ padding: bool
32
+ dac_version: str
33
+
34
+ def save(self, path):
35
+ artifacts = {
36
+ "codes": self.codes.numpy().astype(np.uint16),
37
+ "metadata": {
38
+ "input_db": self.input_db.numpy().astype(np.float32),
39
+ "original_length": self.original_length,
40
+ "sample_rate": self.sample_rate,
41
+ "chunk_length": self.chunk_length,
42
+ "channels": self.channels,
43
+ "padding": self.padding,
44
+ "dac_version": SUPPORTED_VERSIONS[-1],
45
+ },
46
+ }
47
+ path = Path(path).with_suffix(".dac")
48
+ with open(path, "wb") as f:
49
+ np.save(f, artifacts)
50
+ return path
51
+
52
+ @classmethod
53
+ def load(cls, path):
54
+ artifacts = np.load(path, allow_pickle=True)[()]
55
+ codes = torch.from_numpy(artifacts["codes"].astype(int))
56
+ if artifacts["metadata"].get("dac_version", None) not in SUPPORTED_VERSIONS:
57
+ raise RuntimeError(
58
+ f"Given file {path} can't be loaded with this version of descript-audio-codec."
59
+ )
60
+ return cls(codes=codes, **artifacts["metadata"])
61
+
62
+
63
+ class CodecMixin:
64
+ @property
65
+ def padding(self):
66
+ if not hasattr(self, "_padding"):
67
+ self._padding = True
68
+ return self._padding
69
+
70
+ @padding.setter
71
+ def padding(self, value):
72
+ assert isinstance(value, bool)
73
+
74
+ layers = [
75
+ l for l in self.modules() if isinstance(l, (nn.Conv1d, nn.ConvTranspose1d))
76
+ ]
77
+
78
+ for layer in layers:
79
+ if value:
80
+ if hasattr(layer, "original_padding"):
81
+ layer.padding = layer.original_padding
82
+ else:
83
+ layer.original_padding = layer.padding
84
+ layer.padding = tuple(0 for _ in range(len(layer.padding)))
85
+
86
+ self._padding = value
87
+
88
+ def get_delay(self):
89
+ # Any number works here, delay is invariant to input length
90
+ l_out = self.get_output_length(0)
91
+ L = l_out
92
+
93
+ layers = []
94
+ for layer in self.modules():
95
+ if isinstance(layer, (nn.Conv1d, nn.ConvTranspose1d)):
96
+ layers.append(layer)
97
+
98
+ for layer in reversed(layers):
99
+ d = layer.dilation[0]
100
+ k = layer.kernel_size[0]
101
+ s = layer.stride[0]
102
+
103
+ if isinstance(layer, nn.ConvTranspose1d):
104
+ L = ((L - d * (k - 1) - 1) / s) + 1
105
+ elif isinstance(layer, nn.Conv1d):
106
+ L = (L - 1) * s + d * (k - 1) + 1
107
+
108
+ L = math.ceil(L)
109
+
110
+ l_in = L
111
+
112
+ return (l_in - l_out) // 2
113
+
114
+ def get_output_length(self, input_length):
115
+ L = input_length
116
+ # Calculate output length
117
+ for layer in self.modules():
118
+ if isinstance(layer, (nn.Conv1d, nn.ConvTranspose1d)):
119
+ d = layer.dilation[0]
120
+ k = layer.kernel_size[0]
121
+ s = layer.stride[0]
122
+
123
+ if isinstance(layer, nn.Conv1d):
124
+ L = ((L - d * (k - 1) - 1) / s) + 1
125
+ elif isinstance(layer, nn.ConvTranspose1d):
126
+ L = (L - 1) * s + d * (k - 1) + 1
127
+
128
+ L = math.floor(L)
129
+ return L
130
+
131
+ @torch.no_grad()
132
+ def compress(
133
+ self,
134
+ audio_path_or_signal: Union[str, Path, AudioSignal],
135
+ win_duration: float = 1.0,
136
+ verbose: bool = False,
137
+ normalize_db: float = -16,
138
+ n_quantizers: int = None,
139
+ ) -> DACFile:
140
+ """Processes an audio signal from a file or AudioSignal object into
141
+ discrete codes. This function processes the signal in short windows,
142
+ using constant GPU memory.
143
+
144
+ Parameters
145
+ ----------
146
+ audio_path_or_signal : Union[str, Path, AudioSignal]
147
+ audio signal to reconstruct
148
+ win_duration : float, optional
149
+ window duration in seconds, by default 5.0
150
+ verbose : bool, optional
151
+ by default False
152
+ normalize_db : float, optional
153
+ normalize db, by default -16
154
+
155
+ Returns
156
+ -------
157
+ DACFile
158
+ Object containing compressed codes and metadata
159
+ required for decompression
160
+ """
161
+ audio_signal = audio_path_or_signal
162
+ if isinstance(audio_signal, (str, Path)):
163
+ audio_signal = AudioSignal.load_from_file_with_ffmpeg(str(audio_signal))
164
+
165
+ self.eval()
166
+ original_padding = self.padding
167
+ original_device = audio_signal.device
168
+
169
+ audio_signal = audio_signal.clone()
170
+ audio_signal = audio_signal.to_mono()
171
+ original_sr = audio_signal.sample_rate
172
+
173
+ resample_fn = audio_signal.resample
174
+ loudness_fn = audio_signal.loudness
175
+
176
+ # If audio is > 10 minutes long, use the ffmpeg versions
177
+ if audio_signal.signal_duration >= 10 * 60 * 60:
178
+ resample_fn = audio_signal.ffmpeg_resample
179
+ loudness_fn = audio_signal.ffmpeg_loudness
180
+
181
+ original_length = audio_signal.signal_length
182
+ resample_fn(self.sample_rate)
183
+ input_db = loudness_fn()
184
+
185
+ if normalize_db is not None:
186
+ audio_signal.normalize(normalize_db)
187
+ audio_signal.ensure_max_of_audio()
188
+
189
+ nb, nac, nt = audio_signal.audio_data.shape
190
+ audio_signal.audio_data = audio_signal.audio_data.reshape(nb * nac, 1, nt)
191
+ win_duration = (
192
+ audio_signal.signal_duration if win_duration is None else win_duration
193
+ )
194
+
195
+ if audio_signal.signal_duration <= win_duration:
196
+ # Unchunked compression (used if signal length < win duration)
197
+ self.padding = True
198
+ n_samples = nt
199
+ hop = nt
200
+ else:
201
+ # Chunked inference
202
+ self.padding = False
203
+ # Zero-pad signal on either side by the delay
204
+ audio_signal.zero_pad(self.delay, self.delay)
205
+ n_samples = int(win_duration * self.sample_rate)
206
+ # Round n_samples to nearest hop length multiple
207
+ n_samples = int(math.ceil(n_samples / self.hop_length) * self.hop_length)
208
+ hop = self.get_output_length(n_samples)
209
+
210
+ codes = []
211
+ range_fn = range if not verbose else tqdm.trange
212
+
213
+ for i in range_fn(0, nt, hop):
214
+ x = audio_signal[..., i : i + n_samples]
215
+ x = x.zero_pad(0, max(0, n_samples - x.shape[-1]))
216
+
217
+ audio_data = x.audio_data.to(self.device)
218
+ audio_data = self.preprocess(audio_data, self.sample_rate)
219
+ _, c, _, _, _ = self.encode(audio_data, n_quantizers)
220
+ codes.append(c.to(original_device))
221
+ chunk_length = c.shape[-1]
222
+
223
+ codes = torch.cat(codes, dim=-1)
224
+
225
+ dac_file = DACFile(
226
+ codes=codes,
227
+ chunk_length=chunk_length,
228
+ original_length=original_length,
229
+ input_db=input_db,
230
+ channels=nac,
231
+ sample_rate=original_sr,
232
+ padding=self.padding,
233
+ dac_version=SUPPORTED_VERSIONS[-1],
234
+ )
235
+
236
+ if n_quantizers is not None:
237
+ codes = codes[:, :n_quantizers, :]
238
+
239
+ self.padding = original_padding
240
+ return dac_file
241
+
242
+ @torch.no_grad()
243
+ def decompress(
244
+ self,
245
+ obj: Union[str, Path, DACFile],
246
+ verbose: bool = False,
247
+ ) -> AudioSignal:
248
+ """Reconstruct audio from a given .dac file
249
+
250
+ Parameters
251
+ ----------
252
+ obj : Union[str, Path, DACFile]
253
+ .dac file location or corresponding DACFile object.
254
+ verbose : bool, optional
255
+ Prints progress if True, by default False
256
+
257
+ Returns
258
+ -------
259
+ AudioSignal
260
+ Object with the reconstructed audio
261
+ """
262
+ self.eval()
263
+ if isinstance(obj, (str, Path)):
264
+ obj = DACFile.load(obj)
265
+
266
+ original_padding = self.padding
267
+ self.padding = obj.padding
268
+
269
+ range_fn = range if not verbose else tqdm.trange
270
+ codes = obj.codes
271
+ original_device = codes.device
272
+ chunk_length = obj.chunk_length
273
+ recons = []
274
+
275
+ for i in range_fn(0, codes.shape[-1], chunk_length):
276
+ c = codes[..., i : i + chunk_length].to(self.device)
277
+ z = self.quantizer.from_codes(c)[0]
278
+ r = self.decode(z)
279
+ recons.append(r.to(original_device))
280
+
281
+ recons = torch.cat(recons, dim=-1)
282
+ recons = AudioSignal(recons, self.sample_rate)
283
+
284
+ resample_fn = recons.resample
285
+ loudness_fn = recons.loudness
286
+
287
+ # If audio is > 10 minutes long, use the ffmpeg versions
288
+ if recons.signal_duration >= 10 * 60 * 60:
289
+ resample_fn = recons.ffmpeg_resample
290
+ loudness_fn = recons.ffmpeg_loudness
291
+
292
+ if obj.input_db is not None:
293
+ recons.normalize(obj.input_db)
294
+
295
+ resample_fn(obj.sample_rate)
296
+
297
+ if obj.original_length is not None:
298
+ recons = recons[..., : obj.original_length]
299
+ loudness_fn()
300
+ recons.audio_data = recons.audio_data.reshape(
301
+ -1, obj.channels, obj.original_length
302
+ )
303
+ else:
304
+ loudness_fn()
305
+
306
+ self.padding = original_padding
307
+ return recons
308
+
309
+
310
+ def WNConv1d(*args, **kwargs):
311
+ return weight_norm(nn.Conv1d(*args, **kwargs))
312
+
313
+
314
+ def WNConvTranspose1d(*args, **kwargs):
315
+ return weight_norm(nn.ConvTranspose1d(*args, **kwargs))
316
+
317
+
318
+ # Scripting this brings model speed up 1.4x
319
+ @torch.jit.script
320
+ def snake(x, alpha):
321
+ shape = x.shape
322
+ x = x.reshape(shape[0], shape[1], -1)
323
+ x = x + (alpha + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2)
324
+ x = x.reshape(shape)
325
+ return x
326
+
327
+
328
+ class Snake1d(nn.Module):
329
+ def __init__(self, channels):
330
+ super().__init__()
331
+ self.alpha = nn.Parameter(torch.ones(1, channels, 1))
332
+
333
+ def forward(self, x):
334
+ return snake(x, self.alpha)
335
+
336
+
337
+ import torch.nn.functional as F
338
+ from einops import rearrange
339
+
340
+
341
+ class VectorQuantize(nn.Module):
342
+ """
343
+ Implementation of VQ similar to Karpathy's repo:
344
+ https://github.com/karpathy/deep-vector-quantization
345
+ Additionally uses following tricks from Improved VQGAN
346
+ (https://arxiv.org/pdf/2110.04627.pdf):
347
+ 1. Factorized codes: Perform nearest neighbor lookup in low-dimensional space
348
+ for improved codebook usage
349
+ 2. l2-normalized codes: Converts euclidean distance to cosine similarity which
350
+ improves training stability
351
+ """
352
+
353
+ def __init__(self, input_dim: int, codebook_size: int, codebook_dim: int):
354
+ super().__init__()
355
+ self.codebook_size = codebook_size
356
+ self.codebook_dim = codebook_dim
357
+
358
+ self.in_proj = WNConv1d(input_dim, codebook_dim, kernel_size=1)
359
+ self.out_proj = WNConv1d(codebook_dim, input_dim, kernel_size=1)
360
+ self.codebook = nn.Embedding(codebook_size, codebook_dim)
361
+
362
+ def forward(self, z):
363
+ """Quantized the input tensor using a fixed codebook and returns
364
+ the corresponding codebook vectors
365
+
366
+ Parameters
367
+ ----------
368
+ z : Tensor[B x D x T]
369
+
370
+ Returns
371
+ -------
372
+ Tensor[B x D x T]
373
+ Quantized continuous representation of input
374
+ Tensor[1]
375
+ Commitment loss to train encoder to predict vectors closer to codebook
376
+ entries
377
+ Tensor[1]
378
+ Codebook loss to update the codebook
379
+ Tensor[B x T]
380
+ Codebook indices (quantized discrete representation of input)
381
+ Tensor[B x D x T]
382
+ Projected latents (continuous representation of input before quantization)
383
+ """
384
+
385
+ # Factorized codes (ViT-VQGAN) Project input into low-dimensional space
386
+ z_e = self.in_proj(z) # z_e : (B x D x T)
387
+ z_q, indices = self.decode_latents(z_e)
388
+
389
+ commitment_loss = F.mse_loss(z_e, z_q.detach(), reduction="none").mean([1, 2])
390
+ codebook_loss = F.mse_loss(z_q, z_e.detach(), reduction="none").mean([1, 2])
391
+
392
+ z_q = (
393
+ z_e + (z_q - z_e).detach()
394
+ ) # noop in forward pass, straight-through gradient estimator in backward pass
395
+
396
+ z_q = self.out_proj(z_q)
397
+
398
+ return z_q, commitment_loss, codebook_loss, indices, z_e
399
+
400
+ def embed_code(self, embed_id):
401
+ return F.embedding(embed_id, self.codebook.weight)
402
+
403
+ def decode_code(self, embed_id):
404
+ return self.embed_code(embed_id).transpose(1, 2)
405
+
406
+ def decode_latents(self, latents):
407
+ encodings = rearrange(latents, "b d t -> (b t) d")
408
+ codebook = self.codebook.weight # codebook: (N x D)
409
+
410
+ # L2 normalize encodings and codebook (ViT-VQGAN)
411
+ encodings = F.normalize(encodings)
412
+ codebook = F.normalize(codebook)
413
+
414
+ # Compute euclidean distance with codebook
415
+ dist = (
416
+ encodings.pow(2).sum(1, keepdim=True)
417
+ - 2 * encodings @ codebook.t()
418
+ + codebook.pow(2).sum(1, keepdim=True).t()
419
+ )
420
+ indices = rearrange((-dist).max(1)[1], "(b t) -> b t", b=latents.size(0))
421
+ z_q = self.decode_code(indices)
422
+ return z_q, indices
423
+
424
+
425
+ class ResidualVectorQuantize(nn.Module):
426
+ """
427
+ Introduced in SoundStream: An end2end neural audio codec
428
+ https://arxiv.org/abs/2107.03312
429
+ """
430
+
431
+ def __init__(
432
+ self,
433
+ input_dim: int = 512,
434
+ n_codebooks: int = 9,
435
+ codebook_size: int = 1024,
436
+ codebook_dim: Union[int, list] = 8,
437
+ quantizer_dropout: float = 0.0,
438
+ ):
439
+ super().__init__()
440
+ if isinstance(codebook_dim, int):
441
+ codebook_dim = [codebook_dim for _ in range(n_codebooks)]
442
+
443
+ self.n_codebooks = n_codebooks
444
+ self.codebook_dim = codebook_dim
445
+ self.codebook_size = codebook_size
446
+
447
+ self.quantizers = nn.ModuleList(
448
+ [
449
+ VectorQuantize(input_dim, codebook_size, codebook_dim[i])
450
+ for i in range(n_codebooks)
451
+ ]
452
+ )
453
+ self.quantizer_dropout = quantizer_dropout
454
+
455
+ def forward(self, z, n_quantizers: int = None):
456
+ """Quantized the input tensor using a fixed set of `n` codebooks and returns
457
+ the corresponding codebook vectors
458
+ Parameters
459
+ ----------
460
+ z : Tensor[B x D x T]
461
+ n_quantizers : int, optional
462
+ No. of quantizers to use
463
+ (n_quantizers < self.n_codebooks ex: for quantizer dropout)
464
+ Note: if `self.quantizer_dropout` is True, this argument is ignored
465
+ when in training mode, and a random number of quantizers is used.
466
+ Returns
467
+ -------
468
+ dict
469
+ A dictionary with the following keys:
470
+
471
+ "z" : Tensor[B x D x T]
472
+ Quantized continuous representation of input
473
+ "codes" : Tensor[B x N x T]
474
+ Codebook indices for each codebook
475
+ (quantized discrete representation of input)
476
+ "latents" : Tensor[B x N*D x T]
477
+ Projected latents (continuous representation of input before quantization)
478
+ "vq/commitment_loss" : Tensor[1]
479
+ Commitment loss to train encoder to predict vectors closer to codebook
480
+ entries
481
+ "vq/codebook_loss" : Tensor[1]
482
+ Codebook loss to update the codebook
483
+ """
484
+ z_q = 0
485
+ residual = z
486
+ commitment_loss = 0
487
+ codebook_loss = 0
488
+
489
+ codebook_indices = []
490
+ latents = []
491
+
492
+ if n_quantizers is None:
493
+ n_quantizers = self.n_codebooks
494
+ if self.training:
495
+ n_quantizers = torch.ones((z.shape[0],)) * self.n_codebooks + 1
496
+ dropout = torch.randint(1, self.n_codebooks + 1, (z.shape[0],))
497
+ n_dropout = int(z.shape[0] * self.quantizer_dropout)
498
+ n_quantizers[:n_dropout] = dropout[:n_dropout]
499
+ n_quantizers = n_quantizers.to(z.device)
500
+
501
+ for i, quantizer in enumerate(self.quantizers):
502
+ if self.training is False and i >= n_quantizers:
503
+ break
504
+
505
+ z_q_i, commitment_loss_i, codebook_loss_i, indices_i, z_e_i = quantizer(
506
+ residual
507
+ )
508
+
509
+ # Create mask to apply quantizer dropout
510
+ mask = (
511
+ torch.full((z.shape[0],), fill_value=i, device=z.device) < n_quantizers
512
+ )
513
+ z_q = z_q + z_q_i * mask[:, None, None]
514
+ residual = residual - z_q_i
515
+
516
+ # Sum losses
517
+ commitment_loss += (commitment_loss_i * mask).mean()
518
+ codebook_loss += (codebook_loss_i * mask).mean()
519
+
520
+ codebook_indices.append(indices_i)
521
+ latents.append(z_e_i)
522
+
523
+ codes = torch.stack(codebook_indices, dim=1)
524
+ latents = torch.cat(latents, dim=1)
525
+
526
+ return z_q, codes, latents, commitment_loss, codebook_loss
527
+
528
+ def from_codes(self, codes: torch.Tensor):
529
+ """Given the quantized codes, reconstruct the continuous representation
530
+ Parameters
531
+ ----------
532
+ codes : Tensor[B x N x T]
533
+ Quantized discrete representation of input
534
+ Returns
535
+ -------
536
+ Tensor[B x D x T]
537
+ Quantized continuous representation of input
538
+ """
539
+ z_q = 0.0
540
+ z_p = []
541
+ n_codebooks = codes.shape[1]
542
+ for i in range(n_codebooks):
543
+ z_p_i = self.quantizers[i].decode_code(codes[:, i, :])
544
+ z_p.append(z_p_i)
545
+
546
+ z_q_i = self.quantizers[i].out_proj(z_p_i)
547
+ z_q = z_q + z_q_i
548
+ return z_q, torch.cat(z_p, dim=1), codes
549
+
550
+ def from_latents(self, latents: torch.Tensor):
551
+ """Given the unquantized latents, reconstruct the
552
+ continuous representation after quantization.
553
+
554
+ Parameters
555
+ ----------
556
+ latents : Tensor[B x N x T]
557
+ Continuous representation of input after projection
558
+
559
+ Returns
560
+ -------
561
+ Tensor[B x D x T]
562
+ Quantized representation of full-projected space
563
+ Tensor[B x D x T]
564
+ Quantized representation of latent space
565
+ """
566
+ z_q = 0
567
+ z_p = []
568
+ codes = []
569
+ dims = np.cumsum([0] + [q.codebook_dim for q in self.quantizers])
570
+
571
+ n_codebooks = np.where(dims <= latents.shape[1])[0].max(axis=0, keepdims=True)[
572
+ 0
573
+ ]
574
+ for i in range(n_codebooks):
575
+ j, k = dims[i], dims[i + 1]
576
+ z_p_i, codes_i = self.quantizers[i].decode_latents(latents[:, j:k, :])
577
+ z_p.append(z_p_i)
578
+ codes.append(codes_i)
579
+
580
+ z_q_i = self.quantizers[i].out_proj(z_p_i)
581
+ z_q = z_q + z_q_i
582
+
583
+ return z_q, torch.cat(z_p, dim=1), torch.stack(codes, dim=1)
584
+
585
+
586
+ class AbstractDistribution:
587
+ def sample(self):
588
+ raise NotImplementedError()
589
+
590
+ def mode(self):
591
+ raise NotImplementedError()
592
+
593
+
594
+ class DiracDistribution(AbstractDistribution):
595
+ def __init__(self, value):
596
+ self.value = value
597
+
598
+ def sample(self):
599
+ return self.value
600
+
601
+ def mode(self):
602
+ return self.value
603
+
604
+
605
+ class DiagonalGaussianDistribution(object):
606
+ def __init__(self, parameters, deterministic=False):
607
+ self.parameters = parameters
608
+ self.mean, self.logvar = torch.chunk(parameters, 2, dim=1)
609
+ self.logvar = torch.clamp(self.logvar, -30.0, 20.0)
610
+ self.deterministic = deterministic
611
+ self.std = torch.exp(0.5 * self.logvar)
612
+ self.var = torch.exp(self.logvar)
613
+ if self.deterministic:
614
+ self.var = self.std = torch.zeros_like(self.mean).to(device=self.parameters.device)
615
+
616
+ def sample(self):
617
+ x = self.mean + self.std * torch.randn(self.mean.shape).to(device=self.parameters.device)
618
+ return x
619
+
620
+ def kl(self, other=None):
621
+ if self.deterministic:
622
+ return torch.Tensor([0.0])
623
+ else:
624
+ if other is None:
625
+ return 0.5 * torch.mean(
626
+ torch.pow(self.mean, 2) + self.var - 1.0 - self.logvar,
627
+ dim=[1, 2],
628
+ )
629
+ else:
630
+ return 0.5 * torch.mean(
631
+ torch.pow(self.mean - other.mean, 2) / other.var
632
+ + self.var / other.var
633
+ - 1.0
634
+ - self.logvar
635
+ + other.logvar,
636
+ dim=[1, 2],
637
+ )
638
+
639
+ def nll(self, sample, dims=[1, 2]):
640
+ if self.deterministic:
641
+ return torch.Tensor([0.0])
642
+ logtwopi = np.log(2.0 * np.pi)
643
+ return 0.5 * torch.sum(
644
+ logtwopi + self.logvar + torch.pow(sample - self.mean, 2) / self.var,
645
+ dim=dims,
646
+ )
647
+
648
+ def mode(self):
649
+ return self.mean
650
+
651
+
652
+ def normal_kl(mean1, logvar1, mean2, logvar2):
653
+ """
654
+ source: https://github.com/openai/guided-diffusion/blob/27c20a8fab9cb472df5d6bdd6c8d11c8f430b924/guided_diffusion/losses.py#L12
655
+ Compute the KL divergence between two gaussians.
656
+ Shapes are automatically broadcasted, so batches can be compared to
657
+ scalars, among other use cases.
658
+ """
659
+ tensor = None
660
+ for obj in (mean1, logvar1, mean2, logvar2):
661
+ if isinstance(obj, torch.Tensor):
662
+ tensor = obj
663
+ break
664
+ assert tensor is not None, "at least one argument must be a Tensor"
665
+
666
+ # Force variances to be Tensors. Broadcasting helps convert scalars to
667
+ # Tensors, but it does not work for torch.exp().
668
+ logvar1, logvar2 = [x if isinstance(x, torch.Tensor) else torch.tensor(x).to(tensor) for x in (logvar1, logvar2)]
669
+
670
+ return 0.5 * (
671
+ -1.0 + logvar2 - logvar1 + torch.exp(logvar1 - logvar2) + ((mean1 - mean2) ** 2) * torch.exp(-logvar2)
672
+ )
673
+
674
+
675
+ def init_weights(m):
676
+ if isinstance(m, nn.Conv1d):
677
+ nn.init.trunc_normal_(m.weight, std=0.02)
678
+ nn.init.constant_(m.bias, 0)
679
+
680
+
681
+ class ResidualUnit(nn.Module):
682
+ def __init__(self, dim: int = 16, dilation: int = 1):
683
+ super().__init__()
684
+ pad = ((7 - 1) * dilation) // 2
685
+ self.block = nn.Sequential(
686
+ Snake1d(dim),
687
+ WNConv1d(dim, dim, kernel_size=7, dilation=dilation, padding=pad),
688
+ Snake1d(dim),
689
+ WNConv1d(dim, dim, kernel_size=1),
690
+ )
691
+
692
+ def forward(self, x):
693
+ y = self.block(x)
694
+ pad = (x.shape[-1] - y.shape[-1]) // 2
695
+ if pad > 0:
696
+ x = x[..., pad:-pad]
697
+ return x + y
698
+
699
+
700
+ class EncoderBlock(nn.Module):
701
+ def __init__(self, dim: int = 16, stride: int = 1):
702
+ super().__init__()
703
+ self.block = nn.Sequential(
704
+ ResidualUnit(dim // 2, dilation=1),
705
+ ResidualUnit(dim // 2, dilation=3),
706
+ ResidualUnit(dim // 2, dilation=9),
707
+ Snake1d(dim // 2),
708
+ WNConv1d(
709
+ dim // 2,
710
+ dim,
711
+ kernel_size=2 * stride,
712
+ stride=stride,
713
+ padding=math.ceil(stride / 2),
714
+ ),
715
+ )
716
+
717
+ def forward(self, x):
718
+ return self.block(x)
719
+
720
+
721
+ class Encoder(nn.Module):
722
+ def __init__(
723
+ self,
724
+ d_model: int = 64,
725
+ strides: list = [2, 4, 8, 8],
726
+ d_latent: int = 64,
727
+ ):
728
+ super().__init__()
729
+ # Create first convolution
730
+ self.block = [WNConv1d(1, d_model, kernel_size=7, padding=3)]
731
+
732
+ # Create EncoderBlocks that double channels as they downsample by `stride`
733
+ for stride in strides:
734
+ d_model *= 2
735
+ self.block += [EncoderBlock(d_model, stride=stride)]
736
+
737
+ # Create last convolution
738
+ self.block += [
739
+ Snake1d(d_model),
740
+ WNConv1d(d_model, d_latent, kernel_size=3, padding=1),
741
+ ]
742
+
743
+ # Wrap black into nn.Sequential
744
+ self.block = nn.Sequential(*self.block)
745
+ self.enc_dim = d_model
746
+
747
+ def forward(self, x):
748
+ return self.block(x)
749
+
750
+
751
+ class DecoderBlock(nn.Module):
752
+ def __init__(self, input_dim: int = 16, output_dim: int = 8, stride: int = 1):
753
+ super().__init__()
754
+ self.block = nn.Sequential(
755
+ Snake1d(input_dim),
756
+ WNConvTranspose1d(
757
+ input_dim,
758
+ output_dim,
759
+ kernel_size=2 * stride,
760
+ stride=stride,
761
+ padding=math.ceil(stride / 2),
762
+ output_padding=stride % 2,
763
+ ),
764
+ ResidualUnit(output_dim, dilation=1),
765
+ ResidualUnit(output_dim, dilation=3),
766
+ ResidualUnit(output_dim, dilation=9),
767
+ )
768
+
769
+ def forward(self, x):
770
+ return self.block(x)
771
+
772
+
773
+ class Decoder(nn.Module):
774
+ def __init__(
775
+ self,
776
+ input_channel,
777
+ channels,
778
+ rates,
779
+ d_out: int = 1,
780
+ ):
781
+ super().__init__()
782
+
783
+ # Add first conv layer
784
+ layers = [WNConv1d(input_channel, channels, kernel_size=7, padding=3)]
785
+
786
+ # Add upsampling + MRF blocks
787
+ for i, stride in enumerate(rates):
788
+ input_dim = channels // 2**i
789
+ output_dim = channels // 2 ** (i + 1)
790
+ layers += [DecoderBlock(input_dim, output_dim, stride)]
791
+
792
+ # Add final conv layer
793
+ layers += [
794
+ Snake1d(output_dim),
795
+ WNConv1d(output_dim, d_out, kernel_size=7, padding=3),
796
+ nn.Tanh(),
797
+ ]
798
+
799
+ self.model = nn.Sequential(*layers)
800
+
801
+ def forward(self, x):
802
+ return self.model(x)
803
+
804
+
805
+ class DAC(BaseModel, CodecMixin, ModelMixin, ConfigMixin):
806
+
807
+ @register_to_config
808
+ def __init__(
809
+ self,
810
+ encoder_dim: int = 64,
811
+ encoder_rates: List[int] = [2, 4, 8, 8],
812
+ latent_dim: int = None,
813
+ decoder_dim: int = 1536,
814
+ decoder_rates: List[int] = [8, 8, 4, 2],
815
+ n_codebooks: int = 9,
816
+ codebook_size: int = 1024,
817
+ codebook_dim: Union[int, list] = 8,
818
+ quantizer_dropout: bool = False,
819
+ sample_rate: int = 44100,
820
+ continuous: bool = False,
821
+ use_weight_norm: bool = True,
822
+ ):
823
+ super().__init__()
824
+
825
+ self.encoder_dim = encoder_dim
826
+ self.encoder_rates = encoder_rates
827
+ self.decoder_dim = decoder_dim
828
+ self.decoder_rates = decoder_rates
829
+ self.sample_rate = sample_rate
830
+ self.continuous = continuous
831
+ self.use_weight_norm = use_weight_norm
832
+
833
+ if latent_dim is None:
834
+ latent_dim = encoder_dim * (2 ** len(encoder_rates))
835
+
836
+ self.latent_dim = latent_dim
837
+
838
+ self.hop_length = np.prod(encoder_rates)
839
+ self.encoder = Encoder(encoder_dim, encoder_rates, latent_dim)
840
+
841
+ if not continuous:
842
+ self.n_codebooks = n_codebooks
843
+ self.codebook_size = codebook_size
844
+ self.codebook_dim = codebook_dim
845
+ self.quantizer = ResidualVectorQuantize(
846
+ input_dim=latent_dim,
847
+ n_codebooks=n_codebooks,
848
+ codebook_size=codebook_size,
849
+ codebook_dim=codebook_dim,
850
+ quantizer_dropout=quantizer_dropout,
851
+ )
852
+ else:
853
+ self.quant_conv = torch.nn.Conv1d(latent_dim, 2 * latent_dim, 1)
854
+ self.post_quant_conv = torch.nn.Conv1d(latent_dim, latent_dim, 1)
855
+
856
+ self.decoder = Decoder(
857
+ latent_dim,
858
+ decoder_dim,
859
+ decoder_rates,
860
+ )
861
+ self.sample_rate = sample_rate
862
+ self.apply(init_weights)
863
+
864
+ self.delay = self.get_delay()
865
+
866
+ if not self.use_weight_norm:
867
+ self.remove_weight_norm()
868
+
869
+ @property
870
+ def dtype(self):
871
+ """Get the dtype of the model parameters."""
872
+ # Return the dtype of the first parameter found
873
+ for param in self.parameters():
874
+ return param.dtype
875
+ return torch.float32 # fallback
876
+
877
+ @property
878
+ def device(self):
879
+ """Get the device of the model parameters."""
880
+ # Return the device of the first parameter found
881
+ for param in self.parameters():
882
+ return param.device
883
+ return torch.device('cpu') # fallback
884
+
885
+ def preprocess(self, audio_data, sample_rate):
886
+ if sample_rate is None:
887
+ sample_rate = self.sample_rate
888
+ assert sample_rate == self.sample_rate
889
+
890
+ length = audio_data.shape[-1]
891
+ right_pad = math.ceil(length / self.hop_length) * self.hop_length - length
892
+ audio_data = nn.functional.pad(audio_data, (0, right_pad))
893
+
894
+ return audio_data
895
+
896
+ @apply_forward_hook
897
+ def encode(
898
+ self,
899
+ audio_data: torch.Tensor,
900
+ n_quantizers: int = None,
901
+ ):
902
+ """Encode given audio data and return quantized latent codes
903
+
904
+ Parameters
905
+ ----------
906
+ audio_data : Tensor[B x 1 x T]
907
+ Audio data to encode
908
+ n_quantizers : int, optional
909
+ Number of quantizers to use, by default None
910
+ If None, all quantizers are used.
911
+
912
+ Returns
913
+ -------
914
+ dict
915
+ A dictionary with the following keys:
916
+ "z" : Tensor[B x D x T]
917
+ Quantized continuous representation of input
918
+ "codes" : Tensor[B x N x T]
919
+ Codebook indices for each codebook
920
+ (quantized discrete representation of input)
921
+ "latents" : Tensor[B x N*D x T]
922
+ Projected latents (continuous representation of input before quantization)
923
+ "vq/commitment_loss" : Tensor[1]
924
+ Commitment loss to train encoder to predict vectors closer to codebook
925
+ entries
926
+ "vq/codebook_loss" : Tensor[1]
927
+ Codebook loss to update the codebook
928
+ "length" : int
929
+ Number of samples in input audio
930
+ """
931
+ z = self.encoder(audio_data) # [B x D x T]
932
+ if not self.continuous:
933
+ z, codes, latents, commitment_loss, codebook_loss = self.quantizer(z, n_quantizers)
934
+ else:
935
+ z = self.quant_conv(z) # [B x 2D x T]
936
+ z = DiagonalGaussianDistribution(z)
937
+ codes, latents, commitment_loss, codebook_loss = None, None, 0, 0
938
+
939
+ return z, codes, latents, commitment_loss, codebook_loss
940
+
941
+ @apply_forward_hook
942
+ def decode(self, z: torch.Tensor):
943
+ """Decode given latent codes and return audio data
944
+
945
+ Parameters
946
+ ----------
947
+ z : Tensor[B x D x T]
948
+ Quantized continuous representation of input
949
+ length : int, optional
950
+ Number of samples in output audio, by default None
951
+
952
+ Returns
953
+ -------
954
+ dict
955
+ A dictionary with the following keys:
956
+ "audio" : Tensor[B x 1 x length]
957
+ Decoded audio data.
958
+ """
959
+ if not self.continuous:
960
+ audio = self.decoder(z)
961
+ else:
962
+ z = self.post_quant_conv(z)
963
+ audio = self.decoder(z)
964
+
965
+ return audio
966
+
967
+ def forward(
968
+ self,
969
+ audio_data: torch.Tensor,
970
+ sample_rate: int = None,
971
+ n_quantizers: int = None,
972
+ ):
973
+ """Model forward pass
974
+
975
+ Parameters
976
+ ----------
977
+ audio_data : Tensor[B x 1 x T]
978
+ Audio data to encode
979
+ sample_rate : int, optional
980
+ Sample rate of audio data in Hz, by default None
981
+ If None, defaults to `self.sample_rate`
982
+ n_quantizers : int, optional
983
+ Number of quantizers to use, by default None.
984
+ If None, all quantizers are used.
985
+
986
+ Returns
987
+ -------
988
+ dict
989
+ A dictionary with the following keys:
990
+ "z" : Tensor[B x D x T]
991
+ Quantized continuous representation of input
992
+ "codes" : Tensor[B x N x T]
993
+ Codebook indices for each codebook
994
+ (quantized discrete representation of input)
995
+ "latents" : Tensor[B x N*D x T]
996
+ Projected latents (continuous representation of input before quantization)
997
+ "vq/commitment_loss" : Tensor[1]
998
+ Commitment loss to train encoder to predict vectors closer to codebook
999
+ entries
1000
+ "vq/codebook_loss" : Tensor[1]
1001
+ Codebook loss to update the codebook
1002
+ "length" : int
1003
+ Number of samples in input audio
1004
+ "audio" : Tensor[B x 1 x length]
1005
+ Decoded audio data.
1006
+ """
1007
+ length = audio_data.shape[-1]
1008
+ audio_data = self.preprocess(audio_data, sample_rate)
1009
+ if not self.continuous:
1010
+ z, codes, latents, commitment_loss, codebook_loss = self.encode(audio_data, n_quantizers)
1011
+
1012
+ x = self.decode(z)
1013
+ return {
1014
+ "audio": x[..., :length],
1015
+ "z": z,
1016
+ "codes": codes,
1017
+ "latents": latents,
1018
+ "vq/commitment_loss": commitment_loss,
1019
+ "vq/codebook_loss": codebook_loss,
1020
+ }
1021
+ else:
1022
+ posterior, _, _, _, _ = self.encode(audio_data, n_quantizers)
1023
+ z = posterior.sample()
1024
+ x = self.decode(z)
1025
+
1026
+ kl_loss = posterior.kl()
1027
+ kl_loss = kl_loss.mean()
1028
+
1029
+ return {
1030
+ "audio": x[..., :length],
1031
+ "z": z,
1032
+ "kl_loss": kl_loss,
1033
+ }
1034
+
1035
+ def remove_weight_norm(self):
1036
+ """
1037
+ Remove weight_norm from all modules in the model.
1038
+ This fuses the weight_g and weight_v parameters into a single weight parameter.
1039
+ Should be called before inference for better performance.
1040
+ Returns:
1041
+ self: The model with weight_norm removed
1042
+ """
1043
+ from torch.nn.utils import remove_weight_norm
1044
+ num_removed = 0
1045
+ for name, module in list(self.named_modules()):
1046
+ if hasattr(module, "_forward_pre_hooks"):
1047
+ for hook_id, hook in list(module._forward_pre_hooks.items()):
1048
+ if "WeightNorm" in str(type(hook)):
1049
+ try:
1050
+ remove_weight_norm(module)
1051
+ num_removed += 1
1052
+ # print(f"Removed weight_norm from: {name}")
1053
+ except ValueError as e:
1054
+ print(f"Failed to remove weight_norm from {name}: {e}")
1055
+ if num_removed > 0:
1056
+ print(f"Successfully removed weight_norm from {num_removed} modules")
1057
+ self.use_weight_norm = False
1058
+ self.register_to_config(use_weight_norm=False)
1059
+ else:
1060
+ print("No weight_norm found in the model")
1061
+ return self
1062
+
1063
+ if __name__ == "__main__":
1064
+ from functools import partial
1065
+
1066
+ import numpy as np
1067
+
1068
+ model = DAC().to("cpu")
1069
+
1070
+ for n, m in model.named_modules():
1071
+ o = m.extra_repr()
1072
+ p = sum([np.prod(p.size()) for p in m.parameters()])
1073
+ fn = lambda o, p: o + f" {p/1e6:<.3f}M params."
1074
+ setattr(m, "extra_repr", partial(fn, o=o, p=p))
1075
+ print(model)
1076
+ print("Total # of params: ", sum([np.prod(p.size()) for p in model.parameters()]))
1077
+
1078
+ length = 88200 * 2
1079
+ x = torch.randn(1, 1, length).to(model.device)
1080
+ x.requires_grad_(True)
1081
+ x.retain_grad()
1082
+
1083
+ # Make a forward pass
1084
+ out = model(x)["audio"]
1085
+ print("Input shape:", x.shape)
1086
+ print("Output shape:", out.shape)
1087
+
1088
+ # Create gradient variable
1089
+ grad = torch.zeros_like(out)
1090
+ grad[:, :, grad.shape[-1] // 2] = 1
1091
+
1092
+ # Make a backward pass
1093
+ out.backward(grad)
1094
+
1095
+ # Check non-zero values
1096
+ gradmap = x.grad.squeeze(0)
1097
+ gradmap = (gradmap != 0).sum(0) # sum across features
1098
+ rf = (gradmap != 0).sum()
1099
+
1100
+ print(f"Receptive field: {rf.item()}")
1101
+
1102
+ x = AudioSignal(torch.randn(1, 1, 44100 * 60), 44100)
1103
+ model.decompress(model.compress(x, verbose=True), verbose=True)
hymm/models/modules/interactionv2.py ADDED
@@ -0,0 +1,708 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ from typing import Dict, List, Tuple, Optional
5
+
6
+ from einops import rearrange
7
+
8
+ from .wan_video_dit import RMSNorm, AttentionModule, flash_attention
9
+ from hymm.utils.parallel_states import get_sequence_parallel_state, nccl_info, sp_seq_ctx
10
+ from hymm.utils.communications import (
11
+ all_to_all_4D,
12
+ sp_pad_num_heads, sp_pad_heads, sp_num_local_real_heads,
13
+ )
14
+
15
+ class RotaryEmbedding(nn.Module):
16
+ inv_freq: torch.Tensor # fix linting for `register_buffer`
17
+
18
+ def __init__(self, base: float, dim: int, device=None):
19
+ super().__init__()
20
+ self.base = base
21
+ self.dim = dim
22
+ self.attention_scaling = 1.0
23
+
24
+ inv_freq = 1.0 / (
25
+ base ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim))
26
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
27
+ self.original_inv_freq = self.inv_freq
28
+
29
+ @torch.no_grad()
30
+ def forward(self, x, position_ids):
31
+ inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device)
32
+ position_ids_expanded = position_ids[:, None, :].float()
33
+
34
+ device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu"
35
+ with torch.autocast(device_type=device_type, enabled=False): # Force float32
36
+ freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
37
+ emb = torch.cat((freqs, freqs), dim=-1)
38
+ cos = emb.cos() * self.attention_scaling
39
+ sin = emb.sin() * self.attention_scaling
40
+
41
+ return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
42
+
43
+
44
+ def rotate_half(x):
45
+ """Rotates half the hidden dims of the input."""
46
+ x1 = x[..., : x.shape[-1] // 2]
47
+ x2 = x[..., x.shape[-1] // 2:]
48
+ return torch.cat((-x2, x1), dim=-1)
49
+
50
+
51
+ @torch.compile(fullgraph=True)
52
+ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
53
+ """Applies Rotary Position Embedding to the query and key tensors.
54
+
55
+ Args:
56
+ q (`torch.Tensor`): The query tensor.
57
+ k (`torch.Tensor`): The key tensor.
58
+ cos (`torch.Tensor`): The cosine part of the rotary embedding.
59
+ sin (`torch.Tensor`): The sine part of the rotary embedding.
60
+ position_ids (`torch.Tensor`, *optional*):
61
+ Deprecated and unused.
62
+ unsqueeze_dim (`int`, *optional*, defaults to 1):
63
+ The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and
64
+ sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note
65
+ that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and
66
+ k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes
67
+ cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have
68
+ the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2.
69
+ Returns:
70
+ `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding.
71
+ """
72
+ cos = cos.unsqueeze(unsqueeze_dim)
73
+ sin = sin.unsqueeze(unsqueeze_dim)
74
+ q_embed = (q * cos) + (rotate_half(q) * sin)
75
+ k_embed = (k * cos) + (rotate_half(k) * sin)
76
+ return q_embed, k_embed
77
+
78
+
79
+ class PerFrameAttentionPooling(nn.Module):
80
+ """
81
+ Per-frame multi-head attention pooling.
82
+
83
+ Given a flattened token sequence [B, L, D] and grid size (T, H, W), perform a
84
+ single-query attention pooling over the H*W tokens for each time frame, producing
85
+ [B, T, D].
86
+
87
+ Inspired by SigLIP's Multihead Attention Pooling head (without MLP/residual stack).
88
+ """
89
+
90
+ def __init__(self, dim: int, num_heads: int, eps: float = 1e-6):
91
+ super().__init__()
92
+ assert dim % num_heads == 0, "dim must be divisible by num_heads"
93
+ self.dim = dim
94
+ self.num_heads = num_heads
95
+
96
+ self.probe = nn.Parameter(torch.randn(1, 1, dim))
97
+ nn.init.normal_(self.probe, std=0.02)
98
+
99
+ self.attention = nn.MultiheadAttention(embed_dim=dim, num_heads=num_heads, batch_first=True)
100
+ self.layernorm = nn.LayerNorm(dim, eps=eps)
101
+
102
+ def forward(self, x: torch.Tensor, grid_size: Tuple[int, int, int]) -> torch.Tensor:
103
+ """
104
+ Args:
105
+ x: [B, L, D], where L = T*H*W
106
+ grid_size: (T, H, W)
107
+ Returns:
108
+ pooled: [B, T, D]
109
+ """
110
+ B, L, D = x.shape
111
+ T, H, W = grid_size
112
+ assert D == self.dim, f"Channel dimension mismatch: D={D} vs dim={self.dim}"
113
+ assert L == T * H * W, f"Flattened length mismatch: L={L} vs T*H*W={T * H * W}"
114
+
115
+ S = H * W
116
+ # Re-arrange tokens grouped by frame.
117
+ x_bt_s_d = x.view(B, T, S, D).contiguous().view(B * T, S, D) # [B*T, S, D]
118
+
119
+ # A learnable probe as the query (one query per frame).
120
+ probe = self.probe.expand(B * T, -1, -1) # [B*T, 1, D]
121
+
122
+ # Attention pooling: query=probe, key/value=H*W tokens within the frame.
123
+ pooled_bt_1_d = self.attention(probe, x_bt_s_d, x_bt_s_d, need_weights=False)[0] # [B*T, 1, D]
124
+ pooled_bt_d = pooled_bt_1_d.squeeze(1) # [B*T, D]
125
+
126
+ # Restore to [B, T, D].
127
+ pooled = pooled_bt_d.view(B, T, D)
128
+ pooled = self.layernorm(pooled)
129
+ return pooled
130
+
131
+
132
+ class CrossModalInteractionController:
133
+ """
134
+ Strategy class that controls interactions between two towers.
135
+ Manages the interaction mapping between visual DiT (e.g. 30 layers) and audio DiT (e.g. 30 layers).
136
+ """
137
+
138
+ def __init__(self, visual_layers: int = 30, audio_layers: int = 30):
139
+ self.visual_layers = visual_layers
140
+ self.audio_layers = audio_layers
141
+ self.min_layers = min(visual_layers, audio_layers)
142
+
143
+ def get_interaction_layers(self, strategy: str = "shallow_focus") -> Dict[str, List[Tuple[int, int]]]:
144
+ """
145
+ Get interaction layer mappings.
146
+
147
+ Args:
148
+ strategy: interaction strategy
149
+ - "shallow_focus": emphasize shallow layers to avoid deep-layer asymmetry
150
+ - "distributed": distributed interactions across the network
151
+ - "progressive": dense shallow interactions, sparse deeper interactions
152
+ - "custom": custom interaction layers
153
+
154
+ Returns:
155
+ A dict containing mappings for 'v2a' (visual -> audio) and 'a2v' (audio -> visual).
156
+ """
157
+
158
+ if strategy == "shallow_focus":
159
+ # Emphasize the first ~1/3 layers to avoid deep-layer asymmetry.
160
+ num_interact = min(10, self.min_layers // 3)
161
+ interact_layers = list(range(0, num_interact))
162
+
163
+ elif strategy == "distributed":
164
+ # Distribute interactions across the network (every few layers).
165
+ step = 3
166
+ interact_layers = list(range(0, self.min_layers, step))
167
+
168
+ elif strategy == "progressive":
169
+ # Progressive: dense shallow interactions, sparse deeper interactions.
170
+ shallow = list(range(0, min(8, self.min_layers))) # Dense for the first 8 layers.
171
+ if self.min_layers > 8:
172
+ deep = list(range(8, self.min_layers, 3)) # Every 3 layers afterwards.
173
+ interact_layers = shallow + deep
174
+ else:
175
+ interact_layers = shallow
176
+
177
+ elif strategy == "custom":
178
+ # Custom strategy: adjust as needed.
179
+ interact_layers = [0, 2, 4, 6, 8, 12, 16, 20] # Explicit layer indices.
180
+ interact_layers = [i for i in interact_layers if i < self.min_layers]
181
+
182
+ elif strategy == "full":
183
+ interact_layers = list(range(0, self.min_layers))
184
+
185
+ else:
186
+ raise ValueError(f"Unknown interaction strategy: {strategy}")
187
+
188
+ # Build bidirectional mapping.
189
+ mapping = {
190
+ 'v2a': [(i, i) for i in interact_layers], # visual layer i -> audio layer i
191
+ 'a2v': [(i, i) for i in interact_layers] # audio layer i -> visual layer i
192
+ }
193
+
194
+ return mapping
195
+
196
+ def should_interact(self, layer_idx: int, direction: str, interaction_mapping: Dict) -> bool:
197
+ """
198
+ Check whether a given layer should interact.
199
+
200
+ Args:
201
+ layer_idx: current layer index
202
+ direction: interaction direction ('v2a' or 'a2v')
203
+ interaction_mapping: interaction mapping table
204
+
205
+ Returns:
206
+ bool: whether to interact
207
+ """
208
+ if direction not in interaction_mapping:
209
+ return False
210
+
211
+ return any(src == layer_idx for src, _ in interaction_mapping[direction])
212
+
213
+
214
+ class ConditionalCrossAttention(nn.Module):
215
+ def __init__(self, dim: int, kv_dim: int, num_heads: int, eps: float = 1e-6):
216
+ super().__init__()
217
+ self.q_dim = dim
218
+ self.kv_dim = kv_dim
219
+ self.num_heads = num_heads
220
+ self.head_dim = self.q_dim // num_heads
221
+
222
+ self.q = nn.Linear(dim, dim)
223
+ self.k = nn.Linear(kv_dim, dim)
224
+ self.v = nn.Linear(kv_dim, dim)
225
+ self.o = nn.Linear(dim, dim)
226
+ self.norm_q = RMSNorm(dim, eps=eps)
227
+ self.norm_k = RMSNorm(dim, eps=eps)
228
+
229
+ self.attn = AttentionModule(self.num_heads)
230
+
231
+ self.enable_bsa = False
232
+ self.bsa_params = {}
233
+
234
+ # Cross-modal bridge: q and k/v come from two differently-sized SP-sharded
235
+ # streams, so each needs its own global length for the Ulysses all-to-all.
236
+ # MOVABridge sets these per direction (a2v: q=visual/kv=audio, v2a: swapped).
237
+ self.sp_q_stream = "visual"
238
+ self.sp_kv_stream = "audio"
239
+
240
+ def _run_cross_bsa(self, q_bhsd, k_bhsd, v_bhsd, q_structure, k_structure,
241
+ q_grid_size=None, k_grid_size=None):
242
+ """Run cross-modal BSA between video (3D) and audio (1D)."""
243
+ from hymm.models.modules.block_sparse_attention import flash_attn_bsa_cross
244
+ return flash_attn_bsa_cross(
245
+ q_bhsd, k_bhsd, v_bhsd,
246
+ q_structure=q_structure,
247
+ k_structure=k_structure,
248
+ q_grid_size=q_grid_size,
249
+ k_grid_size=k_grid_size,
250
+ sparsity=self.bsa_params.get('sparsity', 0.9375),
251
+ cdf_threshold=self.bsa_params.get('cdf_threshold', None),
252
+ chunk_size_q=self.bsa_params.get('chunk_size_q', 64),
253
+ chunk_size_k=self.bsa_params.get('chunk_size_k', 64),
254
+ chunk_3d_shape_q=self.bsa_params.get('chunk_3d_shape_q', [4, 4, 4]),
255
+ chunk_3d_shape_k=self.bsa_params.get('chunk_3d_shape_k', [4, 4, 4]),
256
+ )
257
+
258
+ def forward(self, x: torch.Tensor, y: torch.Tensor, x_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
259
+ y_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
260
+ q_structure: str = "1d", k_structure: str = "1d",
261
+ q_grid_size=None, k_grid_size=None):
262
+ ctx = y
263
+ q = self.norm_q(self.q(x))
264
+ k = self.norm_k(self.k(ctx))
265
+ v = self.v(ctx)
266
+ if x_freqs is not None:
267
+ x_cos, x_sin = x_freqs
268
+ q_view = rearrange(q, 'b l (h d) -> b l h d', d=self.head_dim)
269
+ x_cos = x_cos.to(q_view.dtype).to(q_view.device)
270
+ x_sin = x_sin.to(q_view.dtype).to(q_view.device)
271
+ q_view, _ = apply_rotary_pos_emb(q_view, q_view, x_cos, x_sin, unsqueeze_dim=2)
272
+ q = rearrange(q_view, 'b l h d -> b l (h d)')
273
+ if y_freqs is not None:
274
+ y_cos, y_sin = y_freqs
275
+ k_view = rearrange(k, 'b l (h d) -> b l h d', d=self.head_dim)
276
+ y_cos = y_cos.to(k_view.dtype).to(k_view.device)
277
+ y_sin = y_sin.to(k_view.dtype).to(k_view.device)
278
+ _, k_view = apply_rotary_pos_emb(k_view, k_view, y_cos, y_sin, unsqueeze_dim=2)
279
+ k = rearrange(k_view, 'b l h d -> b l (h d)')
280
+
281
+ use_bsa = self.enable_bsa and self.bsa_params.get('sparsity', 0) > 0
282
+
283
+ if get_sequence_parallel_state():
284
+ sp_size = nccl_info.sp_size
285
+ sp_rank = nccl_info.rank_within_spgroup
286
+ # Same Ulysses head-padding contract as the DiT attentions. v2a bridge
287
+ # operates in the audio 12-head space, so with sp_size=8 the head dim is
288
+ # padded to 16; padded heads are sliced off before the BSA cross kernel
289
+ # / flash and truncated after, fully isolated from the real heads.
290
+ padded_heads = sp_pad_num_heads(self.num_heads, sp_size)
291
+
292
+ q, k, v = [rearrange(t, "b s (n d) -> b s n d", n=self.num_heads) for t in (q, k, v)]
293
+ if padded_heads != self.num_heads:
294
+ q = sp_pad_heads(q, padded_heads)
295
+ k = sp_pad_heads(k, padded_heads)
296
+ v = sp_pad_heads(v, padded_heads)
297
+
298
+ q_full_len = sp_seq_ctx.get(self.sp_q_stream)
299
+ kv_full_len = sp_seq_ctx.get(self.sp_kv_stream)
300
+ q = all_to_all_4D(q, nccl_info.sp_group, scatter_dim=2, gather_dim=1,
301
+ full_seq_len=q_full_len)
302
+ k = all_to_all_4D(k, nccl_info.sp_group, scatter_dim=2, gather_dim=1,
303
+ full_seq_len=kv_full_len)
304
+ v = all_to_all_4D(v, nccl_info.sp_group, scatter_dim=2, gather_dim=1,
305
+ full_seq_len=kv_full_len)
306
+
307
+ hpr = q.shape[2]
308
+ local_heads = sp_num_local_real_heads(self.num_heads, padded_heads, sp_size, sp_rank)
309
+ if local_heads > 0:
310
+ if local_heads != hpr:
311
+ q = q[:, :, :local_heads, :]
312
+ k = k[:, :, :local_heads, :]
313
+ v = v[:, :, :local_heads, :]
314
+ if use_bsa:
315
+ q_bsa = q.permute(0, 2, 1, 3).contiguous()
316
+ k_bsa = k.permute(0, 2, 1, 3).contiguous()
317
+ v_bsa = v.permute(0, 2, 1, 3).contiguous()
318
+ x_bsa = self._run_cross_bsa(q_bsa, k_bsa, v_bsa, q_structure, k_structure, q_grid_size, k_grid_size)
319
+ x = rearrange(x_bsa, "b n s d -> b s (n d)")
320
+ else:
321
+ x = flash_attention(q.flatten(2), k.flatten(2), v.flatten(2), num_heads=local_heads)
322
+ x = rearrange(x, "b s (n d) -> b s n d", n=local_heads)
323
+ if x.shape[2] != hpr:
324
+ x = sp_pad_heads(x, hpr)
325
+ else:
326
+ # Padding-only rank: no BSA cross / flash kernel is run (padding never
327
+ # enters the kernel). Keep x CONNECTED to q/k/v (value forced to zero)
328
+ # so the q/k/v backward all_to_all collectives still fire here, keeping
329
+ # NCCL symmetric and routing this rank's real-token grads back correctly.
330
+ # Cross-attention: q and k/v have DIFFERENT seq lengths, so connect k/v
331
+ # via scalar reductions (broadcast-safe). Output takes q's seq shape.
332
+ x = q * 0.0 + (k.sum() + v.sum()) * 0.0
333
+ x = all_to_all_4D(x, nccl_info.sp_group, scatter_dim=1, gather_dim=2,
334
+ full_seq_len=q_full_len)
335
+ if padded_heads != self.num_heads:
336
+ x = x[:, :, :self.num_heads, :]
337
+ x = x.flatten(2)
338
+ else:
339
+ if use_bsa:
340
+ q_bsa = rearrange(q, "b s (n d) -> b n s d", n=self.num_heads).contiguous()
341
+ k_bsa = rearrange(k, "b s (n d) -> b n s d", n=self.num_heads).contiguous()
342
+ v_bsa = rearrange(v, "b s (n d) -> b n s d", n=self.num_heads).contiguous()
343
+ x_bsa = self._run_cross_bsa(q_bsa, k_bsa, v_bsa, q_structure, k_structure, q_grid_size, k_grid_size)
344
+ x = rearrange(x_bsa, "b n s d -> b s (n d)")
345
+ else:
346
+ x = self.attn(q, k, v)
347
+
348
+ return self.o(x)
349
+
350
+
351
+ # from diffusers.models.attention import AdaLayerNorm
352
+ class AdaLayerNorm(nn.Module):
353
+ r"""
354
+ Norm layer modified to incorporate timestep embeddings.
355
+
356
+ Parameters:
357
+ embedding_dim (`int`): The size of each embedding vector.
358
+ num_embeddings (`int`, *optional*): The size of the embeddings dictionary.
359
+ output_dim (`int`, *optional*):
360
+ norm_elementwise_affine (`bool`, defaults to `False):
361
+ norm_eps (`bool`, defaults to `False`):
362
+ chunk_dim (`int`, defaults to `0`):
363
+ """
364
+
365
+ def __init__(
366
+ self,
367
+ embedding_dim: int,
368
+ num_embeddings: Optional[int] = None,
369
+ output_dim: Optional[int] = None,
370
+ norm_elementwise_affine: bool = False,
371
+ norm_eps: float = 1e-5,
372
+ chunk_dim: int = 0,
373
+ ):
374
+ super().__init__()
375
+
376
+ self.chunk_dim = chunk_dim
377
+ output_dim = output_dim or embedding_dim * 2
378
+
379
+ if num_embeddings is not None:
380
+ self.emb = nn.Embedding(num_embeddings, embedding_dim)
381
+ else:
382
+ self.emb = None
383
+
384
+ self.silu = nn.SiLU()
385
+ self.linear = nn.Linear(embedding_dim, output_dim)
386
+ self.norm = nn.LayerNorm(output_dim // 2, norm_eps, norm_elementwise_affine)
387
+
388
+ def forward(
389
+ self, x: torch.Tensor, timestep: Optional[torch.Tensor] = None, temb: Optional[torch.Tensor] = None
390
+ ) -> torch.Tensor:
391
+ if self.emb is not None:
392
+ temb = self.emb(timestep)
393
+
394
+ temb = self.linear(self.silu(temb))
395
+
396
+ if self.chunk_dim == 2:
397
+ scale, shift = temb.chunk(2, dim=2)
398
+ # print(f"{x.shape = }, {scale.shape = }, {shift.shape = }")
399
+ elif self.chunk_dim == 1:
400
+ # This is a bit weird why we have the order of "shift, scale" here and "scale, shift" in the
401
+ # other if-branch. This branch is specific to CogVideoX and OmniGen for now.
402
+ shift, scale = temb.chunk(2, dim=1)
403
+ shift = shift[:, None, :]
404
+ scale = scale[:, None, :]
405
+ else:
406
+ scale, shift = temb.chunk(2, dim=0)
407
+
408
+ x = self.norm(x) * (1 + scale) + shift
409
+ return x
410
+
411
+
412
+ class ConditionalCrossAttentionBlock(nn.Module):
413
+ """
414
+ A thin wrapper around ConditionalCrossAttention.
415
+ Applies LayerNorm to the conditioning input `y` before cross-attention.
416
+ """
417
+
418
+ def __init__(self, dim: int, kv_dim: int, num_heads: int, eps: float = 1e-6, pooled_adaln: bool = False):
419
+ super().__init__()
420
+ self.y_norm = nn.LayerNorm(kv_dim, eps=eps)
421
+ self.inner = ConditionalCrossAttention(dim=dim, kv_dim=kv_dim, num_heads=num_heads, eps=eps)
422
+ self.pooled_adaln = pooled_adaln
423
+ if pooled_adaln:
424
+ self.per_frame_pooling = PerFrameAttentionPooling(kv_dim, num_heads=num_heads, eps=eps)
425
+ self.adaln = AdaLayerNorm(kv_dim, output_dim=dim * 2, chunk_dim=2)
426
+
427
+ def set_sp_streams(self, q_stream: str, kv_stream: str):
428
+ """Declare which SP-sharded stream feeds q and which feeds k/v."""
429
+ self.inner.sp_q_stream = q_stream
430
+ self.inner.sp_kv_stream = kv_stream
431
+
432
+ def forward(
433
+ self,
434
+ x: torch.Tensor,
435
+ y: torch.Tensor,
436
+ x_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
437
+ y_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
438
+ video_grid_size: Optional[Tuple[int, int, int]] = None,
439
+ q_structure: str = "1d",
440
+ k_structure: str = "1d",
441
+ q_grid_size=None,
442
+ k_grid_size=None,
443
+ ) -> torch.Tensor:
444
+ if self.pooled_adaln:
445
+ assert not get_sequence_parallel_state() or nccl_info.sp_size == 1, (
446
+ "pooled_adaln requires full-length y for per-frame pooling, "
447
+ "which is incompatible with SP > 1. Set sp_size=1 or disable pooled_adaln."
448
+ )
449
+ assert video_grid_size is not None, "video_grid_size must not be None"
450
+ pooled_y = self.per_frame_pooling(y, video_grid_size)
451
+ if pooled_y.shape[1] != x.shape[1]:
452
+ pooled_y = F.interpolate(
453
+ pooled_y.permute(0, 2, 1),
454
+ size=x.shape[1],
455
+ mode='linear',
456
+ align_corners=False,
457
+ ).permute(0, 2, 1)
458
+ x = self.adaln(x, temb=pooled_y)
459
+ y = self.y_norm(y)
460
+ return self.inner(x=x, y=y, x_freqs=x_freqs, y_freqs=y_freqs,
461
+ q_structure=q_structure, k_structure=k_structure,
462
+ q_grid_size=q_grid_size, k_grid_size=k_grid_size)
463
+
464
+
465
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
466
+ from diffusers.models.modeling_utils import ModelMixin
467
+
468
+
469
+ class DualTowerConditionalBridge(ModelMixin, ConfigMixin):
470
+ """
471
+ Dual-tower conditional bridge.
472
+ """
473
+ _repeated_blocks = ("ConditionalCrossAttentionBlock",)
474
+
475
+ @register_to_config
476
+ def __init__(self,
477
+ visual_layers: int = 30,
478
+ audio_layers: int = 30,
479
+ visual_hidden_dim: int = 3072, # visual DiT hidden state dimension
480
+ audio_hidden_dim: int = 1536, # audio DiT hidden state dimension
481
+ audio_fps: float = 44100.0 / 2048.0,
482
+ head_dim: int = 128, # attention head dimension
483
+ interaction_strategy: str = "shallow_focus",
484
+ apply_cross_rope: bool = False, # whether to apply RoPE in cross-attention
485
+ apply_first_frame_bias_in_rope: bool = False,
486
+ # whether to account for 1/video_fps bias for the first frame in RoPE alignment
487
+ trainable_condition_scale: bool = False,
488
+ pooled_adaln: bool = False,
489
+ ):
490
+ super().__init__()
491
+
492
+ self.visual_hidden_dim = visual_hidden_dim
493
+ self.audio_hidden_dim = audio_hidden_dim
494
+ self.audio_fps = audio_fps
495
+ self.head_dim = head_dim
496
+ self.apply_cross_rope = apply_cross_rope
497
+ self.apply_first_frame_bias_in_rope = apply_first_frame_bias_in_rope
498
+ self.trainable_condition_scale = trainable_condition_scale
499
+ self.pooled_adaln = pooled_adaln
500
+ if self.trainable_condition_scale:
501
+ self.condition_scale = nn.Parameter(torch.tensor([1.0], dtype=torch.float32))
502
+ else:
503
+ self.condition_scale = 1.0
504
+
505
+ self.controller = CrossModalInteractionController(visual_layers, audio_layers)
506
+ self.interaction_mapping = self.controller.get_interaction_layers(interaction_strategy)
507
+
508
+ # Conditional cross-attention modules operating at the DiT hidden-state level.
509
+ self.audio_to_video_conditioners = nn.ModuleDict() # audio hidden states -> visual DiT conditioning
510
+ self.video_to_audio_conditioners = nn.ModuleDict() # visual hidden states -> audio DiT conditioning
511
+
512
+ # Build conditioners for layers that should interact.
513
+ # audio hidden states condition the visual DiT
514
+ self.rotary = RotaryEmbedding(base=10000.0, dim=head_dim)
515
+ for v_layer, _ in self.interaction_mapping['a2v']:
516
+ self.audio_to_video_conditioners[str(v_layer)] = ConditionalCrossAttentionBlock(
517
+ dim=visual_hidden_dim, # 3072 (visual DiT hidden states)
518
+ kv_dim=audio_hidden_dim, # 1536 (audio DiT hidden states)
519
+ num_heads=visual_hidden_dim // head_dim, # derive number of heads from hidden dim
520
+ pooled_adaln=False # a2v typically does not need pooled AdaLN
521
+ )
522
+
523
+ # visual hidden states condition the audio DiT
524
+ for a_layer, _ in self.interaction_mapping['v2a']:
525
+ self.video_to_audio_conditioners[str(a_layer)] = ConditionalCrossAttentionBlock(
526
+ dim=audio_hidden_dim, # 1536 (audio DiT hidden states)
527
+ kv_dim=visual_hidden_dim, # 3072 (visual DiT hidden states)
528
+ num_heads=audio_hidden_dim // head_dim, # safe head count derivation
529
+ pooled_adaln=self.pooled_adaln
530
+ )
531
+
532
+ @torch.no_grad()
533
+ def build_aligned_freqs(self,
534
+ video_fps: float,
535
+ grid_size: Tuple[int, int, int],
536
+ audio_steps: int,
537
+ device: Optional[torch.device] = None,
538
+ dtype: Optional[torch.dtype] = None) -> Tuple[
539
+ Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor, torch.Tensor]]:
540
+ """
541
+ Build aligned RoPE (cos, sin) pairs based on video fps, video grid size (f_v, h, w),
542
+ and audio sequence length `audio_steps` (with fixed audio fps = 44100/2048).
543
+
544
+ Returns:
545
+ visual_freqs: (cos_v, sin_v), shape [1, f_v*h*w, head_dim]
546
+ audio_freqs: (cos_a, sin_a), shape [1, audio_steps, head_dim]
547
+ """
548
+ f_v, h, w = grid_size
549
+ L_v = f_v * h * w
550
+ L_a = int(audio_steps)
551
+
552
+ device = device or next(self.parameters()).device
553
+ dtype = dtype or torch.float32
554
+
555
+ # Audio positions: 0,1,2,...,L_a-1 (audio as reference).
556
+ audio_pos = torch.arange(L_a, device=device, dtype=torch.float32).unsqueeze(0)
557
+
558
+ # Video positions: align video frames to audio-step units.
559
+ # FIXME(dhyu): hard-coded VAE temporal stride = 4
560
+ if self.apply_first_frame_bias_in_rope:
561
+ # Account for the "first frame lasts 1/video_fps" bias.
562
+ video_effective_fps = float(video_fps) / 4.0
563
+ if f_v > 0:
564
+ t_starts = torch.zeros((f_v,), device=device, dtype=torch.float32)
565
+ if f_v > 1:
566
+ t_starts[1:] = (1.0 / float(video_fps)) + torch.arange(f_v - 1, device=device,
567
+ dtype=torch.float32) * (
568
+ 1.0 / video_effective_fps)
569
+ else:
570
+ t_starts = torch.zeros((0,), device=device, dtype=torch.float32)
571
+ # Convert to audio-step units.
572
+ video_pos_per_frame = t_starts * float(self.audio_fps)
573
+ else:
574
+ # No first-frame bias: uniform alignment.
575
+ scale = float(self.audio_fps) / float(video_fps / 4.0)
576
+ video_pos_per_frame = torch.arange(f_v, device=device, dtype=torch.float32) * scale
577
+ # Flatten to f*h*w; tokens within the same frame share the same time position.
578
+ video_pos = video_pos_per_frame.repeat_interleave(h * w).unsqueeze(0)
579
+
580
+ # print(f"video fps: {video_fps}, audio fps: {self.audio_fps}, scale: {scale}")
581
+ # print(f"video pos: {video_pos.shape}, audio pos: {audio_pos.shape}")
582
+
583
+ # Build dummy x to produce cos/sin, dim=head_dim.
584
+ dummy_v = torch.zeros((1, L_v, self.head_dim), device=device, dtype=dtype)
585
+ dummy_a = torch.zeros((1, L_a, self.head_dim), device=device, dtype=dtype)
586
+
587
+ cos_v, sin_v = self.rotary(dummy_v, position_ids=video_pos)
588
+ cos_a, sin_a = self.rotary(dummy_a, position_ids=audio_pos)
589
+
590
+ return (cos_v, sin_v), (cos_a, sin_a)
591
+
592
+ def should_interact(self, layer_idx: int, direction: str) -> bool:
593
+ return self.controller.should_interact(layer_idx, direction, self.interaction_mapping)
594
+
595
+ def apply_conditional_control(self,
596
+ layer_idx: int,
597
+ direction: str,
598
+ primary_hidden_states: torch.Tensor,
599
+ condition_hidden_states: torch.Tensor,
600
+ x_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
601
+ y_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
602
+ condition_scale: Optional[float] = None,
603
+ video_grid_size: Optional[Tuple[int, int, int]] = None) -> torch.Tensor:
604
+ """
605
+ Apply conditional control (at the DiT hidden-state level).
606
+
607
+ Args:
608
+ layer_idx: current layer index
609
+ direction: conditioning direction
610
+ - 'a2v': audio hidden states -> visual DiT
611
+ - 'v2a': visual hidden states -> audio DiT
612
+ primary_hidden_states: primary DiT hidden states [B, L, hidden_dim]
613
+ condition_hidden_states: condition DiT hidden states [B, L, hidden_dim]
614
+ condition_scale: conditioning strength (similar to CFG scale)
615
+
616
+ Returns:
617
+ Conditioned primary DiT hidden states [B, L, hidden_dim]
618
+ """
619
+
620
+ if not self.controller.should_interact(layer_idx, direction, self.interaction_mapping):
621
+ return primary_hidden_states
622
+
623
+ if direction == 'a2v':
624
+ # audio hidden states condition the visual DiT
625
+ conditioner = self.audio_to_video_conditioners[str(layer_idx)]
626
+
627
+ elif direction == 'v2a':
628
+ # visual hidden states condition the audio DiT
629
+ conditioner = self.video_to_audio_conditioners[str(layer_idx)]
630
+ else:
631
+ raise ValueError(f"Invalid direction: {direction}")
632
+
633
+ conditioned_features = conditioner(
634
+ x=primary_hidden_states,
635
+ y=condition_hidden_states,
636
+ x_freqs=x_freqs,
637
+ y_freqs=y_freqs,
638
+ video_grid_size=video_grid_size,
639
+ )
640
+
641
+ if self.trainable_condition_scale and condition_scale is not None:
642
+ print(
643
+ "[WARN] This model has a trainable condition_scale, but an external "
644
+ f"condition_scale={condition_scale} was provided. The trainable condition_scale "
645
+ "will be ignored in favor of the external value."
646
+ )
647
+
648
+ scale = condition_scale if condition_scale is not None else self.condition_scale
649
+
650
+ primary_hidden_states = primary_hidden_states + conditioned_features * scale
651
+
652
+ return primary_hidden_states
653
+
654
+ def forward(
655
+ self,
656
+ layer_idx: int,
657
+ visual_hidden_states: torch.Tensor,
658
+ audio_hidden_states: torch.Tensor,
659
+ *,
660
+ x_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
661
+ y_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
662
+ a2v_condition_scale: Optional[float] = None,
663
+ v2a_condition_scale: Optional[float] = None,
664
+ condition_scale: Optional[float] = None,
665
+ video_grid_size: Optional[Tuple[int, int, int]] = None,
666
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
667
+ """
668
+ Apply bidirectional conditional control to both visual/audio towers.
669
+
670
+ Args:
671
+ layer_idx: current layer index
672
+ visual_hidden_states: visual DiT hidden states
673
+ audio_hidden_states: audio DiT hidden states
674
+ x_freqs / y_freqs: cross-modal RoPE (cos, sin) pairs.
675
+ If provided, x_freqs is assumed to correspond to the primary tower and y_freqs
676
+ to the conditioning tower.
677
+ a2v_condition_scale: audio->visual conditioning strength (overrides global condition_scale)
678
+ v2a_condition_scale: visual->audio conditioning strength (overrides global condition_scale)
679
+ condition_scale: fallback conditioning strength when per-direction scale is None
680
+ video_grid_size: (F, H, W), used on the audio side when pooled_adaln is enabled
681
+
682
+ Returns:
683
+ (visual_hidden_states, audio_hidden_states), both conditioned in their respective directions.
684
+ """
685
+
686
+ visual_conditioned = self.apply_conditional_control(
687
+ layer_idx=layer_idx,
688
+ direction="a2v",
689
+ primary_hidden_states=visual_hidden_states,
690
+ condition_hidden_states=audio_hidden_states,
691
+ x_freqs=x_freqs,
692
+ y_freqs=y_freqs,
693
+ condition_scale=a2v_condition_scale if a2v_condition_scale is not None else condition_scale,
694
+ video_grid_size=video_grid_size,
695
+ )
696
+
697
+ audio_conditioned = self.apply_conditional_control(
698
+ layer_idx=layer_idx,
699
+ direction="v2a",
700
+ primary_hidden_states=audio_hidden_states,
701
+ condition_hidden_states=visual_hidden_states,
702
+ x_freqs=y_freqs,
703
+ y_freqs=x_freqs,
704
+ condition_scale=v2a_condition_scale if v2a_condition_scale is not None else condition_scale,
705
+ video_grid_size=video_grid_size,
706
+ )
707
+
708
+ return visual_conditioned, audio_conditioned
hymm/models/modules/mova.py ADDED
@@ -0,0 +1,964 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ import json
3
+ import os
4
+ from typing import Optional
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.utils.checkpoint as torch_checkpoint
9
+ from diffusers import ModelMixin, ConfigMixin
10
+
11
+ from hymm.models.modules.wan_video_dit import (
12
+ DiTBlock,
13
+ WanModel,
14
+ sinusoidal_embedding_1d,
15
+ advance_dynamic_block_pass_id,
16
+ )
17
+ from hymm.models.modules.wan_audio_dit import WanAudioModel
18
+ from hymm.models.modules.interactionv2 import (
19
+ ConditionalCrossAttentionBlock,
20
+ DualTowerConditionalBridge,
21
+ )
22
+ from hymm.utils.parallel_states import get_sequence_parallel_state, nccl_info, sp_seq_ctx
23
+ from hymm.utils.communications import all_gather, sp_split, assert_sp_splittable
24
+
25
+
26
+ def assemble_visual_freqs(freqs_tuple, t, h, w, device):
27
+ """Assemble 3D RoPE complex-valued freqs for the video DiT.
28
+
29
+ Matches the inline assembly in WanModel.forward() and
30
+ MOVA/mova/diffusion/pipelines/pipeline_mova.py.
31
+
32
+ Args:
33
+ freqs_tuple: (f_freqs_cis, h_freqs_cis, w_freqs_cis) from
34
+ precompute_freqs_cis_3d. Each is a complex tensor.
35
+ t, h, w: temporal, height, width grid dimensions (after patchify).
36
+ device: target device.
37
+
38
+ Returns:
39
+ Complex tensor of shape [t*h*w, 1, head_dim//2].
40
+ """
41
+ freqs = tuple(f.to(device) for f in freqs_tuple)
42
+ return torch.cat([
43
+ freqs[0][:t].view(t, 1, 1, -1).expand(t, h, w, -1),
44
+ freqs[1][:h].view(1, h, 1, -1).expand(t, h, w, -1),
45
+ freqs[2][:w].view(1, 1, w, -1).expand(t, h, w, -1),
46
+ ], dim=-1).reshape(t * h * w, 1, -1)
47
+
48
+
49
+ def assemble_audio_freqs(freqs_tuple, f, device):
50
+ """Assemble 1D RoPE complex-valued freqs for the audio DiT.
51
+
52
+ Matches the inline assembly in WanAudioModel.forward() and
53
+ MOVA/mova/diffusion/pipelines/pipeline_mova.py.
54
+
55
+ Args:
56
+ freqs_tuple: (f0, f1, f2) from precompute_freqs_cis.
57
+ f: sequence length (after audio patchify).
58
+ device: target device.
59
+
60
+ Returns:
61
+ Complex tensor of shape [f, 1, head_dim//2].
62
+ """
63
+ return torch.cat([
64
+ freqs_tuple[0][:f].view(f, -1).expand(f, -1),
65
+ freqs_tuple[1][:f].view(f, -1).expand(f, -1),
66
+ freqs_tuple[2][:f].view(f, -1).expand(f, -1),
67
+ ], dim=-1).reshape(f, 1, -1).to(device)
68
+
69
+
70
+
71
+ class FusedMOVABlock(nn.Module):
72
+
73
+ def __init__(self, video_block, audio_block,
74
+ a2v_conditioner=None, v2a_conditioner=None):
75
+ super().__init__()
76
+ self.video_block = video_block
77
+ self.audio_block = audio_block
78
+ self.a2v_conditioner = a2v_conditioner
79
+ self.v2a_conditioner = v2a_conditioner
80
+
81
+ # Mark inner DiTBlocks so apply_fsdp_checkpointing skips them,
82
+ # avoiding nested checkpointing (outer FusedMOVABlock already covers them).
83
+ self.video_block._inside_fused_block = True
84
+ self.audio_block._inside_fused_block = True
85
+
86
+ # Set to True by _apply_mova_fsdp_checkpointing (only when the YAML trigger fine_grained_gc is enabled) for blocks that ALSO get the outer block-level checkpoint
87
+ # wrapper. When on (and training), the a2v/v2a bridge submodules are
88
+ # additionally inner-checkpointed (deferred) so they are NOT co-resident
89
+ # with the video block at the video-FFN recompute peak. The video/audio
90
+ # DiT blocks run inline (Version B — see forward()). Pure recompute-
91
+ # schedule change: fwd/bwd numerics and loss are bit-identical to the
92
+ # default whole-block checkpointing.
93
+ self.fine_grained_gc = False
94
+
95
+ def forward(
96
+ self,
97
+ visual_x, audio_x,
98
+ visual_context, audio_context,
99
+ visual_t_mod, audio_t_mod,
100
+ visual_freqs, audio_freqs,
101
+ visual_rope_cos_sin=None, audio_rope_cos_sin=None,
102
+ a2v_scale=1.0, v2a_scale=1.0,
103
+ video_grid_size=None,
104
+ override_video_block=None,
105
+ timestep_ratio=None,
106
+ ):
107
+ # Fine-grained (nested) checkpointing helper. wraps ONLY the a2v/v2a bridges; the video/audio
108
+ # blocks run inline. All arguments are passed POSITIONALLY in
109
+ # the submodule's signature order, so the computation is byte-for-byte identical to the un-checkpointed calls (checkpointing only re-runs forward; there are no RNG ops here).
110
+ inner_gc = self.training and self.fine_grained_gc
111
+
112
+ def _maybe_ckpt(fn, *args):
113
+ if inner_gc:
114
+ return torch_checkpoint.checkpoint(fn, *args, use_reentrant=False)
115
+ return fn(*args)
116
+
117
+ # Bridge conditioning — must use ORIGINAL states for both directions,
118
+ # matching DualTowerConditionalBridge.forward which computes a2v and v2a
119
+ # from the same unmodified inputs before returning both conditioned outputs.
120
+ visual_x_orig = visual_x
121
+
122
+ a2v_bridge_residual = None
123
+ if self.a2v_conditioner is not None:
124
+ # Positional order matches ConditionalCrossAttentionBlock.forward:
125
+ # (x, y, x_freqs, y_freqs, video_grid_size, q_structure, k_structure,
126
+ # q_grid_size, k_grid_size)
127
+ cond = _maybe_ckpt(
128
+ self.a2v_conditioner,
129
+ visual_x, audio_x,
130
+ visual_rope_cos_sin, audio_rope_cos_sin,
131
+ video_grid_size,
132
+ "3d", "1d",
133
+ video_grid_size, None,
134
+ )
135
+ # Store the raw bridge residual (before scaling) for audio guidance
136
+ a2v_bridge_residual = cond
137
+ visual_x = visual_x + cond * a2v_scale
138
+
139
+ if self.v2a_conditioner is not None:
140
+ cond = _maybe_ckpt(
141
+ self.v2a_conditioner,
142
+ audio_x, visual_x_orig,
143
+ audio_rope_cos_sin, visual_rope_cos_sin,
144
+ video_grid_size,
145
+ "1d", "3d",
146
+ None, video_grid_size,
147
+ )
148
+ audio_x = audio_x + cond * v2a_scale
149
+
150
+ # DiT blocks (allow video block override for video_dit_2).
151
+ # If this OOMs -> wrapping BOTH calls below in _maybe_ckpt(...) (positional order as in the bridges above).
152
+ # Numerically bit-identical either way (checkpointing only re-runs fwd).
153
+ vblock = override_video_block if override_video_block is not None else self.video_block
154
+
155
+ visual_x = vblock(
156
+ visual_x, visual_context, visual_t_mod, visual_freqs,
157
+ grid_size=video_grid_size,
158
+ a2v_bridge_residual=a2v_bridge_residual,
159
+ timestep_ratio=timestep_ratio,
160
+ )
161
+ audio_x = self.audio_block(audio_x, audio_context, audio_t_mod, audio_freqs)
162
+
163
+ return visual_x, audio_x
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # MOVABridge — top-level wrapper
168
+ # ---------------------------------------------------------------------------
169
+
170
+ class MOVABridge(ModelMixin, ConfigMixin):
171
+
172
+ def __init__(
173
+ self,
174
+ video_dit: WanModel,
175
+ video_dit_2: Optional[WanModel],
176
+ audio_dit: WanAudioModel,
177
+ dual_tower_bridge: DualTowerConditionalBridge,
178
+ boundary_ratio: float = 0.9,
179
+ gradient_checkpoint: bool = False,
180
+ ):
181
+ super().__init__()
182
+ self.boundary_ratio = boundary_ratio
183
+ self.gradient_checkpoint = gradient_checkpoint
184
+
185
+ min_layers = min(len(video_dit.blocks), len(audio_dit.blocks))
186
+
187
+ # --- Build fused blocks (video + audio + bridge per layer) ---
188
+ self.fusion_blocks = nn.ModuleList()
189
+ for i in range(min_layers):
190
+ key = str(i)
191
+ a2v = dual_tower_bridge.audio_to_video_conditioners[key] if key in dual_tower_bridge.audio_to_video_conditioners else None
192
+ v2a = dual_tower_bridge.video_to_audio_conditioners[key] if key in dual_tower_bridge.video_to_audio_conditioners else None
193
+ fused = FusedMOVABlock(
194
+ video_block=video_dit.blocks[i],
195
+ audio_block=audio_dit.blocks[i],
196
+ a2v_conditioner=a2v,
197
+ v2a_conditioner=v2a,
198
+ )
199
+ self.fusion_blocks.append(fused)
200
+
201
+ # Remaining video-only blocks (primary dit, beyond min_layers)
202
+ self.remaining_video_blocks = nn.ModuleList(
203
+ [video_dit.blocks[i] for i in range(min_layers, len(video_dit.blocks))]
204
+ )
205
+
206
+ # --- Clear extracted blocks to avoid double registration ---
207
+ video_dit.blocks = nn.ModuleList()
208
+ audio_dit.blocks = nn.ModuleList()
209
+ dual_tower_bridge.audio_to_video_conditioners = nn.ModuleDict()
210
+ dual_tower_bridge.video_to_audio_conditioners = nn.ModuleDict()
211
+
212
+ # Keep DiTs for utility functions (patchify, unpatchify, head, embeddings, freqs)
213
+ self.video_dit = video_dit
214
+ self.audio_dit = audio_dit
215
+
216
+ # Keep bridge for rotary / build_aligned_freqs / condition_scale
217
+ self.dual_tower_bridge = dual_tower_bridge
218
+
219
+ # Secondary video dit (kept as-is with its own blocks, not fused).
220
+ # Mark override blocks (0..min_layers-1) so they are skipped by
221
+ # apply_fsdp_checkpointing — they run inside a checkpointed
222
+ # FusedMOVABlock and must not be double-wrapped.
223
+ if video_dit_2 is not None:
224
+ for i in range(min(min_layers, len(video_dit_2.blocks))):
225
+ video_dit_2.blocks[i]._inside_fused_block = True
226
+ self.video_dit_2 = video_dit_2
227
+
228
+ # When True, ALL sparse-attention features (top-k/top-p BSA, audio guidance,
229
+ # variance guidance, taylor/rectified bias correction, dynamic block shape)
230
+ # are applied ONLY to the high-noise expert (video_dit = fusion_blocks +
231
+ # remaining_video_blocks). The low-noise expert (video_dit_2) then keeps the
232
+ # backbone's original dense full attention. Default False = sparse on BOTH
233
+ # experts (legacy behavior). Plain python attr (not an FSDP param/buffer).
234
+ self.sparse_high_noise_only = False
235
+
236
+ self._tag_sp_streams()
237
+
238
+ gc.collect()
239
+
240
+ def _tag_sp_streams(self):
241
+ """Tell every attention which SP-sharded stream its tensors belong to.
242
+
243
+ Under sequence parallelism the visual and audio towers are chunked to
244
+ different lengths, and the Ulysses all-to-all has to undo the exact
245
+ ``torch.chunk`` split of the stream it is handling. Attentions default to
246
+ "visual", so only the audio tower and the two bridge directions need
247
+ retagging here.
248
+ """
249
+ from hymm.models.modules.wan_audio_dit import tag_audio_sp_stream
250
+
251
+ for fused in self.fusion_blocks:
252
+ tag_audio_sp_stream([fused.audio_block])
253
+ if fused.a2v_conditioner is not None:
254
+ # a2v: query = visual tokens, context = audio tokens
255
+ fused.a2v_conditioner.set_sp_streams("visual", "audio")
256
+ if fused.v2a_conditioner is not None:
257
+ # v2a: query = audio tokens, context = visual tokens
258
+ fused.v2a_conditioner.set_sp_streams("audio", "visual")
259
+
260
+ @classmethod
261
+ def from_mova_pretrained(cls, ckpt_path, torch_dtype=torch.bfloat16, logger=None):
262
+ """
263
+ Load from a MOVA HuggingFace-style checkpoint directory.
264
+
265
+ The directory should contain subfolders:
266
+ video_dit/, video_dit_2/, audio_dit/, dual_tower_bridge/,
267
+ video_vae/, audio_vae/, text_encoder/, tokenizer/, scheduler/
268
+
269
+ Returns:
270
+ (mova_bridge, extra_components) where extra_components is a dict
271
+ containing {video_vae, audio_vae, text_encoder, tokenizer, scheduler,
272
+ boundary_ratio, audio_vae_type}.
273
+ """
274
+ from diffusers.models.autoencoders import AutoencoderKLWan
275
+ from transformers import T5TokenizerFast, UMT5EncoderModel
276
+
277
+ from hymm.models.modules.dac_vae import DAC
278
+ from hymm.diffusion.schedulers.flow_match_pair import FlowMatchPairScheduler
279
+
280
+ _log = logger.info if logger else print
281
+
282
+ import time as _time
283
+ _load_start = _time.time()
284
+ _log(f"[MOVABridge] Loading from {ckpt_path}")
285
+
286
+ # Trainable components: load in requested dtype on CPU
287
+ _t0 = _time.time()
288
+ video_dit = WanModel.from_pretrained(
289
+ ckpt_path, subfolder="video_dit", torch_dtype=torch_dtype,
290
+ ).cpu()
291
+ _log(f"[MOVABridge] Loaded video_dit ({_time.time()-_t0:.1f}s)")
292
+
293
+ video_dit_2_path = os.path.join(ckpt_path, "video_dit_2")
294
+ if os.path.isdir(video_dit_2_path):
295
+ _t0 = _time.time()
296
+ video_dit_2 = WanModel.from_pretrained(
297
+ ckpt_path, subfolder="video_dit_2", torch_dtype=torch_dtype,
298
+ ).cpu()
299
+ _log(f"[MOVABridge] Loaded video_dit_2 ({_time.time()-_t0:.1f}s)")
300
+ else:
301
+ video_dit_2 = None
302
+ _log("[MOVABridge] No video_dit_2 found, skipping")
303
+
304
+ _t0 = _time.time()
305
+ audio_dit = WanAudioModel.from_pretrained(
306
+ ckpt_path, subfolder="audio_dit", torch_dtype=torch_dtype,
307
+ ).cpu()
308
+ _log(f"[MOVABridge] Loaded audio_dit ({_time.time()-_t0:.1f}s)")
309
+
310
+ _t0 = _time.time()
311
+ dual_tower_bridge = DualTowerConditionalBridge.from_pretrained(
312
+ ckpt_path, subfolder="dual_tower_bridge", torch_dtype=torch_dtype,
313
+ ).cpu()
314
+ _log(f"[MOVABridge] Loaded dual_tower_bridge ({_time.time()-_t0:.1f}s)")
315
+
316
+ # Frozen components: load in bf16 on CPU to save memory
317
+ frozen_dtype = torch.bfloat16
318
+
319
+ _t0 = _time.time()
320
+ video_vae = AutoencoderKLWan.from_pretrained(
321
+ ckpt_path, subfolder="video_vae", torch_dtype=frozen_dtype,
322
+ ).cpu()
323
+ _log(f"[MOVABridge] Loaded video_vae (bf16, frozen, {_time.time()-_t0:.1f}s)")
324
+
325
+ _t0 = _time.time()
326
+ audio_vae = DAC.from_pretrained(
327
+ ckpt_path, subfolder="audio_vae", torch_dtype=frozen_dtype,
328
+ ).cpu()
329
+ _log(f"[MOVABridge] Loaded audio_vae (bf16, frozen, {_time.time()-_t0:.1f}s)")
330
+
331
+ _t0 = _time.time()
332
+ text_encoder = UMT5EncoderModel.from_pretrained(
333
+ ckpt_path, subfolder="text_encoder", torch_dtype=frozen_dtype,
334
+ ).to("cpu")
335
+ _log(f"[MOVABridge] Loaded text_encoder (bf16, frozen, {_time.time()-_t0:.1f}s)")
336
+ _log(f"[MOVABridge] Total model loading time: {_time.time()-_load_start:.1f}s")
337
+
338
+ tokenizer = T5TokenizerFast.from_pretrained(
339
+ ckpt_path, subfolder="tokenizer",
340
+ )
341
+ _log("[MOVABridge] Loaded tokenizer")
342
+
343
+ scheduler_path = os.path.join(ckpt_path, "scheduler")
344
+ if os.path.isdir(scheduler_path):
345
+ scheduler = FlowMatchPairScheduler.from_pretrained(
346
+ ckpt_path, subfolder="scheduler",
347
+ )
348
+ _log("[MOVABridge] Loaded scheduler from pretrained")
349
+ else:
350
+ scheduler = FlowMatchPairScheduler(shift=5.0)
351
+ _log("[MOVABridge] Using default FlowMatchPairScheduler(shift=5.0)")
352
+
353
+ model_index_path = os.path.join(ckpt_path, "model_index.json")
354
+ boundary_ratio = 0.9
355
+ audio_vae_type = "dac"
356
+ if os.path.exists(model_index_path):
357
+ with open(model_index_path, "r") as f:
358
+ model_index = json.load(f)
359
+ boundary_ratio = model_index.get("boundary_ratio", 0.9)
360
+ audio_vae_type = model_index.get("audio_vae_type", "dac")
361
+
362
+ bridge = cls(
363
+ video_dit=video_dit,
364
+ video_dit_2=video_dit_2,
365
+ audio_dit=audio_dit,
366
+ dual_tower_bridge=dual_tower_bridge,
367
+ boundary_ratio=boundary_ratio,
368
+ )
369
+
370
+ extra_components = {
371
+ "video_vae": video_vae,
372
+ "audio_vae": audio_vae,
373
+ "text_encoder": text_encoder,
374
+ "tokenizer": tokenizer,
375
+ "scheduler": scheduler,
376
+ "boundary_ratio": boundary_ratio,
377
+ "audio_vae_type": audio_vae_type,
378
+ }
379
+
380
+ return bridge, extra_components
381
+
382
+ # -----------------------------------------------------------------------
383
+ # FSDP helpers
384
+ # -----------------------------------------------------------------------
385
+
386
+ @staticmethod
387
+ def get_fsdp_no_split_modules():
388
+ """Return module types that should not be split by FSDP."""
389
+ return (FusedMOVABlock, DiTBlock)
390
+
391
+ def get_fsdp_block_list(self):
392
+ """Return a flat list of all blocks for FSDP wrapping."""
393
+ blocks = list(self.fusion_blocks) + list(self.remaining_video_blocks)
394
+ if self.video_dit_2 is not None:
395
+ blocks.extend(self.video_dit_2.blocks)
396
+ return blocks
397
+
398
+ def get_fsdp_nested_block_list(self):
399
+ """Sub-blocks that need an FSDP unit of their own inside a parent block.
400
+
401
+ Each FusedMOVABlock owns the high-noise expert's video block, but on a
402
+ low-noise step that block is bypassed in favour of ``video_dit_2``. If it
403
+ stays in the fused block's parameter group it is still all-gathered on
404
+ every one of those steps -- twice, since activation checkpointing
405
+ re-runs the forward during backward -- and then discarded unused. Its own
406
+ group is simply never entered, the same way ``video_dit_2.blocks`` are
407
+ never entered on a high-noise step.
408
+
409
+ Only worth doing when a second expert exists; otherwise the video block
410
+ runs every step and splitting it just buys an extra collective.
411
+ """
412
+ if self.video_dit_2 is None:
413
+ return []
414
+ return [fused.video_block for fused in self.fusion_blocks]
415
+
416
+ def get_fsdp_forward_tail_blocks(self):
417
+ """Blocks that can be the LAST one executed in a forward pass.
418
+
419
+ FSDP re-gathers the final block almost immediately for backward, so
420
+ resharding it after forward is a wasted round trip. Which block ends the
421
+ forward depends on the expert this step routed to, so both candidates are
422
+ returned rather than just the last entry of ``get_fsdp_block_list()`` --
423
+ that entry belongs to ``video_dit_2`` and is idle on high-noise steps.
424
+ """
425
+ tails = []
426
+ high_noise_tail = self.remaining_video_blocks or self.fusion_blocks
427
+ if len(high_noise_tail) > 0:
428
+ tails.append(high_noise_tail[-1])
429
+ if self.video_dit_2 is not None and len(self.video_dit_2.blocks) > 0:
430
+ tails.append(self.video_dit_2.blocks[-1])
431
+ return tails
432
+
433
+ # -----------------------------------------------------------------------
434
+ # BSA configuration
435
+ # -----------------------------------------------------------------------
436
+
437
+ def set_sparse_high_noise_only(self, flag: bool):
438
+ """Restrict ALL video self-attn sparse features to the high-noise expert.
439
+
440
+ When ``flag`` is True, every subsequent ``configure_*`` call (BSA top-k/top-p,
441
+ audio guidance, variance guidance, taylor/rectified bias correction, IVPQ /
442
+ penalty dynamic block, layer-adaptive) skips the low-noise expert
443
+ (``video_dit_2``), leaving it on the backbone's dense full attention. The
444
+ high-noise expert (``fusion_blocks`` + ``remaining_video_blocks``) is
445
+ unaffected. Must be called BEFORE the ``configure_*`` methods. Default False
446
+ = sparse applied to BOTH experts (legacy behavior).
447
+ """
448
+ self.sparse_high_noise_only = bool(flag)
449
+
450
+ def configure_bsa(self, enable_bsa: bool, bsa_params: dict = None,
451
+ enable_bsa_v2a: bool = False, bsa_params_v2a: dict = None):
452
+ """Runtime configuration of Block Sparse Attention.
453
+
454
+ Args:
455
+ enable_bsa: enable BSA on video self-attention
456
+ bsa_params: params for video self-attn BSA
457
+ enable_bsa_v2a: enable BSA on v2a bridge cross-attention
458
+ (Q=audio 1D blocked internally, K=video 3D blocked)
459
+ a2v does NOT use BSA — audio K is too short.
460
+ bsa_params_v2a: params for v2a BSA
461
+ """
462
+ self._bsa_enabled = enable_bsa
463
+ self._bsa_params = bsa_params or {}
464
+ for fused_block in self.fusion_blocks:
465
+ fused_block.video_block.self_attn.enable_bsa = enable_bsa
466
+ fused_block.video_block.self_attn.bsa_params = bsa_params or {}
467
+ if fused_block.v2a_conditioner is not None:
468
+ fused_block.v2a_conditioner.inner.enable_bsa = enable_bsa_v2a
469
+ fused_block.v2a_conditioner.inner.bsa_params = bsa_params_v2a or {}
470
+ for block in self.remaining_video_blocks:
471
+ block.self_attn.enable_bsa = enable_bsa
472
+ block.self_attn.bsa_params = bsa_params or {}
473
+ if self.video_dit_2 is not None and not self.sparse_high_noise_only:
474
+ for block in self.video_dit_2.blocks:
475
+ block.self_attn.enable_bsa = enable_bsa
476
+ block.self_attn.bsa_params = bsa_params or {}
477
+
478
+ # -----------------------------------------------------------------------
479
+ # Audio Guidance configuration
480
+ # -----------------------------------------------------------------------
481
+
482
+ def configure_audio_guidance(self, enable: bool, enable_concentration_gate: bool = False,
483
+ enable_timestep_reliability_gate: bool = False,
484
+ enable_audio_weighted_pooling: bool = False):
485
+ """Runtime configuration of audio-guided BSA on video self-attention.
486
+
487
+ Two independent paths:
488
+ - Path A (enable=True): Multiplicative score fusion with optional gates.
489
+ - Path B (enable_audio_weighted_pooling=True): Audio-weighted K pooling.
490
+
491
+ Either or both can be enabled independently.
492
+
493
+ Args:
494
+ enable: whether to enable Path A (score modulation).
495
+ enable_concentration_gate: Gate 2 for Path A.
496
+ enable_timestep_reliability_gate: Gate 1 for Path A.
497
+ enable_audio_weighted_pooling: whether to enable Path B (weighted K pooling).
498
+ """
499
+ for fused_block in self.fusion_blocks:
500
+ sa = fused_block.video_block.self_attn
501
+ sa.enable_audio_guidance = enable
502
+ sa.enable_audio_concentration_gate = enable_concentration_gate
503
+ sa.enable_timestep_reliability_gate = enable_timestep_reliability_gate
504
+ sa.enable_audio_weighted_pooling = enable_audio_weighted_pooling
505
+ for block in self.remaining_video_blocks:
506
+ sa = block.self_attn
507
+ sa.enable_audio_guidance = enable
508
+ sa.enable_audio_concentration_gate = enable_concentration_gate
509
+ sa.enable_timestep_reliability_gate = enable_timestep_reliability_gate
510
+ sa.enable_audio_weighted_pooling = enable_audio_weighted_pooling
511
+ if self.video_dit_2 is not None and not self.sparse_high_noise_only:
512
+ for block in self.video_dit_2.blocks:
513
+ sa = block.self_attn
514
+ sa.enable_audio_guidance = enable
515
+ sa.enable_audio_concentration_gate = enable_concentration_gate
516
+ sa.enable_timestep_reliability_gate = enable_timestep_reliability_gate
517
+ sa.enable_audio_weighted_pooling = enable_audio_weighted_pooling
518
+
519
+ # -----------------------------------------------------------------------
520
+ # Variance Guidance configuration (independent from audio guidance)
521
+ # -----------------------------------------------------------------------
522
+
523
+ def configure_variance_guidance(self, enable: bool):
524
+ """Runtime configuration of channel-variance guided BSA on video self-attention.
525
+
526
+ Non-learnable, no gate. Computes ρ_j on V blocks (key/value side only):
527
+ score_{i,j} *= (1 + γ * ρ_j)
528
+ Query-awareness via P^QK zero-gate: same ρ_j, different QK scores per query
529
+ → different top-k per query block. γ read from bsa_params['variance_boost_gamma'].
530
+ Completely independent from audio guidance — no shared code.
531
+
532
+ Args:
533
+ enable: whether to enable channel-variance guidance.
534
+ """
535
+ for fused_block in self.fusion_blocks:
536
+ fused_block.video_block.self_attn.enable_variance_guidance = enable
537
+ for block in self.remaining_video_blocks:
538
+ block.self_attn.enable_variance_guidance = enable
539
+ if self.video_dit_2 is not None and not self.sparse_high_noise_only:
540
+ for block in self.video_dit_2.blocks:
541
+ block.self_attn.enable_variance_guidance = enable
542
+
543
+ def configure_taylor_sparse_attn(self, enable: bool):
544
+ """LIVEditor: selected + non-selected (Taylor) in one unified softmax."""
545
+ for fused_block in self.fusion_blocks:
546
+ fused_block.video_block.self_attn.enable_taylor_sparse_attn = enable
547
+ if enable:
548
+ fused_block.video_block.self_attn.enable_rectified_sparse_attn = False
549
+ for block in self.remaining_video_blocks:
550
+ block.self_attn.enable_taylor_sparse_attn = enable
551
+ if enable:
552
+ block.self_attn.enable_rectified_sparse_attn = False
553
+ if self.video_dit_2 is not None and not self.sparse_high_noise_only:
554
+ for block in self.video_dit_2.blocks:
555
+ block.self_attn.enable_taylor_sparse_attn = enable
556
+ if enable:
557
+ block.self_attn.enable_rectified_sparse_attn = False
558
+
559
+ def configure_rectified_sparse_attn(self, enable: bool):
560
+ """Rectified SpaAttn: o = R_n * o_spa + A_pool[nonsel] · V_pool."""
561
+ for fused_block in self.fusion_blocks:
562
+ fused_block.video_block.self_attn.enable_rectified_sparse_attn = enable
563
+ if enable:
564
+ fused_block.video_block.self_attn.enable_taylor_sparse_attn = False
565
+ for block in self.remaining_video_blocks:
566
+ block.self_attn.enable_rectified_sparse_attn = enable
567
+ if enable:
568
+ block.self_attn.enable_taylor_sparse_attn = False
569
+ if self.video_dit_2 is not None and not self.sparse_high_noise_only:
570
+ for block in self.video_dit_2.blocks:
571
+ block.self_attn.enable_rectified_sparse_attn = enable
572
+ if enable:
573
+ block.self_attn.enable_taylor_sparse_attn = False
574
+
575
+ def _video_self_attns(self):
576
+ """Yield EVERY video self-attn (both experts). NOT gated by
577
+ sparse_high_noise_only — this enumerates all attention modules and is used
578
+ for setting numeric bsa_params (which are inert unless a feature is enabled).
579
+ Never used in the forward pass.
580
+ """
581
+ for fused_block in self.fusion_blocks:
582
+ yield fused_block.video_block.self_attn
583
+ for block in self.remaining_video_blocks:
584
+ yield block.self_attn
585
+ if self.video_dit_2 is not None:
586
+ for block in self.video_dit_2.blocks:
587
+ yield block.self_attn
588
+
589
+ def _video_self_attns_with_idx(self):
590
+ """Yield (global_video_layer_idx, self_attn) for every video self-attn.
591
+
592
+ Layer indexing matches the forward loop: fusion_blocks[i] -> i;
593
+ remaining_video_blocks[j] -> n_fused + j; video_dit_2.blocks[i] -> i
594
+ (the boundary model mirrors the same per-layer indices). NOT gated.
595
+ """
596
+ n_fused = len(self.fusion_blocks)
597
+ for i, fused_block in enumerate(self.fusion_blocks):
598
+ yield i, fused_block.video_block.self_attn
599
+ for j, block in enumerate(self.remaining_video_blocks):
600
+ yield n_fused + j, block.self_attn
601
+ if self.video_dit_2 is not None:
602
+ for i, block in enumerate(self.video_dit_2.blocks):
603
+ yield i, block.self_attn
604
+
605
+ def _sparse_cfg_video_self_attns(self):
606
+
607
+ for fused_block in self.fusion_blocks:
608
+ yield fused_block.video_block.self_attn
609
+ for block in self.remaining_video_blocks:
610
+ yield block.self_attn
611
+ if self.video_dit_2 is not None and not self.sparse_high_noise_only:
612
+ for block in self.video_dit_2.blocks:
613
+ yield block.self_attn
614
+
615
+ def _sparse_cfg_video_self_attns_with_idx(self):
616
+ """Indexed variant of ``_sparse_cfg_video_self_attns`` (low-noise expert
617
+ omitted when ``sparse_high_noise_only`` is set)."""
618
+ n_fused = len(self.fusion_blocks)
619
+ for i, fused_block in enumerate(self.fusion_blocks):
620
+ yield i, fused_block.video_block.self_attn
621
+ for j, block in enumerate(self.remaining_video_blocks):
622
+ yield n_fused + j, block.self_attn
623
+ if self.video_dit_2 is not None and not self.sparse_high_noise_only:
624
+ for i, block in enumerate(self.video_dit_2.blocks):
625
+ yield i, block.self_attn
626
+
627
+ @property
628
+ def _num_video_layers(self):
629
+ return len(self.fusion_blocks) + len(self.remaining_video_blocks)
630
+
631
+ @staticmethod
632
+ def _disable_non_dynamic_bsa_features(sa):
633
+ sa.enable_audio_guidance = False
634
+ sa.enable_audio_concentration_gate = False
635
+ sa.enable_timestep_reliability_gate = False
636
+ sa.enable_audio_weighted_pooling = False
637
+ sa.enable_variance_guidance = False
638
+ sa.enable_taylor_sparse_attn = False
639
+ sa.enable_rectified_sparse_attn = False
640
+
641
+ def configure_ivpq_dynamic_block(self, enable: bool):
642
+ """IVPQ anisotropic dynamic block shape (Section 8.5)."""
643
+ for sa in self._sparse_cfg_video_self_attns():
644
+ sa.enable_ivpq_dynamic_block = enable
645
+ if enable:
646
+ sa.enable_penalty_dynamic_block = False
647
+ self._disable_non_dynamic_bsa_features(sa)
648
+
649
+ def configure_penalty_dynamic_block(self, enable: bool):
650
+ """Penalty-Matching anisotropic dynamic block shape (Section 8.6)."""
651
+ for sa in self._sparse_cfg_video_self_attns():
652
+ sa.enable_penalty_dynamic_block = enable
653
+ if enable:
654
+ sa.enable_ivpq_dynamic_block = False
655
+ self._disable_non_dynamic_bsa_features(sa)
656
+
657
+ def configure_layer_adaptive_dynamic_block(self, enable: bool):
658
+ """Layer-Adaptive Dynamic Block Shape wrapper (Section 8.2).
659
+
660
+ Must be configured AFTER ``configure_ivpq/penalty_dynamic_block`` (it only
661
+ takes effect when one of those is active). When ``enable``:
662
+
663
+ * shallow half of the video self-attn layers (idx < split) run FIXED
664
+ uniform-shape BSA;
665
+ * deep half (idx >= split, split = num_video_layers // 2) run the
666
+ dynamic-block method;
667
+ * a deep layer that HAS its own a2v bridge uses its OWN audio; only the
668
+ deep layers WITHOUT a bridge (the tail beyond audio_layers) reuse the
669
+ cache. The cache is seeded by the DEEPEST bridge layer — its
670
+ full-sequence ``||Δh_v||_2`` is cached (per forward) and reused by those
671
+ no-bridge deep layers so the audio-directional gradient still guides
672
+ their 64/128 + triple split, combined with each deep layer's own video
673
+ channel-variance.
674
+
675
+ Returns (split, cache_source_idx) for logging. Idempotent / safe to call
676
+ with ``enable=False`` to turn the wrapper off (back to all-layer dynamic).
677
+ """
678
+ n_fused = len(self.fusion_blocks)
679
+ num_video_layers = self._num_video_layers
680
+ split = num_video_layers // 2
681
+
682
+
683
+ bridge_layers = [
684
+ i for i, fb in enumerate(self.fusion_blocks)
685
+ if getattr(fb, "a2v_conditioner", None) is not None
686
+ ]
687
+ cache_source_idx = max(bridge_layers) if bridge_layers else None
688
+
689
+ # One mutable holder shared by reference across ALL video self-attns; reset
690
+ # at the start of every forward. Plain python attr → not an FSDP param/buffer.
691
+ if getattr(self, "_la_audio_cache", None) is None:
692
+ self._la_audio_cache = {"norms": None}
693
+ cache = self._la_audio_cache
694
+
695
+ for idx, sa in self._sparse_cfg_video_self_attns_with_idx():
696
+ sa.enable_layer_adaptive_dynamic_block = enable
697
+ sa._la_layer_idx = idx
698
+ sa._la_split = split
699
+ sa._la_is_cache_source = bool(
700
+ enable and cache_source_idx is not None and idx == cache_source_idx
701
+ )
702
+ sa._la_audio_cache = cache if enable else None
703
+
704
+ return split, cache_source_idx
705
+
706
+ # -----------------------------------------------------------------------
707
+ # Forward
708
+ # -----------------------------------------------------------------------
709
+
710
+ def forward(
711
+ self,
712
+ visual_latents: torch.Tensor,
713
+ audio_latents: torch.Tensor,
714
+ context: torch.Tensor,
715
+ timestep: torch.Tensor,
716
+ audio_context: Optional[torch.Tensor] = None,
717
+ audio_timestep: Optional[torch.Tensor] = None,
718
+ video_fps: float = 24.0,
719
+ num_train_timesteps: int = 1000,
720
+ condition_scale: float = 1.0,
721
+ a2v_condition_scale: Optional[float] = None,
722
+ v2a_condition_scale: Optional[float] = None,
723
+ use_video_dit_2: bool = False,
724
+ ):
725
+ """
726
+ Single denoising step. Reproduces MOVA's inference_single_step exactly.
727
+ """
728
+ advance_dynamic_block_pass_id() # once per real forward (not on recompute)
729
+
730
+ use_dit_2 = use_video_dit_2 and self.video_dit_2 is not None
731
+ visual_dit = self.video_dit_2 if use_dit_2 else self.video_dit
732
+
733
+ if audio_context is None:
734
+ audio_context = context
735
+ if audio_timestep is None:
736
+ audio_timestep = timestep
737
+
738
+ # Timestep embeddings (force fp32 for numerical stability, same as MOVA)
739
+ with torch.autocast("cuda", dtype=torch.float32):
740
+ visual_t = visual_dit.time_embedding(
741
+ sinusoidal_embedding_1d(visual_dit.freq_dim, timestep)
742
+ )
743
+ visual_t_mod = visual_dit.time_projection(visual_t).unflatten(1, (6, visual_dit.dim))
744
+
745
+ audio_t = self.audio_dit.time_embedding(
746
+ sinusoidal_embedding_1d(self.audio_dit.freq_dim, audio_timestep)
747
+ )
748
+ audio_t_mod = self.audio_dit.time_projection(audio_t).unflatten(1, (6, self.audio_dit.dim))
749
+
750
+ model_dtype = visual_dit.dtype
751
+ visual_t = visual_t.to(model_dtype)
752
+ visual_t_mod = visual_t_mod.to(model_dtype)
753
+ audio_t = audio_t.to(model_dtype)
754
+ audio_t_mod = audio_t_mod.to(model_dtype)
755
+
756
+ visual_context_emb = visual_dit.text_embedding(context)
757
+ audio_context_emb = self.audio_dit.text_embedding(audio_context)
758
+
759
+ visual_x = visual_latents.to(model_dtype)
760
+ audio_x = audio_latents.to(model_dtype)
761
+
762
+ # Patchify
763
+ visual_x, (t, h, w) = visual_dit.patchify(visual_x)
764
+ grid_size = (t, h, w)
765
+ visual_freqs = assemble_visual_freqs(visual_dit.freqs, t, h, w, visual_x.device)
766
+
767
+ audio_x, (f,) = self.audio_dit.patchify(audio_x, None)
768
+ audio_freqs = assemble_audio_freqs(self.audio_dit.freqs, f, audio_x.device)
769
+
770
+ # Compute normalized timestep ratio for Timestep Reliability Gate
771
+ # In flow matching: timestep ∈ [0, 1] where 0=clean, 1=noise
772
+ # If timestep > 1 (e.g., in [0, 1000] convention), normalize
773
+ t_val = timestep.float().mean()
774
+ timestep_ratio = t_val if t_val <= 1.0 else t_val / float(num_train_timesteps)
775
+
776
+ # Forward through fused dual-tower blocks
777
+ visual_x, audio_x = self._forward_dual_tower(
778
+ visual_x=visual_x,
779
+ audio_x=audio_x,
780
+ visual_context=visual_context_emb,
781
+ audio_context=audio_context_emb,
782
+ visual_t_mod=visual_t_mod,
783
+ audio_t_mod=audio_t_mod,
784
+ visual_freqs=visual_freqs,
785
+ audio_freqs=audio_freqs,
786
+ grid_size=grid_size,
787
+ video_fps=video_fps,
788
+ condition_scale=condition_scale,
789
+ a2v_condition_scale=a2v_condition_scale,
790
+ v2a_condition_scale=v2a_condition_scale,
791
+ use_dit_2=use_dit_2,
792
+ timestep_ratio=timestep_ratio,
793
+ )
794
+
795
+ # Head projections (operate on SP-split sequence for efficiency)
796
+ visual_output = visual_dit.head(visual_x, visual_t)
797
+ audio_output = self.audio_dit.head(audio_x, audio_t)
798
+
799
+ # SP: gather back full sequences after head
800
+ if get_sequence_parallel_state():
801
+ visual_output = all_gather(visual_output, dim=1, group=nccl_info.sp_group,
802
+ full_seq_len=t * h * w)
803
+ audio_output = all_gather(audio_output, dim=1, group=nccl_info.sp_group,
804
+ full_seq_len=f)
805
+
806
+ visual_output = visual_dit.unpatchify(visual_output, grid_size)
807
+ audio_output = self.audio_dit.unpatchify(audio_output, (f,))
808
+
809
+ return visual_output, audio_output
810
+
811
+ def _forward_dual_tower(
812
+ self,
813
+ visual_x,
814
+ audio_x,
815
+ visual_context,
816
+ audio_context,
817
+ visual_t_mod,
818
+ audio_t_mod,
819
+ visual_freqs,
820
+ audio_freqs,
821
+ grid_size,
822
+ video_fps,
823
+ condition_scale=1.0,
824
+ a2v_condition_scale=None,
825
+ v2a_condition_scale=None,
826
+ use_dit_2=False,
827
+ timestep_ratio=None,
828
+ ):
829
+ """
830
+ Forward through fused dual-tower blocks + remaining visual-only blocks.
831
+
832
+ Ulysses SP flow:
833
+ - Before blocks: chunk sequences and freqs
834
+ - Inside each attention: all_to_all_4D scatter heads / gather seq
835
+ - After all blocks: all_gather to reconstruct full sequences
836
+ """
837
+ sp_enabled = get_sequence_parallel_state()
838
+
839
+ # Build bridge cross-modal RoPE
840
+ if self.dual_tower_bridge.apply_cross_rope:
841
+ (visual_rope_cos_sin, audio_rope_cos_sin) = (
842
+ self.dual_tower_bridge.build_aligned_freqs(
843
+ video_fps=video_fps,
844
+ grid_size=grid_size,
845
+ audio_steps=audio_x.shape[1],
846
+ device=visual_x.device,
847
+ dtype=visual_x.dtype,
848
+ )
849
+ )
850
+ else:
851
+ visual_rope_cos_sin = None
852
+ audio_rope_cos_sin = None
853
+
854
+ # Resolve condition scales
855
+ bridge_scale = self.dual_tower_bridge.condition_scale
856
+ a2v_scale = (a2v_condition_scale if a2v_condition_scale is not None
857
+ else (condition_scale if condition_scale is not None
858
+ else bridge_scale))
859
+ v2a_scale = (v2a_condition_scale if v2a_condition_scale is not None
860
+ else (condition_scale if condition_scale is not None
861
+ else bridge_scale))
862
+
863
+ # --- SP: chunk sequences and freqs ---
864
+ if sp_enabled:
865
+ if getattr(nccl_info, 'use_dynamic_ring_attention', False):
866
+ raise NotImplementedError(
867
+ "MOVABridge does not support dynamic_ring_attention. "
868
+ "Please set use_dynamic_ring_attention=False."
869
+ )
870
+ sp_size = nccl_info.sp_size
871
+ sp_rank = nccl_info.rank_within_spgroup
872
+
873
+ # Publish the pre-shard lengths of both streams. Every Ulysses
874
+ # all-to-all below reads them from here instead of renegotiating the
875
+ # split with a per-call all_gather_object.
876
+ visual_seq_len = visual_x.shape[1]
877
+ audio_seq_len = audio_x.shape[1]
878
+ assert_sp_splittable(visual_seq_len, sp_size, "visual sequence")
879
+ assert_sp_splittable(audio_seq_len, sp_size, "audio sequence")
880
+ sp_seq_ctx.set(visual=visual_seq_len, audio=audio_seq_len)
881
+
882
+ visual_x = sp_split(visual_x, sp_size, sp_rank, dim=1)
883
+ audio_x = sp_split(audio_x, sp_size, sp_rank, dim=1)
884
+ visual_freqs = sp_split(visual_freqs, sp_size, sp_rank, dim=0)
885
+ audio_freqs = sp_split(audio_freqs, sp_size, sp_rank, dim=0)
886
+
887
+ if visual_rope_cos_sin is not None:
888
+ v_cos, v_sin = visual_rope_cos_sin
889
+ v_cos = sp_split(v_cos, sp_size, sp_rank, dim=1)
890
+ v_sin = sp_split(v_sin, sp_size, sp_rank, dim=1)
891
+ visual_rope_cos_sin = (v_cos, v_sin)
892
+ if audio_rope_cos_sin is not None:
893
+ a_cos, a_sin = audio_rope_cos_sin
894
+ a_cos = sp_split(a_cos, sp_size, sp_rank, dim=1)
895
+ a_sin = sp_split(a_sin, sp_size, sp_rank, dim=1)
896
+ audio_rope_cos_sin = (a_cos, a_sin)
897
+
898
+ use_gc = self.training and self.gradient_checkpoint
899
+
900
+ # Reset the layer-adaptive audio cache for this forward. The deepest-bridge
901
+ # cache-source layer seeds it; the no-bridge tail layers (which run AFTER it,
902
+ # sequentially) read it. During checkpoint recompute it still holds the
903
+ # forward value because the tail layers recompute first (reverse order) and
904
+ # the cache is detached/no-grad, so the seed→read order is preserved.
905
+ if getattr(self, "_la_audio_cache", None) is not None:
906
+ self._la_audio_cache["norms"] = None
907
+
908
+ # --- Fused dual-tower layers ---
909
+ for i, fused_block in enumerate(self.fusion_blocks):
910
+ override = None
911
+ if use_dit_2:
912
+ override = self.video_dit_2.blocks[i]
913
+
914
+ if use_gc:
915
+ visual_x, audio_x = torch_checkpoint.checkpoint(
916
+ fused_block,
917
+ visual_x, audio_x,
918
+ visual_context, audio_context,
919
+ visual_t_mod, audio_t_mod,
920
+ visual_freqs, audio_freqs,
921
+ visual_rope_cos_sin, audio_rope_cos_sin,
922
+ a2v_scale, v2a_scale,
923
+ grid_size,
924
+ override,
925
+ timestep_ratio,
926
+ use_reentrant=False,
927
+ )
928
+ else:
929
+ visual_x, audio_x = fused_block(
930
+ visual_x, audio_x,
931
+ visual_context, audio_context,
932
+ visual_t_mod, audio_t_mod,
933
+ visual_freqs, audio_freqs,
934
+ visual_rope_cos_sin=visual_rope_cos_sin,
935
+ audio_rope_cos_sin=audio_rope_cos_sin,
936
+ a2v_scale=a2v_scale, v2a_scale=v2a_scale,
937
+ video_grid_size=grid_size,
938
+ override_video_block=override,
939
+ timestep_ratio=timestep_ratio,
940
+ )
941
+
942
+ # --- Remaining video-only layers ---
943
+ if use_dit_2:
944
+ remaining = [
945
+ self.video_dit_2.blocks[i]
946
+ for i in range(len(self.fusion_blocks), len(self.video_dit_2.blocks))
947
+ ]
948
+ else:
949
+ remaining = self.remaining_video_blocks
950
+
951
+ for block in remaining:
952
+ if use_gc:
953
+ visual_x = torch_checkpoint.checkpoint(
954
+ block, visual_x, visual_context, visual_t_mod, visual_freqs, grid_size,
955
+ None, timestep_ratio,
956
+ use_reentrant=False,
957
+ )
958
+ else:
959
+ visual_x = block(
960
+ visual_x, visual_context, visual_t_mod, visual_freqs,
961
+ grid_size=grid_size, timestep_ratio=timestep_ratio,
962
+ )
963
+
964
+ return visual_x, audio_x
hymm/models/modules/wan_audio_dit.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import math
3
+ from typing import Literal, Optional, Tuple
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
8
+ from diffusers.models.modeling_utils import ModelMixin
9
+ from einops import rearrange
10
+
11
+ from .wan_video_dit import DiTBlock
12
+ from hymm.utils.parallel_states import get_sequence_parallel_state, nccl_info, sp_seq_ctx
13
+ from hymm.utils.communications import all_gather, sp_split, assert_sp_splittable
14
+
15
+
16
+ def tag_audio_sp_stream(blocks):
17
+ """Mark a stack of DiTBlocks as operating on the audio SP stream.
18
+
19
+ The blocks are plain video DiTBlocks, so their attentions default to the
20
+ "visual" stream; without this the Ulysses all-to-all would look up the wrong
21
+ sequence length once MOVABridge fuses the two towers.
22
+ """
23
+ for block in blocks:
24
+ block.self_attn.sp_stream = "audio"
25
+ block.cross_attn.sp_stream = "audio"
26
+
27
+
28
+ def modulate(x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor):
29
+ # x is fp32 after layer norm
30
+ # print(f"{shift.dtype = }")
31
+ return (x * (1 + scale) + shift).to(shift.dtype)
32
+
33
+
34
+ def sinusoidal_embedding_1d(dim, position):
35
+ sinusoid = torch.outer(position.type(torch.float64), torch.pow(
36
+ 10000, -torch.arange(dim//2, dtype=torch.float64, device=position.device).div(dim//2)))
37
+ x = torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
38
+ return x.to(position.dtype)
39
+
40
+
41
+ def precompute_freqs_cis_3d(dim: int, end: int = 1024, theta: float = 10000.0):
42
+ # 3d rope precompute
43
+ f_freqs_cis = precompute_freqs_cis(dim - 2 * (dim // 3), end, theta)
44
+ h_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
45
+ w_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
46
+ return f_freqs_cis, h_freqs_cis, w_freqs_cis
47
+
48
+ def legacy_precompute_freqs_cis_1d(dim: int, end: int = 16384, theta: float = 10000.0, base_tps=4.0, target_tps=44100/2048):
49
+ s = float(base_tps) / float(target_tps)
50
+ # 1d rope precompute
51
+ f_freqs_cis = precompute_freqs_cis(dim - 2 * (dim // 3), end, theta, s)
52
+ # Do not apply positional encoding to the remaining dimensions.
53
+ no_freqs_cis = precompute_freqs_cis(dim // 3, end, theta, s)
54
+ no_freqs_cis = torch.ones_like(no_freqs_cis)
55
+ return f_freqs_cis, no_freqs_cis, no_freqs_cis
56
+
57
+
58
+ def precompute_freqs_cis_1d(dim: int, end: int = 16384, theta: float = 10000.0):
59
+ f_freqs_cis = precompute_freqs_cis(dim, end, theta)
60
+ return f_freqs_cis.chunk(3, dim=-1)
61
+
62
+
63
+ def precompute_freqs_cis(dim: int, end: int = 16384, theta: float = 10000.0, s: float = 1.0):
64
+ # 1d rope precompute
65
+ freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)
66
+ [: (dim // 2)].double() / dim))
67
+ pos = torch.arange(end, dtype=torch.float64, device=freqs.device) * s
68
+ freqs = torch.outer(pos, freqs)
69
+ freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64
70
+ return freqs_cis
71
+
72
+
73
+ class MLP(torch.nn.Module):
74
+ def __init__(self, in_dim, out_dim, has_pos_emb=False):
75
+ super().__init__()
76
+ self.proj = torch.nn.Sequential(
77
+ nn.LayerNorm(in_dim),
78
+ nn.Linear(in_dim, in_dim),
79
+ nn.GELU(),
80
+ nn.Linear(in_dim, out_dim),
81
+ nn.LayerNorm(out_dim)
82
+ )
83
+ self.has_pos_emb = has_pos_emb
84
+ if has_pos_emb:
85
+ self.emb_pos = torch.nn.Parameter(torch.zeros((1, 514, 1280)))
86
+
87
+ def forward(self, x):
88
+ if self.has_pos_emb:
89
+ x = x + self.emb_pos.to(dtype=x.dtype, device=x.device)
90
+ return self.proj(x)
91
+
92
+
93
+ class Head(nn.Module):
94
+ def __init__(self, dim: int, out_dim: int, patch_size: Tuple[int, int, int], eps: float):
95
+ super().__init__()
96
+ self.dim = dim
97
+ self.patch_size = patch_size
98
+ self.norm = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
99
+ self.head = nn.Linear(dim, out_dim * math.prod(patch_size))
100
+ self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
101
+
102
+ def forward(self, x, t_mod):
103
+ # print(f"{t_mod.shape = }")
104
+ if len(t_mod.shape) == 3:
105
+ shift, scale = (self.modulation.unsqueeze(0).to(dtype=t_mod.dtype, device=t_mod.device) + t_mod.unsqueeze(2)).chunk(2, dim=2)
106
+ x = (self.head(self.norm(x) * (1 + scale.squeeze(2)) + shift.squeeze(2)))
107
+ else:
108
+ # NOTE: `t_mod` used to be [B, C]. When B=1 broadcasting works, but for B>1
109
+ # it does not align with [1, 2, C]. We therefore unsqueeze at dim=1 here.
110
+ shift, scale = (self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod.unsqueeze(1)).chunk(2, dim=1)
111
+ x = (self.head(self.norm(x) * (1 + scale) + shift))
112
+ return x
113
+
114
+
115
+ class WanAudioModel(ModelMixin, ConfigMixin):
116
+ _repeated_blocks = ("DiTBlock",)
117
+
118
+ @register_to_config
119
+ def __init__(
120
+ self,
121
+ dim: int,
122
+ in_dim: int,
123
+ ffn_dim: int,
124
+ out_dim: int,
125
+ text_dim: int,
126
+ freq_dim: int,
127
+ eps: float,
128
+ patch_size: Tuple[int, int, int],
129
+ num_heads: int,
130
+ num_layers: int,
131
+ has_image_input: bool,
132
+ has_image_pos_emb: bool = False,
133
+ has_ref_conv: bool = False,
134
+ add_control_adapter: bool = False,
135
+ in_dim_control_adapter: int = 24,
136
+ seperated_timestep: bool = False,
137
+ require_vae_embedding: bool = True,
138
+ require_clip_embedding: bool = True,
139
+ fuse_vae_embedding_in_latents: bool = False,
140
+ vae_type: Literal["oobleck", "dac"] = "oobleck",
141
+ ):
142
+ super().__init__()
143
+ self.dim = dim
144
+ self.freq_dim = freq_dim
145
+ self.has_image_input = has_image_input
146
+ self.patch_size = patch_size
147
+ self.seperated_timestep = seperated_timestep
148
+ self.require_vae_embedding = require_vae_embedding
149
+ self.require_clip_embedding = require_clip_embedding
150
+ self.fuse_vae_embedding_in_latents = fuse_vae_embedding_in_latents
151
+ self.vae_type = vae_type
152
+ # self.patch_embedding = nn.Conv3d(
153
+ # in_dim, dim, kernel_size=patch_size, stride=patch_size)
154
+ self.patch_embedding = nn.Conv1d(
155
+ in_dim, dim, kernel_size=patch_size, stride=patch_size
156
+ )
157
+ self.text_embedding = nn.Sequential(
158
+ nn.Linear(text_dim, dim),
159
+ nn.GELU(approximate='tanh'),
160
+ nn.Linear(dim, dim)
161
+ )
162
+ self.time_embedding = nn.Sequential(
163
+ nn.Linear(freq_dim, dim),
164
+ nn.SiLU(),
165
+ nn.Linear(dim, dim)
166
+ )
167
+ self.time_projection = nn.Sequential(
168
+ nn.SiLU(), nn.Linear(dim, dim * 6))
169
+ self.blocks = nn.ModuleList([
170
+ DiTBlock(has_image_input, dim, num_heads, ffn_dim, eps)
171
+ for _ in range(num_layers)
172
+ ])
173
+ tag_audio_sp_stream(self.blocks)
174
+ self.head = Head(dim, out_dim, patch_size, eps)
175
+ head_dim = dim // num_heads
176
+ if vae_type == "oobleck":
177
+ self.freqs = legacy_precompute_freqs_cis_1d(head_dim, base_tps=4.0, target_tps=44100/2048)
178
+ elif vae_type == "dac":
179
+ self.freqs = precompute_freqs_cis_1d(head_dim)
180
+ else:
181
+ raise ValueError(f"Invalid VAE type: {vae_type}")
182
+
183
+ if has_image_input:
184
+ self.img_emb = MLP(1280, dim, has_pos_emb=has_image_pos_emb) # clip_feature_dim = 1280
185
+ if has_ref_conv:
186
+ self.ref_conv = nn.Conv2d(16, dim, kernel_size=(2, 2), stride=(2, 2))
187
+ self.has_image_pos_emb = has_image_pos_emb
188
+ self.has_ref_conv = has_ref_conv
189
+ self.control_adapter = None
190
+
191
+ def patchify(self, x: torch.Tensor, control_camera_latents_input: Optional[torch.Tensor] = None):
192
+ x = self.patch_embedding(x)
193
+ if self.control_adapter is not None and control_camera_latents_input is not None:
194
+ y_camera = self.control_adapter(control_camera_latents_input)
195
+ x = [u + v for u, v in zip(x, y_camera)]
196
+ x = x[0].unsqueeze(0)
197
+ grid_size = x.shape[2:]
198
+ x = rearrange(x, 'b c f -> b f c').contiguous()
199
+ return x, grid_size # x, grid_size: (f)
200
+
201
+ def unpatchify(self, x: torch.Tensor, grid_size: torch.Tensor):
202
+ return rearrange(
203
+ x, 'b f (p c) -> b c (f p)',
204
+ f=grid_size[0],
205
+ p=self.patch_size[0]
206
+ )
207
+
208
+ def forward(self,
209
+ x: torch.Tensor,
210
+ timestep: torch.Tensor,
211
+ context: torch.Tensor,
212
+ clip_feature: Optional[torch.Tensor] = None,
213
+ y: Optional[torch.Tensor] = None,
214
+ use_gradient_checkpointing: bool = False,
215
+ use_gradient_checkpointing_offload: bool = False,
216
+ **kwargs,
217
+ ):
218
+ t = self.time_embedding(
219
+ sinusoidal_embedding_1d(self.freq_dim, timestep))
220
+ t_mod = self.time_projection(t).unflatten(1, (6, self.dim))
221
+ context = self.text_embedding(context)
222
+
223
+ if self.has_image_input:
224
+ x = torch.cat([x, y], dim=1) # (b, c_x + c_y, f, h, w)
225
+ clip_embdding = self.img_emb(clip_feature)
226
+ context = torch.cat([clip_embdding, context], dim=1)
227
+
228
+ x, (f, ) = self.patchify(x)
229
+
230
+ freqs = torch.cat([
231
+ self.freqs[0][:f].view(f, -1).expand(f, -1),
232
+ self.freqs[1][:f].view(f, -1).expand(f, -1),
233
+ self.freqs[2][:f].view(f, -1).expand(f, -1),
234
+ ], dim=-1).reshape(f, 1, -1).to(x.device)
235
+
236
+ if get_sequence_parallel_state():
237
+ sp_size = nccl_info.sp_size
238
+ sp_rank = nccl_info.rank_within_spgroup
239
+ assert_sp_splittable(f, sp_size, "audio sequence")
240
+ sp_seq_ctx.set(audio=f)
241
+ x = sp_split(x, sp_size, sp_rank, dim=1)
242
+ freqs = sp_split(freqs, sp_size, sp_rank, dim=0)
243
+
244
+ def create_custom_forward(module):
245
+ def custom_forward(*inputs):
246
+ return module(*inputs)
247
+ return custom_forward
248
+
249
+ for block in self.blocks:
250
+ if self.training and use_gradient_checkpointing:
251
+ if use_gradient_checkpointing_offload:
252
+ with torch.autograd.graph.save_on_cpu():
253
+ x = torch.utils.checkpoint.checkpoint(
254
+ create_custom_forward(block),
255
+ x, context, t_mod, freqs,
256
+ use_reentrant=False,
257
+ )
258
+ else:
259
+ x = torch.utils.checkpoint.checkpoint(
260
+ create_custom_forward(block),
261
+ x, context, t_mod, freqs,
262
+ use_reentrant=False,
263
+ )
264
+ else:
265
+ x = block(x, context, t_mod, freqs)
266
+
267
+ x = self.head(x, t)
268
+ if get_sequence_parallel_state():
269
+ x = all_gather(x, dim=1, group=nccl_info.sp_group, full_seq_len=f)
270
+ x = self.unpatchify(x, (f, ))
271
+ return x
hymm/models/modules/wan_video_dit.py ADDED
@@ -0,0 +1,1324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import math
3
+ from typing import Optional, Tuple
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
9
+ from diffusers.models.modeling_utils import ModelMixin
10
+ from einops import rearrange
11
+ from torch.distributed.tensor import DTensor
12
+ from torch.nn import RMSNorm
13
+
14
+ from hymm.utils.parallel_states import get_sequence_parallel_state, nccl_info, sp_seq_ctx
15
+ from hymm.utils.communications import (
16
+ all_to_all_4D, all_gather,
17
+ sp_pad_num_heads, sp_pad_heads, sp_num_local_real_heads,
18
+ sp_split, assert_sp_splittable,
19
+ )
20
+ from hymm.models.modules.block_sparse_attention import flash_attn_bsa_3d
21
+
22
+ _DYN_BLOCK_FWD_ID = 0
23
+
24
+
25
+ def advance_dynamic_block_pass_id():
26
+ """Call once at the start of each real outer forward (never from recompute)."""
27
+ global _DYN_BLOCK_FWD_ID
28
+ _DYN_BLOCK_FWD_ID += 1
29
+ return _DYN_BLOCK_FWD_ID
30
+
31
+
32
+ def current_dynamic_block_pass_id():
33
+ return _DYN_BLOCK_FWD_ID
34
+
35
+
36
+ try:
37
+ from flash_attn_interface import flash_attn_func as flash_attn_3_func
38
+ print("flash_attn_interface loaded from flash_attn_interface")
39
+ FLASH_ATTN_3_AVAILABLE = True
40
+ except ModuleNotFoundError:
41
+ FLASH_ATTN_3_AVAILABLE = False
42
+ try:
43
+ from kernels import get_kernel
44
+ flash_attn_kernel = get_kernel("kernels-community/flash-attn3")
45
+ flash_attn_3_func = flash_attn_kernel.flash_attn_func
46
+ print("flash_attn_interface loaded from kernels-community/flash-attn3")
47
+ FLASH_ATTN_3_AVAILABLE = True
48
+ except ModuleNotFoundError:
49
+ FLASH_ATTN_3_AVAILABLE = False
50
+
51
+ try:
52
+ import flash_attn
53
+ FLASH_ATTN_2_AVAILABLE = True
54
+ except ModuleNotFoundError:
55
+ FLASH_ATTN_2_AVAILABLE = False
56
+
57
+ try:
58
+ from sageattention import sageattn
59
+ SAGE_ATTN_AVAILABLE = True
60
+ except ModuleNotFoundError:
61
+ SAGE_ATTN_AVAILABLE = False
62
+
63
+ try:
64
+ from yunchang import LongContextAttention
65
+ from yunchang.kernels import AttnType
66
+ LONG_CONTEXT_ATTN_AVAILABLE = True
67
+ except Exception:
68
+ LONG_CONTEXT_ATTN_AVAILABLE = False
69
+ LongContextAttention = None
70
+ # Fallback so module import succeeds even when yunchang isn't available.
71
+ # `USPAttention` will error if instantiated without LongContextAttention.
72
+ class AttnType: # type: ignore
73
+ FA = None
74
+ FA3 = None
75
+
76
+
77
+ def flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, num_heads: int, compatibility_mode=False):
78
+ if compatibility_mode:
79
+ q = rearrange(q, "b s (n d) -> b n s d", n=num_heads)
80
+ k = rearrange(k, "b s (n d) -> b n s d", n=num_heads)
81
+ v = rearrange(v, "b s (n d) -> b n s d", n=num_heads)
82
+ x = F.scaled_dot_product_attention(q, k, v)
83
+ x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
84
+ elif FLASH_ATTN_3_AVAILABLE:
85
+ q = rearrange(q, "b s (n d) -> b s n d", n=num_heads)
86
+ k = rearrange(k, "b s (n d) -> b s n d", n=num_heads)
87
+ v = rearrange(v, "b s (n d) -> b s n d", n=num_heads)
88
+ x = flash_attn_3_func(q, k, v)
89
+ if isinstance(x,tuple):
90
+ x = x[0]
91
+ x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
92
+ elif FLASH_ATTN_2_AVAILABLE:
93
+ q = rearrange(q, "b s (n d) -> b s n d", n=num_heads)
94
+ k = rearrange(k, "b s (n d) -> b s n d", n=num_heads)
95
+ v = rearrange(v, "b s (n d) -> b s n d", n=num_heads)
96
+ x = flash_attn.flash_attn_func(q, k, v)
97
+ x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
98
+ elif SAGE_ATTN_AVAILABLE:
99
+ q = rearrange(q, "b s (n d) -> b s n d", n=num_heads)
100
+ k = rearrange(k, "b s (n d) -> b s n d", n=num_heads)
101
+ v = rearrange(v, "b s (n d) -> b s n d", n=num_heads)
102
+ x = sageattn(q, k, v, tensor_layout="NHD")
103
+ x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
104
+ else:
105
+ q = rearrange(q, "b s (n d) -> b n s d", n=num_heads)
106
+ k = rearrange(k, "b s (n d) -> b n s d", n=num_heads)
107
+ v = rearrange(v, "b s (n d) -> b n s d", n=num_heads)
108
+ x = F.scaled_dot_product_attention(q, k, v)
109
+ x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
110
+ return x
111
+
112
+
113
+ @torch.compile(fullgraph=True)
114
+ def modulate(x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor):
115
+ return (x * (1 + scale) + shift)
116
+
117
+
118
+ def sinusoidal_embedding_1d(dim, position):
119
+ sinusoid = torch.outer(position.type(torch.float64), torch.pow(
120
+ 10000, -torch.arange(dim//2, dtype=torch.float64, device=position.device).div(dim//2)))
121
+ x = torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
122
+ return x.to(position.dtype)
123
+
124
+
125
+ def precompute_freqs_cis_3d(dim: int, end: int = 1024, theta: float = 10000.0):
126
+ # 3d rope precompute
127
+ f_freqs_cis = precompute_freqs_cis(dim - 2 * (dim // 3), end, theta)
128
+ h_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
129
+ w_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
130
+ return f_freqs_cis, h_freqs_cis, w_freqs_cis
131
+
132
+
133
+ def precompute_freqs_cis(dim: int, end: int = 1024, theta: float = 10000.0):
134
+ # 1d rope precompute
135
+ freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)
136
+ [: (dim // 2)].double() / dim))
137
+ freqs = torch.outer(torch.arange(end, device=freqs.device), freqs)
138
+ freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64
139
+ return freqs_cis
140
+
141
+
142
+ def _rope_rotate(x_4d, freqs):
143
+ """RoPE via sin/cos rotation in the input dtype — avoids float64 allocation.
144
+
145
+ Complex multiply (a+bi)(c+di) = (ac-bd) + (ad+bc)i is equivalent to:
146
+ out_even = x_even * cos - x_odd * sin
147
+ out_odd = x_even * sin + x_odd * cos
148
+ where freqs = cos + i*sin (complex64).
149
+
150
+ Args:
151
+ x_4d: [B, S, n, head_dim] in bf16/fp32.
152
+ freqs: [S, 1, head_dim//2] complex64.
153
+ Returns:
154
+ [B, S, n*head_dim] in same dtype as x_4d.
155
+ """
156
+ cos_f = freqs.real.unsqueeze(0).to(x_4d.dtype) # [1, S, 1, head_dim//2]
157
+ sin_f = freqs.imag.unsqueeze(0).to(x_4d.dtype) # [1, S, 1, head_dim//2]
158
+ x_pairs = x_4d.unflatten(-1, (-1, 2)) # [..., head_dim//2, 2]
159
+ x_even = x_pairs[..., 0] # view
160
+ x_odd = x_pairs[..., 1] # view
161
+ out_even = x_even * cos_f
162
+ out_even.sub_(x_odd * sin_f)
163
+ out_odd = x_even * sin_f
164
+ out_odd.add_(x_odd * cos_f)
165
+ return torch.stack([out_even, out_odd], dim=-1).flatten(-2).flatten(2)
166
+
167
+
168
+ @torch.amp.autocast('cuda', enabled=False)
169
+ def rope_apply(x, freqs, num_heads):
170
+ x = rearrange(x, "b s (n d) -> b s n d", n=num_heads)
171
+ x_out = torch.view_as_complex(x.to(torch.float64).reshape(x.shape[0], x.shape[1], x.shape[2], -1, 2))
172
+ x_out = torch.view_as_real(x_out * freqs).flatten(2)
173
+ return x_out.to(x.dtype)
174
+ # return _rope_rotate(x, freqs)
175
+
176
+
177
+ @torch.amp.autocast('cuda', enabled=False)
178
+ def rope_apply_head_dim(x, freqs, head_dim):
179
+ x = rearrange(x, "b s (n d) -> b s n d", d=head_dim)
180
+ x_out = torch.view_as_complex(x.to(torch.float64).reshape(x.shape[0], x.shape[1], x.shape[2], -1, 2))
181
+ x_out = torch.view_as_real(x_out * freqs).flatten(2)
182
+ return x_out.to(x.dtype)
183
+ # return _rope_rotate(x, freqs)
184
+
185
+
186
+ class SlowRMSNorm(nn.Module):
187
+ def __init__(self, dim, eps=1e-5):
188
+ super().__init__()
189
+ self.eps = eps
190
+ self.weight = nn.Parameter(torch.ones(dim))
191
+
192
+ def norm(self, x):
193
+ return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
194
+
195
+ def forward(self, x):
196
+ dtype = x.dtype
197
+ return (self.norm(x.float()) * self.weight).to(dtype)
198
+
199
+
200
+ class AttentionModule(nn.Module):
201
+ def __init__(self, num_heads):
202
+ super().__init__()
203
+ self.num_heads = num_heads
204
+
205
+ def forward(self, q, k, v):
206
+ x = flash_attention(q=q, k=k, v=v, num_heads=self.num_heads)
207
+ return x
208
+
209
+
210
+ class SelfAttention(nn.Module):
211
+ def __init__(self, dim: int, num_heads: int, eps: float = 1e-6,
212
+ enable_bsa: bool = False, bsa_params: dict = None,
213
+ enable_audio_guidance: bool = False,
214
+ enable_audio_concentration_gate: bool = False,
215
+ enable_timestep_reliability_gate: bool = False,
216
+ enable_variance_guidance: bool = False):
217
+ super().__init__()
218
+ self.dim = dim
219
+ self.num_heads = num_heads
220
+ self.head_dim = dim // num_heads
221
+
222
+ self.q = nn.Linear(dim, dim)
223
+ self.k = nn.Linear(dim, dim)
224
+ self.v = nn.Linear(dim, dim)
225
+ self.o = nn.Linear(dim, dim)
226
+ self.norm_q = RMSNorm(dim, eps=eps)
227
+ self.norm_k = RMSNorm(dim, eps=eps)
228
+
229
+ self.attn = AttentionModule(self.num_heads)
230
+ self.enable_bsa = enable_bsa
231
+ self.bsa_params = bsa_params or {}
232
+
233
+ # Which SP-sharded stream this attention operates on. MOVABridge retags
234
+ # the audio tower to "audio"; see sp_seq_ctx in parallel_states.
235
+ self.sp_stream = "visual"
236
+
237
+ # --- Audio Guidance (Section 3.5.1 Path A: Post-QK Score Modulation) ---
238
+ self.enable_audio_guidance = enable_audio_guidance
239
+ self.enable_audio_concentration_gate = enable_audio_concentration_gate
240
+ self.enable_timestep_reliability_gate = enable_timestep_reliability_gate
241
+ # --- Audio Weighted Pooling (Section 3.5.2 Path B: Pre-QK Representation Enhancement) ---
242
+ self.enable_audio_weighted_pooling = False
243
+
244
+ # --- Channel-Variance Guidance (non-learnable, no gate) ---
245
+ self.enable_variance_guidance = enable_variance_guidance
246
+
247
+ # --- Bias correction (two independent methods, at most one enabled) ---
248
+ self.enable_taylor_sparse_attn = False # LIVEditor method
249
+ self.enable_rectified_sparse_attn = False # Rectified SpaAttn method
250
+
251
+ # --- Anisotropic Dynamic Block Shape (Section 8; two methods, at most
252
+ # one enabled). When EITHER is on, ALL of audio-guidance,
253
+ # variance-guidance and bias-correction are disabled (full isolation).
254
+ self.enable_ivpq_dynamic_block = False # Section 8.5
255
+ self.enable_penalty_dynamic_block = False # Section 8.6
256
+
257
+ # (fwd_id, grid_padded, shape_ids) cache; training-only, reused on recompute.
258
+ self._dyn_shape_cache = None
259
+
260
+ # --- Layer-Adaptive Dynamic Block Shape (Section 8.2) ---
261
+ # Optional wrapper on top of a dynamic-block method. When enabled:
262
+ # * shallow layers (idx < _la_split) use FIXED uniform-shape BSA;
263
+ # * deep layers (idx >= _la_split) use the dynamic-block method;
264
+ # * deep layers WITH their own a2v bridge use their own audio; deep
265
+ # layers WITHOUT a bridge (tail beyond audio_layers) reuse the cached
266
+ # audio norms (seeded by the deepest bridge layer = cache-source) to
267
+ # drive the audio-directional gradient, combined with the deep layer's
268
+ # own video channel-variance.
269
+ # All fields are plain attributes set by configure_layer_adaptive_dynamic_block.
270
+ self.enable_layer_adaptive_dynamic_block = False
271
+ self._la_layer_idx = None # this self-attn's global video-layer index
272
+ self._la_split = None # shallow/deep boundary (num_video_layers // 2)
273
+ self._la_is_cache_source = False # deepest shallow bridge layer → seeds the cache
274
+ self._la_audio_cache = None # shared {'norms': tensor|None} across all video self-attns
275
+
276
+ _bsa_fallback_logged = False
277
+ _path_b_in_correction_warned = False
278
+
279
+ def _check_bsa(self, grid_size):
280
+ """
281
+ Check if BSA can be applied. Returns (can_use, fallback_reason).
282
+ Divisibility is NOT required — internal padding + masking handles arbitrary T/H/W.
283
+ When sparsity=0, BSA selects ALL blocks and produces results identical to full attention.
284
+ """
285
+ if not self.enable_bsa:
286
+ return False, None
287
+ if grid_size is None:
288
+ return False, "grid_size is None"
289
+ T, H, W = grid_size
290
+ if T <= 1:
291
+ return False, f"T={T} <= 1 (image mode)"
292
+ return True, None
293
+
294
+ def _prepare_audio_guidance(self, audio_token_norms, q_bhsd, valid_mask,
295
+ T, H, W, pad_t, pad_h, pad_w, need_pad,
296
+ timestep_ratio, use_path_a, use_path_b):
297
+ """Prepare audio guidance parameters (isolated from variance guidance)."""
298
+ from hymm.models.modules.block_sparse_attention import (
299
+ compute_audio_spatial_concentration_gate,
300
+ compute_timestep_reliability_gate,
301
+ )
302
+ if need_pad:
303
+ norms_3d = audio_token_norms.view(T, H, W)
304
+ norms_padded = F.pad(norms_3d, (0, pad_w, 0, pad_h, 0, pad_t), value=0.0)
305
+ audio_norms_padded = norms_padded.reshape(-1).contiguous()
306
+ else:
307
+ audio_norms_padded = audio_token_norms.contiguous()
308
+
309
+ audio_gate = torch.ones(1, device=q_bhsd.device, dtype=q_bhsd.dtype)
310
+ audio_boost_gamma = torch.zeros(1, device=q_bhsd.device, dtype=q_bhsd.dtype)
311
+ audio_weighted_lambda = 0.0
312
+
313
+ if use_path_a:
314
+ if self.enable_timestep_reliability_gate and timestep_ratio is not None:
315
+ r_t = compute_timestep_reliability_gate(
316
+ timestep_ratio,
317
+ mode=self.bsa_params.get('timestep_gate_mode', 'power'),
318
+ power=self.bsa_params.get('timestep_gate_power', 1.5),
319
+ scale=self.bsa_params.get('timestep_gate_scale', 10.0),
320
+ threshold=self.bsa_params.get('timestep_gate_threshold', 0.7),
321
+ )
322
+ else:
323
+ r_t = torch.ones(1, device=q_bhsd.device, dtype=q_bhsd.dtype)
324
+
325
+ if self.enable_audio_concentration_gate:
326
+ g_a = compute_audio_spatial_concentration_gate(
327
+ audio_norms_padded, valid_mask=valid_mask)
328
+ else:
329
+ g_a = torch.ones(1, device=q_bhsd.device, dtype=q_bhsd.dtype)
330
+
331
+ audio_gate = r_t * g_a
332
+ audio_boost_gamma = torch.tensor(
333
+ self.bsa_params.get('audio_boost_gamma', 1.0),
334
+ device=q_bhsd.device, dtype=q_bhsd.dtype,
335
+ )
336
+
337
+ if use_path_b:
338
+ audio_weighted_lambda = self.bsa_params.get('audio_weighted_lambda', 1.0)
339
+
340
+ return audio_norms_padded, audio_gate, audio_boost_gamma, audio_weighted_lambda
341
+
342
+ def _run_bsa_dynamic(self, q_bhsd, k_bhsd, v_bhsd, grid_size, audio_token_norms=None):
343
+ """Anisotropic Dynamic Block Shape BSA (Section 8) — video self-attn only.
344
+
345
+ Fully isolated from `_run_bsa`'s uniform path. Pads T/H/W to a multiple of
346
+ the macro-zone size (8) using the SAME internal padding+mask+unpad
347
+ contract, builds a `valid_mask` so padded tokens never participate, then
348
+ defers all the heavy lifting (per-zone shape selection, anisotropic
349
+ rearrange, logical-block selection, Triton kernel reuse, scatter back) to
350
+ `flash_attn_bsa_3d_dynamic`.
351
+
352
+ `audio_token_norms` (per-token ||Δh_v||_2 of the A→V bridge residual, in
353
+ THW order, full sequence) drives the audio-directional gradient when
354
+ available; it is optional (silence/no-bridge → video-only shape decision).
355
+ """
356
+ from hymm.models.modules.block_sparse_attention.dynamic_block_attention import (
357
+ flash_attn_bsa_3d_dynamic,
358
+ )
359
+ from hymm.models.modules.block_sparse_attention.dynamic_block_shape import ZONE_SIZE
360
+
361
+ T, H, W = grid_size
362
+ B, n_heads, S, D = q_bhsd.shape
363
+
364
+ pad_t = (ZONE_SIZE - T % ZONE_SIZE) % ZONE_SIZE
365
+ pad_h = (ZONE_SIZE - H % ZONE_SIZE) % ZONE_SIZE
366
+ pad_w = (ZONE_SIZE - W % ZONE_SIZE) % ZONE_SIZE
367
+ need_pad = pad_t > 0 or pad_h > 0 or pad_w > 0
368
+
369
+ valid_mask = None
370
+ audio_norms_p = audio_token_norms
371
+ if need_pad:
372
+ T_p, H_p, W_p = T + pad_t, H + pad_h, W + pad_w
373
+ idx_t = torch.arange(T_p, device=q_bhsd.device)
374
+ idx_h = torch.arange(H_p, device=q_bhsd.device)
375
+ idx_w = torch.arange(W_p, device=q_bhsd.device)
376
+ valid_mask = (
377
+ (idx_t[:, None, None] < T)
378
+ & (idx_h[None, :, None] < H)
379
+ & (idx_w[None, None, :] < W)
380
+ ).reshape(-1).contiguous()
381
+
382
+ def _pad_3d(t):
383
+ t_3d = t.view(B, n_heads, T, H, W, D)
384
+ t_padded = F.pad(t_3d, (0, 0, 0, pad_w, 0, pad_h, 0, pad_t))
385
+ return t_padded.reshape(B, n_heads, -1, D).contiguous()
386
+
387
+ q_bhsd = _pad_3d(q_bhsd)
388
+ k_bhsd = _pad_3d(k_bhsd)
389
+ v_bhsd = _pad_3d(v_bhsd)
390
+ if audio_token_norms is not None:
391
+ norms_3d = audio_token_norms.reshape(T, H, W)
392
+ audio_norms_p = F.pad(
393
+ norms_3d, (0, pad_w, 0, pad_h, 0, pad_t), value=0.0
394
+ ).reshape(-1).contiguous()
395
+ grid_padded = (T_p, H_p, W_p)
396
+ else:
397
+ grid_padded = grid_size
398
+
399
+ method = "ivpq" if self.enable_ivpq_dynamic_block else "penalty"
400
+
401
+ # Training-only: reuse this forward's shape decision on the recompute
402
+ # (cache hit when fwd_id+grid match). Eval bypasses the cache entirely.
403
+ use_shape_cache = self.training
404
+ cached_shape_ids = None
405
+ if use_shape_cache:
406
+ cur_fwd_id = current_dynamic_block_pass_id()
407
+ cached = self._dyn_shape_cache
408
+ if (cached is not None and cached[0] == cur_fwd_id
409
+ and cached[1] == grid_padded):
410
+ cached_shape_ids = cached[2]
411
+
412
+ out, used_shape_ids = flash_attn_bsa_3d_dynamic(
413
+ q_bhsd, k_bhsd, v_bhsd, grid_padded,
414
+ method=method,
415
+ sparsity=self.bsa_params.get('sparsity', 0.9375),
416
+ cdf_threshold=self.bsa_params.get('cdf_threshold', None),
417
+ audio_token_norms=audio_norms_p,
418
+ valid_mask=valid_mask,
419
+ lambda_a=self.bsa_params.get('dynamic_block_lambda_a', 0.5),
420
+ tau_128=self.bsa_params.get('dynamic_block_tau_128', 0.15),
421
+ lambda_128=self.bsa_params.get('dynamic_block_lambda_128', 1.0),
422
+ shape_ids=cached_shape_ids,
423
+ return_shape_ids=True,
424
+ )
425
+
426
+ if use_shape_cache:
427
+ self._dyn_shape_cache = (cur_fwd_id, grid_padded, used_shape_ids)
428
+
429
+ if need_pad:
430
+ out_3d = out.view(B, n_heads, T_p, H_p, W_p, D)
431
+ out = out_3d[:, :, :T, :H, :W, :].contiguous().reshape(B, n_heads, T * H * W, D)
432
+ return out
433
+
434
+ def _run_bsa(self, q_bhsd, k_bhsd, v_bhsd, grid_size, audio_token_norms=None, timestep_ratio=None):
435
+ """Run BSA with internal temporal+spatial padding/masking/unpadding.
436
+
437
+ Execution order (when all features are enabled):
438
+ 1. Padding (temporal + spatial) + valid_mask creation
439
+ 2. Audio/Variance score modulation (multiplicative boost)
440
+ 3. Block selection (top-k / top-p)
441
+ 4. BSA Triton kernel → o_spa
442
+ 5. **Bias rectification** (post-hoc, training-free, Section 3.6):
443
+ a. Reproduce block-level scoring (O(N_q*N_k), negligible cost)
444
+ b. Compute R_n (selected weight fraction), M_n (query sharpness)
445
+ c. Compute O_nonsel_pool (Taylor approximation from non-selected blocks)
446
+ d. flat: O = R_n * O_spa + O_nonsel_pool
447
+ sharp: O = R_n * O_spa
448
+ 6. Unpadding
449
+
450
+ Bias correction controlled by enable_taylor_sparse_attn / enable_rectified_sparse_attn.
451
+ Isolated from audio/variance guidance code paths.
452
+ """
453
+ # === Anisotropic Dynamic Block Shape (Section 8) — fully isolated path ===
454
+ # When a dynamic-block trigger is on we route to a dedicated method and
455
+ # never touch the uniform-shape / audio / variance / bias-correction code
456
+ # below. The two dynamic methods are mutually exclusive with each other
457
+ # and with all the other BSA features (enforced in configure_* + asserts).
458
+ use_dynamic = self.enable_ivpq_dynamic_block or self.enable_penalty_dynamic_block
459
+ if use_dynamic and self.enable_layer_adaptive_dynamic_block \
460
+ and self._la_layer_idx is not None and self._la_split is not None:
461
+ # Layer-adaptive (Section 8.2): shallow half → FIXED uniform-shape BSA
462
+ # (falls through to the uniform dispatch below); deep half → dynamic.
463
+ if self._la_layer_idx < self._la_split:
464
+ use_dynamic = False
465
+ if use_dynamic:
466
+ return self._run_bsa_dynamic(
467
+ q_bhsd, k_bhsd, v_bhsd, grid_size, audio_token_norms=audio_token_norms,
468
+ )
469
+
470
+ from hymm.models.modules.block_sparse_attention import flash_attn_bsa_3d_audio_guided
471
+
472
+ T, H, W = grid_size
473
+ cq = self.bsa_params.get('chunk_3d_shape_q', [4, 4, 4])
474
+ ck = self.bsa_params.get('chunk_3d_shape_k', [4, 4, 4])
475
+ chunk_t = max(cq[0], ck[0])
476
+ chunk_h = max(cq[1], ck[1])
477
+ chunk_w = max(cq[2], ck[2])
478
+
479
+ pad_t = (chunk_t - T % chunk_t) % chunk_t
480
+ pad_h = (chunk_h - H % chunk_h) % chunk_h
481
+ pad_w = (chunk_w - W % chunk_w) % chunk_w
482
+
483
+ B, n_heads, S, D = q_bhsd.shape
484
+ need_pad = pad_t > 0 or pad_h > 0 or pad_w > 0
485
+
486
+ if need_pad:
487
+ T_p, H_p, W_p = T + pad_t, H + pad_h, W + pad_w
488
+
489
+ idx_t = torch.arange(T_p, device=q_bhsd.device)
490
+ idx_h = torch.arange(H_p, device=q_bhsd.device)
491
+ idx_w = torch.arange(W_p, device=q_bhsd.device)
492
+ valid_mask = (
493
+ (idx_t[:, None, None] < T)
494
+ & (idx_h[None, :, None] < H)
495
+ & (idx_w[None, None, :] < W)
496
+ ).reshape(-1).contiguous()
497
+
498
+ def _pad_3d(t):
499
+ t_3d = t.view(B, n_heads, T, H, W, D)
500
+ t_padded = F.pad(t_3d, (0, 0, 0, pad_w, 0, pad_h, 0, pad_t))
501
+ return t_padded.reshape(B, n_heads, -1, D).contiguous()
502
+
503
+ q_bhsd = _pad_3d(q_bhsd)
504
+ k_bhsd = _pad_3d(k_bhsd)
505
+ v_bhsd = _pad_3d(v_bhsd)
506
+ grid_padded = (T_p, H_p, W_p)
507
+ else:
508
+ grid_padded = grid_size
509
+ valid_mask = None
510
+
511
+ # --- Separate guidance-specific keys from core BSA params ---
512
+ _GUIDANCE_ONLY_KEYS = ('audio_boost_gamma', 'audio_weighted_lambda',
513
+ 'timestep_gate_mode', 'timestep_gate_power',
514
+ 'timestep_gate_scale', 'timestep_gate_threshold',
515
+ 'variance_boost_gamma',
516
+ 'taylor_alpha_f',
517
+ # dynamic-block knobs — never go to the uniform kernel
518
+ # (used by shallow layers under layer-adaptive mode)
519
+ 'dynamic_block_lambda_a', 'dynamic_block_tau_128',
520
+ 'dynamic_block_lambda_128')
521
+ bsa_kwargs = {k: v for k, v in self.bsa_params.items() if k not in _GUIDANCE_ONLY_KEYS}
522
+
523
+ has_audio_norms = (audio_token_norms is not None and audio_token_norms.numel() > 0)
524
+ use_path_a = self.enable_audio_guidance and has_audio_norms
525
+ use_path_b = self.enable_audio_weighted_pooling and has_audio_norms
526
+ use_audio = use_path_a or use_path_b
527
+ use_variance = self.enable_variance_guidance
528
+
529
+ # =====================================================================
530
+ # Prepare audio guidance data (shared by both dispatch and bias_rect).
531
+ # Done once, used in whichever path is active.
532
+ # =====================================================================
533
+ audio_norms_padded = audio_gate = audio_boost_gamma = audio_weighted_lambda = None
534
+ if use_audio:
535
+ audio_norms_padded, audio_gate, audio_boost_gamma, audio_weighted_lambda = (
536
+ self._prepare_audio_guidance(
537
+ audio_token_norms, q_bhsd, valid_mask, T, H, W,
538
+ pad_t, pad_h, pad_w, need_pad, timestep_ratio,
539
+ use_path_a, use_path_b,
540
+ )
541
+ )
542
+
543
+ # =====================================================================
544
+ # Dispatch: 3 mutually exclusive modes
545
+ # 1. Taylor sparse attn (LIVEditor) — selected + Taylor in one softmax
546
+ # 2. Rectified sparse attn (Rectified SpaAttn) — R_n * o_spa + o_ncri
547
+ # 3. Plain BSA (default) — original paths, unchanged
548
+ # Audio/variance guidance affects block SELECTION only (score_modifier).
549
+ # =====================================================================
550
+ use_correction = self.enable_taylor_sparse_attn or self.enable_rectified_sparse_attn
551
+
552
+ if use_correction:
553
+ # Bias rectification lives in a fully-isolated module so that the
554
+ # audio-guidance / variance-guidance code paths above are untouched.
555
+ from hymm.models.modules.block_sparse_attention.bias_rectification import (
556
+ bsa_taylor_sparse_attn, bsa_rectified_sparse_attn,
557
+ )
558
+ from hymm.models.modules.block_sparse_attention.bsa_interface import (
559
+ rearrange_THW_to_3d_block, rearrange_3d_block_to_THW,
560
+ rearrange_THW_to_3d_block_1d,
561
+ )
562
+
563
+ _ck = bsa_kwargs.get('chunk_3d_shape_k', [4, 4, 4])
564
+ _cq = bsa_kwargs.get('chunk_3d_shape_q', [4, 4, 4])
565
+ _tk, _hk, _wk = _ck[0], _ck[1], _ck[2]
566
+ _tq, _hq, _wq = _cq[0], _cq[1], _cq[2]
567
+ _Tp, _Hp, _Wp = grid_padded
568
+ _Ntk, _Nhk, _Nwk = _Tp // _tk, _Hp // _hk, _Wp // _wk
569
+ _Ntq, _Nhq, _Nwq = _Tp // _tq, _Hp // _hq, _Wp // _wq
570
+
571
+ # Rearrange once, reuse for variance/audio/correction
572
+ q_ro = rearrange_THW_to_3d_block(q_bhsd, _Ntq, _Nhq, _Nwq, _tq, _hq, _wq, D)
573
+ k_ro = rearrange_THW_to_3d_block(k_bhsd, _Ntk, _Nhk, _Nwk, _tk, _hk, _wk, D)
574
+ v_ro = rearrange_THW_to_3d_block(v_bhsd, _Ntk, _Nhk, _Nwk, _tk, _hk, _wk, D)
575
+ kv_vm_ro = rearrange_THW_to_3d_block_1d(valid_mask, _Ntk, _Nhk, _Nwk, _tk, _hk, _wk) if valid_mask is not None else None
576
+
577
+ score_mod = None
578
+ if use_variance:
579
+ from hymm.models.modules.block_sparse_attention.bsa_interface import compute_channel_variance_density
580
+ with torch.no_grad():
581
+ _rho = compute_channel_variance_density(v_ro, _tk*_hk*_wk, _tk, _hk, _wk, kv_vm_ro)
582
+ score_mod = self.bsa_params.get('variance_boost_gamma', 1.0) * _rho.to(q_bhsd.dtype).view(1, 1, 1, -1)
583
+
584
+ if use_audio and audio_norms_padded is not None:
585
+ # Path A boost: γ_audio * (g_a * S̃_j + (1 - g_a) / N_bk)
586
+ # All ops stay on-GPU. Avoid `.item()` here — `audio_gate` and
587
+ # `audio_boost_gamma` are 1-element CUDA tensors, and `.item()`
588
+ # synchronises the GPU pipeline (each call drains the queue;
589
+ # 30 layers x 50 steps x 2 (checkpoint recompute) = 3000 syncs
590
+ # per training step). Use plain tensor broadcasting instead.
591
+ from hymm.models.modules.block_sparse_attention.bsa_interface import compute_block_audio_saliency
592
+ _anorms_bo = rearrange_THW_to_3d_block_1d(audio_norms_padded, _Ntk, _Nhk, _Nwk, _tk, _hk, _wk)
593
+ _bsal = compute_block_audio_saliency(_anorms_bo, _tk*_hk*_wk, valid_mask=kv_vm_ro)
594
+ _N_bk = _Ntk * _Nhk * _Nwk
595
+ # Cast scalar/Python audio_gate to a 1-elem tensor for uniform broadcast.
596
+ if not torch.is_tensor(audio_gate):
597
+ audio_gate = torch.tensor(audio_gate, device=q_bhsd.device, dtype=q_bhsd.dtype)
598
+ if not torch.is_tensor(audio_boost_gamma):
599
+ audio_boost_gamma = torch.tensor(audio_boost_gamma, device=q_bhsd.device, dtype=q_bhsd.dtype)
600
+ _gv = audio_gate.to(q_bhsd.dtype)
601
+ _bg = audio_boost_gamma.to(q_bhsd.dtype)
602
+ _gated = _gv * _bsal.view(1, 1, 1, -1).to(q_bhsd.dtype) + (1.0 - _gv) / _N_bk
603
+ audio_term = _bg * _gated
604
+ score_mod = (score_mod + audio_term) if score_mod is not None else audio_term
605
+
606
+ # Warn-once: Path B (audio-weighted K pooling) has no effect in the
607
+ # bias-rectification path. Taylor / Rectified methods use plain mean
608
+ # pooling for K (so autograd through pooling is correct). Path A
609
+ # boost (audio_boost_gamma) still applies.
610
+ if self.enable_audio_weighted_pooling and not SelfAttention._path_b_in_correction_warned:
611
+ print(
612
+ "[BSA] enable_audio_weighted_pooling (Path B) is silently ignored "
613
+ "when enable_taylor_sparse_attn / enable_rectified_sparse_attn is "
614
+ "active. Path A boost still applies."
615
+ )
616
+ SelfAttention._path_b_in_correction_warned = True
617
+
618
+ if score_mod is not None:
619
+ score_mod = (1.0 + score_mod).contiguous()
620
+
621
+ _sm = 1.0 / (D ** 0.5)
622
+ _sp = bsa_kwargs.get('sparsity', 0.9375)
623
+ _cdf = bsa_kwargs.get('cdf_threshold', None)
624
+ if self.enable_taylor_sparse_attn:
625
+ out = bsa_taylor_sparse_attn(
626
+ q_ro, k_ro, v_ro,
627
+ chunk_size_q=_tq * _hq * _wq,
628
+ chunk_size_k=_tk * _hk * _wk,
629
+ sparsity=_sp, cdf_threshold=_cdf, sm_scale=_sm,
630
+ alpha_f=self.bsa_params.get('taylor_alpha_f', 0.5),
631
+ kv_valid_mask=kv_vm_ro, score_modifier=score_mod,
632
+ )
633
+ else:
634
+ out = bsa_rectified_sparse_attn(
635
+ q_ro, k_ro, v_ro,
636
+ chunk_size_q=_tq * _hq * _wq,
637
+ chunk_size_k=_tk * _hk * _wk,
638
+ sparsity=_sp, cdf_threshold=_cdf, sm_scale=_sm,
639
+ kv_valid_mask=kv_vm_ro, score_modifier=score_mod,
640
+ )
641
+ out = rearrange_3d_block_to_THW(out, _Ntq, _Nhq, _Nwq, _tq, _hq, _wq, D)
642
+
643
+ else:
644
+ # ---------- Original dispatch paths (completely unchanged) ----------
645
+ if use_variance and not use_audio:
646
+ from hymm.models.modules.block_sparse_attention import flash_attn_bsa_3d_variance_guided
647
+ out = flash_attn_bsa_3d_variance_guided(
648
+ q_bhsd, k_bhsd, v_bhsd, grid_padded, grid_padded,
649
+ variance_boost_gamma=self.bsa_params.get('variance_boost_gamma', 1.0),
650
+ valid_mask=valid_mask, **bsa_kwargs,
651
+ )
652
+ elif use_audio and not use_variance:
653
+ out = flash_attn_bsa_3d_audio_guided(
654
+ q_bhsd, k_bhsd, v_bhsd, grid_padded, grid_padded,
655
+ audio_token_norms=audio_norms_padded,
656
+ audio_gate_value=audio_gate,
657
+ audio_boost_gamma=audio_boost_gamma,
658
+ valid_mask=valid_mask,
659
+ audio_weighted_lambda=audio_weighted_lambda,
660
+ **bsa_kwargs,
661
+ )
662
+ elif use_audio and use_variance:
663
+ # Pass variance_boost_gamma so ρ is computed INSIDE on the already-
664
+ # rearranged V — saves one large `rearrange_THW_to_3d_block(v)` per
665
+ # layer per step (significant on 2K where V is ~600K tokens).
666
+ # Mathematically identical to the explicit-score_modifier route.
667
+ out = flash_attn_bsa_3d_audio_guided(
668
+ q_bhsd, k_bhsd, v_bhsd, grid_padded, grid_padded,
669
+ audio_token_norms=audio_norms_padded,
670
+ audio_gate_value=audio_gate,
671
+ audio_boost_gamma=audio_boost_gamma,
672
+ valid_mask=valid_mask,
673
+ audio_weighted_lambda=audio_weighted_lambda,
674
+ variance_boost_gamma=self.bsa_params.get('variance_boost_gamma', 1.0),
675
+ **bsa_kwargs,
676
+ )
677
+ else:
678
+ out = flash_attn_bsa_3d(
679
+ q_bhsd, k_bhsd, v_bhsd, grid_padded, grid_padded,
680
+ valid_mask=valid_mask, **bsa_kwargs,
681
+ )
682
+
683
+ if need_pad:
684
+ out_3d = out.view(B, n_heads, T_p, H_p, W_p, D)
685
+ out = out_3d[:, :, :T, :H, :W, :].contiguous().reshape(B, n_heads, T * H * W, D)
686
+
687
+ return out
688
+
689
+ def forward(self, x, freqs, grid_size=None, a2v_bridge_residual=None, timestep_ratio=None):
690
+ """
691
+ Args:
692
+ a2v_bridge_residual: [B, S, D] — A→V bridge cross-attention output (before scale).
693
+ Used for audio-guided BSA scoring. Only effective when
694
+ enable_audio_guidance=True and BSA is active.
695
+ timestep_ratio: scalar in [0,1] — normalized timestep (0=clean, 1=noise).
696
+ Used for Timestep Reliability Gate. Only effective when
697
+ enable_timestep_reliability_gate=True.
698
+ """
699
+ q = self.norm_q(self.q(x))
700
+ k = self.norm_k(self.k(x))
701
+ v = self.v(x)
702
+ if isinstance(freqs, DTensor):
703
+ freqs = freqs.to_local()
704
+ q = rope_apply_head_dim(q, freqs, self.head_dim)
705
+ k = rope_apply_head_dim(k, freqs, self.head_dim)
706
+
707
+ use_bsa, fallback_reason = self._check_bsa(grid_size)
708
+
709
+ if self.enable_bsa and not use_bsa and not SelfAttention._bsa_fallback_logged:
710
+ print(f"[BSA] Falling back to full attention: {fallback_reason} "
711
+ f"(grid_size={grid_size})")
712
+ SelfAttention._bsa_fallback_logged = True
713
+
714
+ # Compute per-token audio norms for audio guidance (before SP reshuffling).
715
+ # Also needed by the dynamic-block path for the audio-directional gradient.
716
+ audio_token_norms = None
717
+ # Layer-adaptive dynamic block (Section 8.2): only effective with a dynamic method.
718
+ la_on = (self.enable_layer_adaptive_dynamic_block
719
+ and (self.enable_ivpq_dynamic_block or self.enable_penalty_dynamic_block)
720
+ and self._la_layer_idx is not None and self._la_split is not None)
721
+ la_is_deep = la_on and (self._la_layer_idx >= self._la_split)
722
+
723
+ use_any_audio = use_bsa and (
724
+ self.enable_audio_guidance or self.enable_audio_weighted_pooling
725
+ or self.enable_ivpq_dynamic_block or self.enable_penalty_dynamic_block
726
+ )
727
+ # Decide whether THIS layer needs to compute audio norms (a per-layer
728
+ # all-gather under SP). Under layer-adaptive mode we avoid wasted work:
729
+ # * shallow layers run FIXED BSA (ignore audio) → only the designated
730
+ # cache-source layer computes audio (to seed the shallow→deep cache);
731
+ # * deep layers compute their own only if they actually have a bridge
732
+ # (otherwise audio stays None and they reuse the cache below).
733
+ if la_on:
734
+ need_audio = True if la_is_deep else self._la_is_cache_source
735
+ else:
736
+ need_audio = use_any_audio
737
+ sp_full_len = sp_seq_ctx.get(self.sp_stream)
738
+
739
+ if need_audio and a2v_bridge_residual is not None:
740
+ # a2v_bridge_residual: [B, S_local, D] (SP-split) or [B, S, D] (no SP)
741
+ # Compute per-token L2 norm: ||Δh_v[m]||_2
742
+ with torch.no_grad():
743
+ local_norms = a2v_bridge_residual.norm(dim=-1) # [B, S_local]
744
+
745
+ if get_sequence_parallel_state():
746
+ # All-gather norms across SP ranks to get full sequence
747
+ local_norms_4d = local_norms.unsqueeze(-1) # [B, S_local, 1]
748
+ full_norms = all_gather(local_norms_4d, dim=1, group=nccl_info.sp_group,
749
+ full_seq_len=sp_full_len)
750
+ audio_token_norms = full_norms.squeeze(-1)[0] # [S]
751
+ else:
752
+ audio_token_norms = local_norms[0] # [S]
753
+
754
+ # Seed the shallow→deep cache (detached, full-seq THW order, SP-consistent).
755
+ # `audio_token_norms` is computed under no_grad above (no graph retained).
756
+ if la_on and self._la_is_cache_source and self._la_audio_cache is not None:
757
+ self._la_audio_cache['norms'] = audio_token_norms.contiguous()
758
+
759
+ # Deep layers under layer-adaptive: reuse cached shallow audio when this layer
760
+ # has no bridge of its own (own audio_token_norms is None for those layers).
761
+ if la_is_deep and audio_token_norms is None and self._la_audio_cache is not None:
762
+ audio_token_norms = self._la_audio_cache.get('norms', None)
763
+
764
+ if get_sequence_parallel_state():
765
+ sp_size = nccl_info.sp_size
766
+ sp_rank = nccl_info.rank_within_spgroup
767
+ # Ulysses requires num_heads % sp_size == 0. When it is not (e.g. audio
768
+ # DiT = 12 heads with sp_size=8), pad the head dim with ZERO heads to the
769
+ # next multiple. Padded heads are sliced off before any attention compute
770
+ # and truncated after the inverse all_to_all, so they never enter the
771
+ # BSA Triton / flash kernel and never affect the real heads' fwd/bwd.
772
+ # When num_heads is already divisible (e.g. video = 40 heads), padded_heads
773
+ # == num_heads and this path is bit-identical to the original code.
774
+ padded_heads = sp_pad_num_heads(self.num_heads, sp_size)
775
+
776
+ q, k, v = [rearrange(t, "b s (n d) -> b s n d", n=self.num_heads) for t in (q, k, v)]
777
+ if padded_heads != self.num_heads:
778
+ q = sp_pad_heads(q, padded_heads)
779
+ k = sp_pad_heads(k, padded_heads)
780
+ v = sp_pad_heads(v, padded_heads)
781
+
782
+ q = all_to_all_4D(q, nccl_info.sp_group, scatter_dim=2, gather_dim=1,
783
+ full_seq_len=sp_full_len)
784
+ k = all_to_all_4D(k, nccl_info.sp_group, scatter_dim=2, gather_dim=1,
785
+ full_seq_len=sp_full_len)
786
+ v = all_to_all_4D(v, nccl_info.sp_group, scatter_dim=2, gather_dim=1,
787
+ full_seq_len=sp_full_len)
788
+
789
+ hpr = q.shape[2] # heads-per-rank after scatter (= padded_heads // sp_size)
790
+ local_heads = sp_num_local_real_heads(self.num_heads, padded_heads, sp_size, sp_rank)
791
+ if local_heads > 0:
792
+ if local_heads != hpr:
793
+ # Mixed rank: keep only the real-head prefix. The slice stays
794
+ # connected to the all_to_all output, so its backward yields grad
795
+ # for all hpr local heads (zeros for the padded tail) and the
796
+ # q/k/v all_to_all collectives still fire here.
797
+ q = q[:, :, :local_heads, :]
798
+ k = k[:, :, :local_heads, :]
799
+ v = v[:, :, :local_heads, :]
800
+ if use_bsa:
801
+ q_bsa = q.permute(0, 2, 1, 3).contiguous()
802
+ k_bsa = k.permute(0, 2, 1, 3).contiguous()
803
+ v_bsa = v.permute(0, 2, 1, 3).contiguous()
804
+ x_bsa = self._run_bsa(q_bsa, k_bsa, v_bsa, grid_size,
805
+ audio_token_norms=audio_token_norms,
806
+ timestep_ratio=timestep_ratio)
807
+ x = rearrange(x_bsa, "b n s d -> b s (n d)")
808
+ else:
809
+ x = flash_attention(q.flatten(2), k.flatten(2), v.flatten(2), num_heads=local_heads)
810
+ x = rearrange(x, "b s (n d) -> b s n d", n=local_heads)
811
+ if x.shape[2] != hpr:
812
+ # Re-pad to hpr so the inverse all_to_all (head gather) is balanced.
813
+ x = sp_pad_heads(x, hpr)
814
+ else:
815
+ # Padding-only rank: this rank holds NO real heads, so NO attention
816
+ # kernel is run (padding never enters the BSA Triton / flash kernel).
817
+ # CRITICAL: x must stay CONNECTED to q/k/v (value forced to zero) so the
818
+ # backward all_to_all for q/k/v still fires on this rank. This keeps the
819
+ # NCCL collectives symmetric across ranks (no deadlock) AND lets this
820
+ # rank's real tokens receive their correct q/k/v gradients, which are
821
+ # computed on the head-owner ranks and routed back through the all_to_all.
822
+ # `* 0.0` guarantees zero contribution to forward/backward of real heads.
823
+ x = (q + k + v) * 0.0
824
+ x = all_to_all_4D(x, nccl_info.sp_group, scatter_dim=1, gather_dim=2,
825
+ full_seq_len=sp_full_len)
826
+ if padded_heads != self.num_heads:
827
+ x = x[:, :, :self.num_heads, :]
828
+ x = x.flatten(2)
829
+ else:
830
+ if use_bsa:
831
+ q_bsa = rearrange(q, "b s (n d) -> b n s d", n=self.num_heads).contiguous()
832
+ k_bsa = rearrange(k, "b s (n d) -> b n s d", n=self.num_heads).contiguous()
833
+ v_bsa = rearrange(v, "b s (n d) -> b n s d", n=self.num_heads).contiguous()
834
+ x_bsa = self._run_bsa(q_bsa, k_bsa, v_bsa, grid_size,
835
+ audio_token_norms=audio_token_norms,
836
+ timestep_ratio=timestep_ratio)
837
+ x = rearrange(x_bsa, "b n s d -> b s (n d)")
838
+ else:
839
+ x = self.attn(q, k, v)
840
+
841
+ return self.o(x)
842
+
843
+
844
+ class USPAttention(nn.Module):
845
+ def __init__(self, num_heads: int, attn_type=AttnType.FA):
846
+ super().__init__()
847
+ if not LONG_CONTEXT_ATTN_AVAILABLE or LongContextAttention is None:
848
+ raise RuntimeError(
849
+ "USPAttention requires `yunchang` (LongContextAttention). "
850
+ "Please install/enable yunchang or avoid using USPAttention."
851
+ )
852
+ self.num_heads = num_heads
853
+ self.attn = LongContextAttention(ring_impl_type="basic", attn_type=attn_type)
854
+
855
+ def forward(self, q, k, v):
856
+ q = rearrange(q, "b s (n d) -> b s n d", n=self.num_heads)
857
+ k = rearrange(k, "b s (n d) -> b s n d", n=self.num_heads)
858
+ v = rearrange(v, "b s (n d) -> b s n d", n=self.num_heads)
859
+ x = self.attn(q, k, v)
860
+ return rearrange(x, "b s n d -> b s (n d)")
861
+
862
+
863
+ class CrossAttention(nn.Module):
864
+ def __init__(self, dim: int, num_heads: int, eps: float = 1e-6, has_image_input: bool = False):
865
+ super().__init__()
866
+ self.dim = dim
867
+ self.num_heads = num_heads
868
+ self.head_dim = dim // num_heads
869
+
870
+ self.q = nn.Linear(dim, dim)
871
+ self.k = nn.Linear(dim, dim)
872
+ self.v = nn.Linear(dim, dim)
873
+ self.o = nn.Linear(dim, dim)
874
+ self.norm_q = RMSNorm(dim, eps=eps)
875
+ self.norm_k = RMSNorm(dim, eps=eps)
876
+ self.has_image_input = has_image_input
877
+ if has_image_input:
878
+ self.k_img = nn.Linear(dim, dim)
879
+ self.v_img = nn.Linear(dim, dim)
880
+ self.norm_k_img = RMSNorm(dim, eps=eps)
881
+
882
+ self.attn = AttentionModule(self.num_heads)
883
+
884
+ # Query stream for SP; the text context is replicated, not sharded.
885
+ self.sp_stream = "visual"
886
+
887
+ def forward(self, x: torch.Tensor, y: torch.Tensor):
888
+ if self.has_image_input:
889
+ img = y[:, :257]
890
+ ctx = y[:, 257:]
891
+ else:
892
+ ctx = y
893
+ q = self.norm_q(self.q(x))
894
+ k = self.norm_k(self.k(ctx))
895
+ v = self.v(ctx)
896
+
897
+ sp_enabled = get_sequence_parallel_state()
898
+
899
+ if sp_enabled:
900
+ sp_size = nccl_info.sp_size
901
+ sp_rank = nccl_info.rank_within_spgroup
902
+ # Same Ulysses head-padding contract as SelfAttention: pad to a multiple
903
+ # of sp_size, slice real heads before compute, truncate after. The k/v
904
+ # context is NOT all_to_all'd (replicated); it is padded then sliced by
905
+ # head_start so each rank takes the same global heads as its q.
906
+ padded_heads = sp_pad_num_heads(self.num_heads, sp_size)
907
+
908
+ q_4d = rearrange(q, "b s (n d) -> b s n d", n=self.num_heads)
909
+ k_4d = rearrange(k, "b s (n d) -> b s n d", n=self.num_heads)
910
+ v_4d = rearrange(v, "b s (n d) -> b s n d", n=self.num_heads)
911
+ if padded_heads != self.num_heads:
912
+ q_4d = sp_pad_heads(q_4d, padded_heads)
913
+ k_4d = sp_pad_heads(k_4d, padded_heads)
914
+ v_4d = sp_pad_heads(v_4d, padded_heads)
915
+
916
+ sp_full_len = sp_seq_ctx.get(self.sp_stream)
917
+ q_4d = all_to_all_4D(q_4d, nccl_info.sp_group, scatter_dim=2, gather_dim=1,
918
+ full_seq_len=sp_full_len)
919
+ hpr = q_4d.shape[2]
920
+ head_start = sp_rank * hpr
921
+ k_4d = k_4d[:, :, head_start:head_start + hpr, :]
922
+ v_4d = v_4d[:, :, head_start:head_start + hpr, :]
923
+
924
+ local_heads = sp_num_local_real_heads(self.num_heads, padded_heads, sp_size, sp_rank)
925
+ if local_heads > 0:
926
+ if local_heads != hpr:
927
+ q_4d = q_4d[:, :, :local_heads, :]
928
+ k_4d = k_4d[:, :, :local_heads, :]
929
+ v_4d = v_4d[:, :, :local_heads, :]
930
+ out = flash_attention(q_4d.flatten(2), k_4d.flatten(2), v_4d.flatten(2), num_heads=local_heads)
931
+ out = rearrange(out, "b s (n d) -> b s n d", n=local_heads)
932
+ if self.has_image_input:
933
+ k_img = self.norm_k_img(self.k_img(img))
934
+ v_img = self.v_img(img)
935
+ k_img_4d = rearrange(k_img, "b s (n d) -> b s n d", n=self.num_heads)
936
+ v_img_4d = rearrange(v_img, "b s (n d) -> b s n d", n=self.num_heads)
937
+ if padded_heads != self.num_heads:
938
+ k_img_4d = sp_pad_heads(k_img_4d, padded_heads)
939
+ v_img_4d = sp_pad_heads(v_img_4d, padded_heads)
940
+ k_img_4d = k_img_4d[:, :, head_start:head_start + local_heads, :]
941
+ v_img_4d = v_img_4d[:, :, head_start:head_start + local_heads, :]
942
+ img_out = flash_attention(q_4d.flatten(2), k_img_4d.flatten(2), v_img_4d.flatten(2), num_heads=local_heads)
943
+ img_out = rearrange(img_out, "b s (n d) -> b s n d", n=local_heads)
944
+ out = out + img_out
945
+ if out.shape[2] != hpr:
946
+ out = sp_pad_heads(out, hpr)
947
+ else:
948
+ # Padding-only rank: no kernel. Keep out CONNECTED to q_4d (the only
949
+ # all_to_all'd tensor here; k/v are local context slices with no
950
+ # collective) so q's backward all_to_all fires on this rank too.
951
+ # `* 0.0` forces zero so it never affects real heads' fwd/bwd.
952
+ out = q_4d * 0.0
953
+ out = all_to_all_4D(out, nccl_info.sp_group, scatter_dim=1, gather_dim=2,
954
+ full_seq_len=sp_full_len)
955
+ if padded_heads != self.num_heads:
956
+ out = out[:, :, :self.num_heads, :]
957
+ x = out.flatten(2)
958
+ else:
959
+ x = self.attn(q, k, v)
960
+ if self.has_image_input:
961
+ k_img = self.norm_k_img(self.k_img(img))
962
+ v_img = self.v_img(img)
963
+ y = flash_attention(q, k_img, v_img, num_heads=self.num_heads)
964
+ x = x + y
965
+
966
+ return self.o(x)
967
+
968
+
969
+ class GateModule(nn.Module):
970
+ def __init__(self,):
971
+ super().__init__()
972
+
973
+ def forward(self, x, gate, residual):
974
+ return x + gate * residual
975
+
976
+ class DiTBlock(nn.Module):
977
+ def __init__(self, has_image_input: bool, dim: int, num_heads: int, ffn_dim: int, eps: float = 1e-6,
978
+ enable_bsa: bool = False, bsa_params: dict = None,
979
+ enable_audio_guidance: bool = False,
980
+ enable_audio_concentration_gate: bool = False,
981
+ enable_variance_guidance: bool = False):
982
+ super().__init__()
983
+ self.dim = dim
984
+ self.num_heads = num_heads
985
+ self.ffn_dim = ffn_dim
986
+
987
+ self.self_attn = SelfAttention(
988
+ dim, num_heads, eps,
989
+ enable_bsa=enable_bsa, bsa_params=bsa_params,
990
+ enable_audio_guidance=enable_audio_guidance,
991
+ enable_audio_concentration_gate=enable_audio_concentration_gate,
992
+ enable_variance_guidance=enable_variance_guidance,
993
+ )
994
+ self.cross_attn = CrossAttention(
995
+ dim, num_heads, eps, has_image_input=has_image_input)
996
+ self.norm1 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
997
+ self.norm2 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
998
+ self.norm3 = nn.LayerNorm(dim, eps=eps)
999
+ self.ffn = nn.Sequential(nn.Linear(dim, ffn_dim), nn.GELU(
1000
+ approximate='tanh'), nn.Linear(ffn_dim, dim))
1001
+ self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
1002
+ self.gate = GateModule()
1003
+
1004
+ def forward(self, x, context, t_mod, freqs, grid_size=None, a2v_bridge_residual=None, timestep_ratio=None):
1005
+ has_seq = len(t_mod.shape) == 4
1006
+ chunk_dim = 2 if has_seq else 1
1007
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
1008
+ self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod).chunk(6, dim=chunk_dim)
1009
+ if has_seq:
1010
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
1011
+ shift_msa.squeeze(2), scale_msa.squeeze(2), gate_msa.squeeze(2),
1012
+ shift_mlp.squeeze(2), scale_mlp.squeeze(2), gate_mlp.squeeze(2),
1013
+ )
1014
+ input_x = modulate(self.norm1(x), shift_msa, scale_msa)
1015
+ x = self.gate(x, gate_msa, self.self_attn(
1016
+ input_x, freqs, grid_size=grid_size,
1017
+ a2v_bridge_residual=a2v_bridge_residual,
1018
+ timestep_ratio=timestep_ratio,
1019
+ ))
1020
+ x = x + self.cross_attn(self.norm3(x), context)
1021
+ input_x = modulate(self.norm2(x), shift_mlp, scale_mlp)
1022
+ x = self.gate(x, gate_mlp, self.ffn(input_x))
1023
+ return x
1024
+
1025
+
1026
+ class MLP(torch.nn.Module):
1027
+ def __init__(self, in_dim, out_dim, has_pos_emb=False):
1028
+ super().__init__()
1029
+ self.proj = torch.nn.Sequential(
1030
+ nn.LayerNorm(in_dim),
1031
+ nn.Linear(in_dim, in_dim),
1032
+ nn.GELU(),
1033
+ nn.Linear(in_dim, out_dim),
1034
+ nn.LayerNorm(out_dim)
1035
+ )
1036
+ self.has_pos_emb = has_pos_emb
1037
+ if has_pos_emb:
1038
+ self.emb_pos = torch.nn.Parameter(torch.zeros((1, 514, 1280)))
1039
+
1040
+ def forward(self, x):
1041
+ if self.has_pos_emb:
1042
+ x = x + self.emb_pos.to(dtype=x.dtype, device=x.device)
1043
+ return self.proj(x)
1044
+
1045
+
1046
+ class Head(nn.Module):
1047
+ def __init__(self, dim: int, out_dim: int, patch_size: Tuple[int, int, int], eps: float):
1048
+ super().__init__()
1049
+ self.dim = dim
1050
+ self.patch_size = patch_size
1051
+ self.norm = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
1052
+ self.head = nn.Linear(dim, out_dim * math.prod(patch_size))
1053
+ self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
1054
+
1055
+ def forward(self, x, t_mod):
1056
+ if len(t_mod.shape) == 3:
1057
+ shift, scale = (self.modulation.unsqueeze(0).to(dtype=t_mod.dtype, device=t_mod.device) + t_mod.unsqueeze(2)).chunk(2, dim=2)
1058
+ x = (self.head(self.norm(x) * (1 + scale.squeeze(2)) + shift.squeeze(2)))
1059
+ else:
1060
+ shift, scale = (self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod).chunk(2, dim=1)
1061
+ x = (self.head(self.norm(x) * (1 + scale) + shift))
1062
+ return x
1063
+
1064
+
1065
+ class WanModel(ModelMixin, ConfigMixin):
1066
+ _repeated_blocks = ("DiTBlock",)
1067
+
1068
+ @register_to_config
1069
+ def __init__(
1070
+ self,
1071
+ dim: int,
1072
+ in_dim: int,
1073
+ ffn_dim: int,
1074
+ out_dim: int,
1075
+ text_dim: int,
1076
+ freq_dim: int,
1077
+ eps: float,
1078
+ patch_size: Tuple[int, int, int],
1079
+ num_heads: int,
1080
+ num_layers: int,
1081
+ has_image_input: bool,
1082
+ has_image_pos_emb: bool = False,
1083
+ has_ref_conv: bool = False,
1084
+ add_control_adapter: bool = False,
1085
+ in_dim_control_adapter: int = 24,
1086
+ seperated_timestep: bool = False,
1087
+ require_vae_embedding: bool = True,
1088
+ require_clip_embedding: bool = True,
1089
+ fuse_vae_embedding_in_latents: bool = False,
1090
+ enable_bsa: bool = False,
1091
+ bsa_params: dict = None,
1092
+ enable_variance_guidance: bool = False,
1093
+ ):
1094
+ super().__init__()
1095
+ self.dim = dim
1096
+ self.freq_dim = freq_dim
1097
+ self.has_image_input = has_image_input
1098
+ self.patch_size = patch_size
1099
+ self.seperated_timestep = seperated_timestep
1100
+ self.require_vae_embedding = require_vae_embedding
1101
+ self.require_clip_embedding = require_clip_embedding
1102
+ self.fuse_vae_embedding_in_latents = fuse_vae_embedding_in_latents
1103
+ self.enable_bsa = enable_bsa
1104
+ self.bsa_params = bsa_params
1105
+ self.enable_variance_guidance = enable_variance_guidance
1106
+
1107
+ self.patch_embedding = nn.Conv3d(
1108
+ in_dim, dim, kernel_size=patch_size, stride=patch_size)
1109
+ self.text_embedding = nn.Sequential(
1110
+ nn.Linear(text_dim, dim),
1111
+ nn.GELU(approximate='tanh'),
1112
+ nn.Linear(dim, dim)
1113
+ )
1114
+ self.time_embedding = nn.Sequential(
1115
+ nn.Linear(freq_dim, dim),
1116
+ nn.SiLU(),
1117
+ nn.Linear(dim, dim)
1118
+ )
1119
+ self.time_projection = nn.Sequential(
1120
+ nn.SiLU(), nn.Linear(dim, dim * 6))
1121
+ self.blocks = nn.ModuleList([
1122
+ DiTBlock(has_image_input, dim, num_heads, ffn_dim, eps,
1123
+ enable_bsa=enable_bsa, bsa_params=bsa_params,
1124
+ enable_audio_guidance=False,
1125
+ enable_audio_concentration_gate=False,
1126
+ enable_variance_guidance=enable_variance_guidance)
1127
+ for _ in range(num_layers)
1128
+ ])
1129
+ self.head = Head(dim, out_dim, patch_size, eps)
1130
+ head_dim = dim // num_heads
1131
+ self.freqs = precompute_freqs_cis_3d(head_dim)
1132
+
1133
+ if has_image_input:
1134
+ self.img_emb = MLP(1280, dim, has_pos_emb=has_image_pos_emb) # clip_feature_dim = 1280
1135
+ if has_ref_conv:
1136
+ self.ref_conv = nn.Conv2d(16, dim, kernel_size=(2, 2), stride=(2, 2))
1137
+ self.has_image_pos_emb = has_image_pos_emb
1138
+ self.has_ref_conv = has_ref_conv
1139
+ self.control_adapter = None
1140
+
1141
+ def patchify(self, x: torch.Tensor,control_camera_latents_input: torch.Tensor = None):
1142
+ # NOTE(dhyu): avoid slow_conv
1143
+ x = x.contiguous(memory_format=torch.channels_last_3d)
1144
+ x = self.patch_embedding(x)
1145
+ if self.control_adapter is not None and control_camera_latents_input is not None:
1146
+ y_camera = self.control_adapter(control_camera_latents_input)
1147
+ x = [u + v for u, v in zip(x, y_camera)]
1148
+ x = x[0].unsqueeze(0)
1149
+ grid_size = x.shape[2:]
1150
+ x = rearrange(x, 'b c f h w -> b (f h w) c').contiguous()
1151
+ return x, grid_size # x, grid_size: (f, h, w)
1152
+
1153
+ def unpatchify(self, x: torch.Tensor, grid_size: torch.Tensor):
1154
+ return rearrange(
1155
+ x, 'b (f h w) (x y z c) -> b c (f x) (h y) (w z)',
1156
+ f=grid_size[0], h=grid_size[1], w=grid_size[2],
1157
+ x=self.patch_size[0], y=self.patch_size[1], z=self.patch_size[2]
1158
+ )
1159
+
1160
+ def forward(self,
1161
+ x: torch.Tensor,
1162
+ timestep: torch.Tensor,
1163
+ context: torch.Tensor,
1164
+ clip_feature: Optional[torch.Tensor] = None,
1165
+ y: Optional[torch.Tensor] = None,
1166
+ use_gradient_checkpointing: bool = False,
1167
+ use_gradient_checkpointing_offload: bool = False,
1168
+ **kwargs,
1169
+ ):
1170
+ advance_dynamic_block_pass_id() # once per real forward (not on recompute)
1171
+ t = self.time_embedding(
1172
+ sinusoidal_embedding_1d(self.freq_dim, timestep))
1173
+ t_mod = self.time_projection(t).unflatten(1, (6, self.dim))
1174
+ context = self.text_embedding(context)
1175
+
1176
+ if self.has_image_input:
1177
+ x = torch.cat([x, y], dim=1) # (b, c_x + c_y, f, h, w)
1178
+ clip_embdding = self.img_emb(clip_feature)
1179
+ context = torch.cat([clip_embdding, context], dim=1)
1180
+
1181
+ x, (f, h, w) = self.patchify(x)
1182
+
1183
+ freqs = torch.cat([
1184
+ self.freqs[0][:f].view(f, 1, 1, -1).expand(f, h, w, -1),
1185
+ self.freqs[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
1186
+ self.freqs[2][:w].view(1, 1, w, -1).expand(f, h, w, -1)
1187
+ ], dim=-1).reshape(f * h * w, 1, -1).to(x.device)
1188
+
1189
+ visual_seq_len = f * h * w
1190
+ if get_sequence_parallel_state():
1191
+ sp_size = nccl_info.sp_size
1192
+ sp_rank = nccl_info.rank_within_spgroup
1193
+ assert_sp_splittable(visual_seq_len, sp_size, "visual sequence")
1194
+ sp_seq_ctx.set(visual=visual_seq_len)
1195
+ x = sp_split(x, sp_size, sp_rank, dim=1)
1196
+ freqs = sp_split(freqs, sp_size, sp_rank, dim=0)
1197
+
1198
+ def create_custom_forward(module):
1199
+ def custom_forward(*inputs):
1200
+ return module(*inputs)
1201
+ return custom_forward
1202
+
1203
+ grid_size = (f, h, w) if self.enable_bsa else None
1204
+
1205
+ for block in self.blocks:
1206
+ if self.training and use_gradient_checkpointing:
1207
+ if use_gradient_checkpointing_offload:
1208
+ with torch.autograd.graph.save_on_cpu():
1209
+ x = torch.utils.checkpoint.checkpoint(
1210
+ create_custom_forward(block),
1211
+ x, context, t_mod, freqs, grid_size,
1212
+ use_reentrant=False,
1213
+ )
1214
+ else:
1215
+ x = torch.utils.checkpoint.checkpoint(
1216
+ create_custom_forward(block),
1217
+ x, context, t_mod, freqs, grid_size,
1218
+ use_reentrant=False,
1219
+ )
1220
+ else:
1221
+ x = block(x, context, t_mod, freqs, grid_size=grid_size)
1222
+
1223
+ x = self.head(x, t)
1224
+ if get_sequence_parallel_state():
1225
+ x = all_gather(x, dim=1, group=nccl_info.sp_group,
1226
+ full_seq_len=visual_seq_len)
1227
+ x = self.unpatchify(x, (f, h, w))
1228
+ return x
1229
+
1230
+ def configure_bsa(self, enable_bsa: bool, bsa_params: dict = None):
1231
+ """Runtime configuration of Block Sparse Attention on all video self-attention blocks."""
1232
+ self.enable_bsa = enable_bsa
1233
+ self.bsa_params = bsa_params
1234
+ for block in self.blocks:
1235
+ block.self_attn.enable_bsa = enable_bsa
1236
+ block.self_attn.bsa_params = bsa_params or {}
1237
+
1238
+ def configure_audio_guidance(self, enable: bool, enable_concentration_gate: bool = False):
1239
+ """Runtime configuration of audio-guided BSA on all video self-attention blocks.
1240
+
1241
+ Audio guidance modulates block-level QK scores with audio saliency
1242
+ from the A→V bridge residual. Only effective when BSA is also enabled.
1243
+
1244
+ The boost strength γ (audio_boost_gamma) is read from bsa_params at runtime.
1245
+ Set bsa_params['audio_boost_gamma'] to control the modulation strength
1246
+ (default 1.0; 0.0 disables modulation).
1247
+
1248
+ Args:
1249
+ enable: whether to enable audio-guided score modulation.
1250
+ enable_concentration_gate: whether to enable Gate 2 (Audio Spatial
1251
+ Concentration Gate) which down-weights audio saliency when the
1252
+ bridge residual is spatially uniform (silence/ambient scenarios).
1253
+ """
1254
+ for block in self.blocks:
1255
+ sa = block.self_attn
1256
+ sa.enable_audio_guidance = enable
1257
+ sa.enable_audio_concentration_gate = enable_concentration_gate
1258
+
1259
+ def configure_variance_guidance(self, enable: bool):
1260
+ """Runtime configuration of channel-variance guidance on all video self-attention blocks.
1261
+
1262
+ Non-learnable, no gate. When enabled, block-level QK scores are boosted:
1263
+ score_{i,j} *= (1 + γ * ρ_j)
1264
+ where ρ_j is the V-block channel-variance density ∈ [0,1] (key/value side only).
1265
+ Query-awareness comes from P^QK being per-query (zero-gate effect):
1266
+ each query block has its own QK scores → different top-k after boost.
1267
+
1268
+ Completely independent from audio guidance — no shared code or state.
1269
+ Only effective when BSA (enable_bsa) is also enabled.
1270
+ Boost strength γ is read from bsa_params['variance_boost_gamma'] (default 1.0).
1271
+
1272
+ Args:
1273
+ enable: whether to enable channel-variance guidance.
1274
+ """
1275
+ self.enable_variance_guidance = enable
1276
+ for block in self.blocks:
1277
+ block.self_attn.enable_variance_guidance = enable
1278
+
1279
+ def configure_taylor_sparse_attn(self, enable: bool):
1280
+ """LIVEditor: selected (exact) + non-selected (Taylor) in one unified softmax.
1281
+ Weights sum to exactly 1. No R_n. Mutually exclusive with rectified."""
1282
+ for block in self.blocks:
1283
+ block.self_attn.enable_taylor_sparse_attn = enable
1284
+ if enable:
1285
+ block.self_attn.enable_rectified_sparse_attn = False
1286
+
1287
+ def configure_rectified_sparse_attn(self, enable: bool):
1288
+ """Rectified SpaAttn: o = R_n * o_spa + A_pool[nonsel] · V_pool.
1289
+ Mutually exclusive with taylor."""
1290
+ for block in self.blocks:
1291
+ block.self_attn.enable_rectified_sparse_attn = enable
1292
+ if enable:
1293
+ block.self_attn.enable_taylor_sparse_attn = False
1294
+
1295
+ def _disable_non_dynamic_bsa_features(self, sa):
1296
+ """Turn off every other BSA feature on a SelfAttention module so the
1297
+ dynamic-block path is fully isolated (Section 8 requirement)."""
1298
+ sa.enable_audio_guidance = False
1299
+ sa.enable_audio_concentration_gate = False
1300
+ sa.enable_timestep_reliability_gate = False
1301
+ sa.enable_audio_weighted_pooling = False
1302
+ sa.enable_variance_guidance = False
1303
+ sa.enable_taylor_sparse_attn = False
1304
+ sa.enable_rectified_sparse_attn = False
1305
+
1306
+ def configure_ivpq_dynamic_block(self, enable: bool):
1307
+ """IVPQ anisotropic dynamic block shape (Section 8.5). Video self-attn only.
1308
+ Mutually exclusive with penalty-matching AND with all other BSA features."""
1309
+ for block in self.blocks:
1310
+ sa = block.self_attn
1311
+ sa.enable_ivpq_dynamic_block = enable
1312
+ if enable:
1313
+ sa.enable_penalty_dynamic_block = False
1314
+ self._disable_non_dynamic_bsa_features(sa)
1315
+
1316
+ def configure_penalty_dynamic_block(self, enable: bool):
1317
+ """Penalty-Matching anisotropic dynamic block shape (Section 8.6). Video
1318
+ self-attn only. Mutually exclusive with IVPQ AND all other BSA features."""
1319
+ for block in self.blocks:
1320
+ sa = block.self_attn
1321
+ sa.enable_penalty_dynamic_block = enable
1322
+ if enable:
1323
+ sa.enable_ivpq_dynamic_block = False
1324
+ self._disable_non_dynamic_bsa_features(sa)
hymm/utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
hymm/utils/communications.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from typing import Any, List, Optional, Tuple
3
+
4
+ import torch
5
+ import torch.distributed as dist
6
+ from torch.nn import functional as F
7
+
8
+
9
+
10
+
11
+ def broadcast(input_: torch.Tensor, group: dist.ProcessGroup):
12
+ src = dist.get_global_rank(group, 0)
13
+ dist.broadcast(input_, src=src, group=group)
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Shard bookkeeping
18
+ #
19
+ # Sequences are split across SP ranks with ``torch.chunk`` semantics: every rank
20
+ # gets ``ceil(S / sp)`` tokens except the last, which gets the remainder. The
21
+ # collectives below need those sizes, but a rank cannot infer the global length
22
+ # S from its own shard. Rather than negotiating it at runtime with a
23
+ # ``dist.all_gather_object`` on every call (which costs a pickle round-trip plus
24
+ # a device sync ~800 times per training step), the towers publish S through
25
+ # ``sp_seq_ctx`` and pass it down as ``full_seq_len``.
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def sp_chunk_sizes(total: int, world_size: int) -> List[int]:
29
+ """Shard sizes produced by ``torch.chunk(x, world_size)`` along one dim."""
30
+ chunk = (total + world_size - 1) // world_size
31
+ return [min(chunk, max(total - i * chunk, 0)) for i in range(world_size)]
32
+
33
+
34
+ def assert_sp_splittable(total: int, world_size: int, name: str = "sequence"):
35
+ """``torch.chunk`` silently returns fewer than ``world_size`` chunks when the
36
+ sequence is very short, which would make high SP ranks index out of range."""
37
+ if world_size <= 1:
38
+ return
39
+ sizes = sp_chunk_sizes(total, world_size)
40
+ assert sizes[-1] > 0, (
41
+ f"{name} length {total} is too short for sp_size={world_size}: "
42
+ f"the trailing SP rank(s) would receive zero tokens. Reduce sp_size or "
43
+ f"increase the resolution / duration bucket."
44
+ )
45
+
46
+
47
+ def sp_split(x: torch.Tensor, world_size: int, rank: int, dim: int) -> torch.Tensor:
48
+ """``torch.chunk``-compatible shard selection that never indexes past the end."""
49
+ if world_size <= 1:
50
+ return x
51
+ sizes = sp_chunk_sizes(x.shape[dim], world_size)
52
+ return torch.split(x, sizes, dim=dim)[rank]
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Ulysses head padding (for num_heads not divisible by sp_size, e.g. audio
57
+ # DiT = 12 heads with sp_size = 8).
58
+ #
59
+ # `all_to_all_4D` requires `num_heads % sp_size == 0` because it scatters the
60
+ # head dim evenly across SP ranks. These helpers let a module pad its head dim
61
+ # with ZERO heads up to the next multiple of sp_size purely at runtime, with a
62
+ # strict isolation contract enforced by the call sites:
63
+ #
64
+ # 1. Padding is appended (never weight-level) -> pretrained checkpoints load
65
+ # unchanged.
66
+ # 2. After the head-scatter all_to_all, each rank holds the CONTIGUOUS global
67
+ # head block [r*hpr, (r+1)*hpr); real heads are the prefix [0, num_heads),
68
+ # so the real heads on a rank are always its FIRST `local_real` local heads.
69
+ # Call sites slice those off BEFORE any attention compute, so padded (zero)
70
+ # heads NEVER enter the BSA Triton / flash kernels and cannot produce NaNs
71
+ # or perturb the real heads.
72
+ # 3. Ranks that hold only padding heads (local_real == 0) skip the kernel
73
+ # entirely and emit a zero-width head slice; they still join the all_to_all
74
+ # collectives so distributed/FSDP execution stays in lock-step.
75
+ # 4. After the inverse all_to_all the head dim is truncated back to num_heads,
76
+ # so forward and backward for the real heads are bit-identical to the
77
+ # unpadded computation (padding contributes exactly zero gradient).
78
+ # ---------------------------------------------------------------------------
79
+
80
+ def sp_pad_num_heads(num_heads: int, sp_size: int) -> int:
81
+ """Smallest multiple of ``sp_size`` that is >= ``num_heads``."""
82
+ remainder = num_heads % sp_size
83
+ if remainder == 0:
84
+ return num_heads
85
+ return num_heads + (sp_size - remainder)
86
+
87
+
88
+ def sp_pad_heads(x: torch.Tensor, padded_heads: int, dim: int = 2) -> torch.Tensor:
89
+ """Append zero-valued heads along ``dim`` so the head count becomes
90
+ ``padded_heads``. Autograd-safe: gradients routed to the appended heads are
91
+ discarded by the caller's later truncation, leaving the real heads' grads
92
+ untouched. Returns ``x`` unchanged when no padding is needed."""
93
+ cur = x.shape[dim]
94
+ if cur >= padded_heads:
95
+ return x
96
+ pad_shape = list(x.shape)
97
+ pad_shape[dim] = padded_heads - cur
98
+ pad = x.new_zeros(pad_shape)
99
+ return torch.cat([x, pad], dim=dim)
100
+
101
+
102
+ def sp_num_local_real_heads(num_real_heads: int, padded_heads: int,
103
+ sp_size: int, sp_rank: int) -> int:
104
+ """Number of this rank's local heads that are REAL after ``all_to_all_4D``
105
+ evenly distributes ``padded_heads``. Real heads are the global prefix
106
+ ``[0, num_real_heads)`` and rank ``r`` owns the contiguous global block
107
+ ``[r*hpr, (r+1)*hpr)`` (hpr = padded_heads // sp_size), so the real heads are
108
+ always this rank's FIRST ``k`` local heads."""
109
+ hpr = padded_heads // sp_size
110
+ return max(0, min(num_real_heads - sp_rank * hpr, hpr))
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Ulysses all-to-all
115
+ # ---------------------------------------------------------------------------
116
+
117
+ _UNKNOWN_SEQLEN_WARNED = False
118
+
119
+
120
+ def _negotiate_seq_lens(local_len: int, group) -> List[int]:
121
+ global _UNKNOWN_SEQLEN_WARNED
122
+ if not _UNKNOWN_SEQLEN_WARNED:
123
+ print(
124
+ "[SP] all_to_all_4D called without full_seq_len; falling back to a "
125
+ "per-call all_gather_object. This is correct but slow -- publish the "
126
+ "sequence length through hymm.utils.parallel_states.sp_seq_ctx."
127
+ )
128
+ _UNKNOWN_SEQLEN_WARNED = True
129
+ seq_lens = [None] * dist.get_world_size(group)
130
+ dist.all_gather_object(seq_lens, local_len, group)
131
+ return seq_lens
132
+
133
+
134
+ def _all_to_all_4D(
135
+ input: torch.Tensor,
136
+ scatter_idx: int = 2,
137
+ gather_idx: int = 1,
138
+ group=None,
139
+ full_seq_len: Optional[int] = None,
140
+ ) -> torch.Tensor:
141
+ """all-to-all for QKV.
142
+
143
+ Args:
144
+ input: a 4D tensor sharded along the scatter dim.
145
+ scatter_idx / gather_idx: 2/1 scatters heads and gathers sequence,
146
+ 1/2 does the inverse.
147
+ full_seq_len: global (unsharded) sequence length. Required for the 2/1
148
+ direction, where it is the only way to know how much padding the
149
+ trailing shard carries.
150
+ """
151
+ assert (
152
+ input.dim() == 4
153
+ ), f"input must be 4D tensor, got {input.dim()} and shape {input.shape}"
154
+
155
+ seq_world_size = dist.get_world_size(group)
156
+
157
+ if scatter_idx == 2 and gather_idx == 1:
158
+ # (bs, seqlen/P, hc, hs) -> (bs, seqlen, hc/P, hs)
159
+ if full_seq_len is not None:
160
+ sizes = sp_chunk_sizes(int(full_seq_len), seq_world_size)
161
+ gap = sizes[0] - sizes[-1]
162
+ else:
163
+ seq_lens = _negotiate_seq_lens(input.shape[1], group)
164
+ gap = seq_lens[0] - seq_lens[-1] if seq_lens[-1] != seq_lens[0] else 0
165
+ assert gap >= 0, "only the trailing SP rank may hold a shorter shard"
166
+
167
+ if gap > 0 and dist.get_group_rank(group, dist.get_rank()) == seq_world_size - 1:
168
+ input = F.pad(input, (0, 0, 0, 0, 0, gap))
169
+
170
+ bs, shard_seqlen, hc, hs = input.shape
171
+ seqlen = shard_seqlen * seq_world_size
172
+ assert hc % seq_world_size == 0, (
173
+ f'Invalid Head size: {hc}, which should be divisible by spsize {seq_world_size}'
174
+ )
175
+ shard_hc = hc // seq_world_size
176
+
177
+ # transpose groups of heads with the seq-len parallel dimension, so that we can scatter them!
178
+ # (bs, seqlen/P, hc, hs) -reshape-> (bs, seq_len/P, P, hc/P, hs) -transpose(0,2)-> (P, seq_len/P, bs, hc/P, hs)
179
+ input_t = (
180
+ input.reshape(bs, shard_seqlen, seq_world_size, shard_hc, hs)
181
+ .transpose(0, 2)
182
+ .contiguous()
183
+ )
184
+
185
+ if seq_world_size > 1:
186
+ output = torch.empty_like(input_t)
187
+ # Stream-ordered: NCCL enqueues on the current stream and every
188
+ # consumer below is on that same stream, so no host sync is needed.
189
+ dist.all_to_all_single(output, input_t, group=group)
190
+ else:
191
+ output = input_t
192
+
193
+ output = output.reshape(seqlen, bs, shard_hc, hs)
194
+ # (seq_len, bs, hc/P, hs) -reshape-> (bs, seq_len, hc/P, hs)
195
+ output = output.transpose(0, 1).contiguous().reshape(bs, seqlen, shard_hc, hs)
196
+ if gap > 0:
197
+ output = output[:, :-gap]
198
+
199
+ return output
200
+
201
+ elif scatter_idx == 1 and gather_idx == 2:
202
+ # (bs, seqlen, hc/P, hs) -> (bs, seqlen/P, hc, hs)
203
+ # `seqlen` here is already the global length, so the split is derived
204
+ # locally with no negotiation.
205
+ bs, seqlen, shard_hc, hs = input.shape
206
+
207
+ hc = shard_hc * seq_world_size
208
+ if seqlen % seq_world_size != 0:
209
+ new_seqlen = (seqlen // seq_world_size + 1) * seq_world_size
210
+ gap = new_seqlen - seqlen
211
+ input = F.pad(input, (0, 0, 0, 0, 0, gap))
212
+ bs, seqlen, shard_hc, hs = input.shape
213
+ else:
214
+ gap = 0
215
+
216
+ shard_seqlen = seqlen // seq_world_size
217
+
218
+ # (bs, seqlen, hc/P, hs) -reshape-> (bs, P, seq_len/P, hc/P, hs) -transpose(0, 3)-> (hc/P, P, seqlen/P, bs, hs) -transpose(0, 1) -> (P, hc/P, seqlen/P, bs, hs)
219
+ input_t = (
220
+ input.reshape(bs, seq_world_size, shard_seqlen, shard_hc, hs)
221
+ .transpose(0, 3)
222
+ .transpose(0, 1)
223
+ .contiguous()
224
+ .reshape(seq_world_size, shard_hc, shard_seqlen, bs, hs)
225
+ )
226
+
227
+ if seq_world_size > 1:
228
+ output = torch.empty_like(input_t)
229
+ dist.all_to_all_single(output, input_t, group=group)
230
+ else:
231
+ output = input_t
232
+
233
+ output = output.reshape(hc, shard_seqlen, bs, hs)
234
+ # (hc, seqlen/N, bs, hs) -tranpose(0,2)-> (bs, seqlen/N, hc, hs)
235
+ output = output.transpose(0, 2).contiguous().reshape(bs, shard_seqlen, hc, hs)
236
+
237
+ if gap > 0 and dist.get_group_rank(group, dist.get_rank()) == seq_world_size - 1:
238
+ output = output[:, :-gap]
239
+
240
+ return output
241
+ else:
242
+ raise RuntimeError("scatter_idx must be 1 or 2 and gather_idx must be 1 or 2")
243
+
244
+
245
+ class SeqAllToAll4D(torch.autograd.Function):
246
+ @staticmethod
247
+ def forward(
248
+ ctx: Any,
249
+ group: dist.ProcessGroup,
250
+ input: torch.Tensor,
251
+ scatter_idx: int,
252
+ gather_idx: int,
253
+ full_seq_len: Optional[int] = None,
254
+ ) -> torch.Tensor:
255
+ ctx.group = group
256
+ ctx.scatter_idx = scatter_idx
257
+ ctx.gather_idx = gather_idx
258
+ ctx.full_seq_len = full_seq_len
259
+
260
+ return _all_to_all_4D(input, scatter_idx, gather_idx, group=group,
261
+ full_seq_len=full_seq_len)
262
+
263
+ @staticmethod
264
+ def backward(ctx: Any, *grad_output: torch.Tensor):
265
+ return (
266
+ None,
267
+ SeqAllToAll4D.apply(
268
+ ctx.group, *grad_output, ctx.gather_idx, ctx.scatter_idx,
269
+ ctx.full_seq_len,
270
+ ),
271
+ None,
272
+ None,
273
+ None,
274
+ )
275
+
276
+
277
+ def all_to_all_4D(
278
+ input_: torch.Tensor,
279
+ group: dist.ProcessGroup,
280
+ scatter_dim: int = 2,
281
+ gather_dim: int = 1,
282
+ full_seq_len: Optional[int] = None,
283
+ ):
284
+ return SeqAllToAll4D.apply(group, input_, scatter_dim, gather_dim, full_seq_len)
285
+
286
+
287
+ def _all_to_all(
288
+ input_: torch.Tensor,
289
+ world_size: int,
290
+ group: dist.ProcessGroup,
291
+ scatter_dim: int,
292
+ gather_dim: int,
293
+ ):
294
+ input_list = [
295
+ t.contiguous() for t in torch.tensor_split(input_, world_size, scatter_dim)
296
+ ]
297
+ output_list = [torch.empty_like(input_list[0]) for _ in range(world_size)]
298
+ dist.all_to_all(output_list, input_list, group=group)
299
+ return torch.cat(output_list, dim=gather_dim).contiguous()
300
+
301
+
302
+ class _AllToAll(torch.autograd.Function):
303
+ """All-to-all communication.
304
+
305
+ Args:
306
+ input_: input matrix
307
+ process_group: communication group
308
+ scatter_dim: scatter dimension
309
+ gather_dim: gather dimension
310
+ """
311
+
312
+ @staticmethod
313
+ def forward(ctx, input_, process_group, scatter_dim, gather_dim):
314
+ ctx.process_group = process_group
315
+ ctx.scatter_dim = scatter_dim
316
+ ctx.gather_dim = gather_dim
317
+ ctx.world_size = dist.get_world_size(process_group)
318
+ output = _all_to_all(
319
+ input_, ctx.world_size, process_group, scatter_dim, gather_dim
320
+ )
321
+ return output
322
+
323
+ @staticmethod
324
+ def backward(ctx, grad_output):
325
+ grad_output = _all_to_all(
326
+ grad_output,
327
+ ctx.world_size,
328
+ ctx.process_group,
329
+ ctx.gather_dim,
330
+ ctx.scatter_dim,
331
+ )
332
+ return (
333
+ grad_output,
334
+ None,
335
+ None,
336
+ None,
337
+ )
338
+
339
+
340
+ def all_to_all(
341
+ input_: torch.Tensor, group: dist.ProcessGroup, scatter_dim: int = 2, gather_dim: int = 1
342
+ ):
343
+ return _AllToAll.apply(input_, group, scatter_dim, gather_dim)
344
+
345
+
346
+ class _AllGather(torch.autograd.Function):
347
+ """All-gather along ``dim`` with autograd support.
348
+
349
+ ``full_seq_len`` lets both the forward gather and the backward split derive
350
+ the per-rank shard sizes locally, so neither direction needs the shape
351
+ negotiation collective.
352
+ """
353
+
354
+ @staticmethod
355
+ def forward(ctx, input_, dim, group, full_seq_len=None):
356
+ world_size = dist.get_world_size(group)
357
+ rank = dist.get_group_rank(group, dist.get_rank())
358
+
359
+ if full_seq_len is not None:
360
+ sizes = sp_chunk_sizes(int(full_seq_len), world_size)
361
+ else:
362
+ shapes = [None] * world_size
363
+ dist.all_gather_object(shapes, input_.shape[dim], group)
364
+ sizes = [int(s) for s in shapes]
365
+
366
+ ctx.dim = dim
367
+ ctx.group = group
368
+ ctx.sizes = sizes
369
+ ctx.rank = rank
370
+
371
+ input_ = input_.contiguous()
372
+ if all(s == sizes[0] for s in sizes):
373
+ # Uniform shards: a single ncclAllGather instead of the
374
+ # broadcast-per-rank fallback PyTorch falls back to for ragged inputs.
375
+ if dim == 0:
376
+ out_shape = list(input_.shape)
377
+ out_shape[0] = sizes[0] * world_size
378
+ flat = torch.empty(out_shape, dtype=input_.dtype, device=input_.device)
379
+ dist.all_gather_into_tensor(flat, input_, group=group)
380
+ return flat
381
+ tensor_list = [torch.empty_like(input_) for _ in range(world_size)]
382
+ dist.all_gather(tensor_list, input_, group=group)
383
+ return torch.cat(tensor_list, dim=dim)
384
+
385
+ tensor_list = []
386
+ for size in sizes:
387
+ shape = list(input_.shape)
388
+ shape[dim] = size
389
+ tensor_list.append(
390
+ torch.empty(shape, dtype=input_.dtype, device=input_.device)
391
+ )
392
+ dist.all_gather(tensor_list, input_, group=group)
393
+ return torch.cat(tensor_list, dim=dim)
394
+
395
+ @staticmethod
396
+ def backward(ctx, grad_output):
397
+ grad_input = torch.split(grad_output, ctx.sizes, dim=ctx.dim)[ctx.rank]
398
+ return grad_input.contiguous(), None, None, None
399
+
400
+
401
+ def all_gather(input_: torch.Tensor, dim: int = 1, group=None,
402
+ full_seq_len: Optional[int] = None):
403
+ """Gather ``input_`` shards along ``dim`` across the SP group.
404
+
405
+ Args:
406
+ input_: this rank's shard.
407
+ dim: concatenation dimension.
408
+ group: SP process group.
409
+ full_seq_len: global length along ``dim``. When supplied the shard sizes
410
+ are computed locally, avoiding a shape-negotiation collective in both
411
+ the forward and the backward pass.
412
+ """
413
+ return _AllGather.apply(input_, dim, group, full_seq_len)
hymm/utils/parallel_states.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Distributed / sequence-parallel state for MOVA.
2
+
3
+ The topology follows HunyuanVideo-1.5: two independent device meshes over the
4
+ same world.
5
+
6
+ world_mesh = [dp, sp] -> data parallel x Ulysses sequence parallel
7
+ fsdp_mesh = [dp_replicate, fsdp_shard] -> FSDP2 parameter sharding
8
+
9
+ SP shards activations, FSDP shards parameters, so the two meshes are
10
+ orthogonal and ``fsdp_shard`` deliberately spans SP ranks as well.
11
+
12
+ Model code only ever touches :data:`nccl_info` and
13
+ :func:`get_sequence_parallel_state`, which are kept as thin views over the
14
+ active :class:`ParallelDims`, so training and inference share one init path.
15
+ """
16
+
17
+ import os
18
+ import random
19
+ from dataclasses import dataclass, field
20
+ from datetime import timedelta
21
+ from typing import Any, Optional
22
+
23
+ import numpy as np
24
+ import torch
25
+ import torch.distributed as dist
26
+ from torch.distributed.device_mesh import init_device_mesh
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Topology
31
+ # ---------------------------------------------------------------------------
32
+
33
+ @dataclass
34
+ class ParallelDims:
35
+ sp: int = 1
36
+ dp_replicate: int = 1
37
+ world_size: int = -1
38
+ use_dynamic_ring_attention: bool = False
39
+
40
+ world_mesh: Any = field(default=None, init=False, repr=False)
41
+ fsdp_mesh: Any = field(default=None, init=False, repr=False)
42
+ sp_group: Any = field(default=None, init=False, repr=False)
43
+ sp_rank: int = field(default=0, init=False)
44
+ dp_rank: int = field(default=0, init=False)
45
+ dp_size: int = field(default=1, init=False)
46
+
47
+ def __post_init__(self):
48
+ if self.world_size == -1:
49
+ if dist.is_initialized():
50
+ self.world_size = dist.get_world_size()
51
+ else:
52
+ self.world_size = int(os.environ.get("WORLD_SIZE", "1"))
53
+ if self.dp_replicate == -1:
54
+ # One replica per node; FSDP shards within a node.
55
+ local_world = int(os.environ.get("LOCAL_WORLD_SIZE", "8"))
56
+ assert self.world_size % local_world == 0, (
57
+ f"world_size({self.world_size}) must be divisible by "
58
+ f"LOCAL_WORLD_SIZE({local_world}) for dp_replicate=-1"
59
+ )
60
+ self.dp_replicate = self.world_size // local_world
61
+ assert self.sp >= 1 and self.dp_replicate >= 1
62
+ assert self.world_size % self.sp == 0, (
63
+ f"world_size({self.world_size}) must be divisible by sp({self.sp})"
64
+ )
65
+ assert self.world_size % self.dp_replicate == 0, (
66
+ f"world_size({self.world_size}) must be divisible by "
67
+ f"dp_replicate({self.dp_replicate})"
68
+ )
69
+
70
+ # -- mesh -------------------------------------------------------------
71
+
72
+ def build_mesh(self, device_type: str = "cuda"):
73
+ self.world_mesh = init_device_mesh(
74
+ device_type,
75
+ (self.world_size // self.sp, self.sp),
76
+ mesh_dim_names=("dp", "sp"),
77
+ )
78
+ self.fsdp_mesh = init_device_mesh(
79
+ device_type,
80
+ (self.dp_replicate, self.world_size // self.dp_replicate),
81
+ mesh_dim_names=("dp_replicate", "fsdp_shard"),
82
+ )
83
+
84
+ self.dp_rank = self.world_mesh["dp"].get_local_rank()
85
+ self.dp_size = self.world_mesh["dp"].size()
86
+ if self.sp_enabled:
87
+ self.sp_rank = self.world_mesh["sp"].get_local_rank()
88
+ self.sp_group = self.world_mesh["sp"].get_group()
89
+ else:
90
+ self.sp_rank = 0
91
+ self.sp_group = None
92
+ return self.world_mesh
93
+
94
+ # -- properties -------------------------------------------------------
95
+
96
+ @property
97
+ def sp_enabled(self) -> bool:
98
+ return self.sp > 1
99
+
100
+ @property
101
+ def dp_enabled(self) -> bool:
102
+ return self.dp_size > 1
103
+
104
+ @property
105
+ def dp_replicate_enabled(self) -> bool:
106
+ return self.dp_replicate > 1
107
+
108
+ @property
109
+ def dp_shard_enabled(self) -> bool:
110
+ return (self.world_size // self.dp_replicate) > 1
111
+
112
+ @property
113
+ def pp_enabled(self) -> bool:
114
+ return False
115
+
116
+ @property
117
+ def sp_mesh(self):
118
+ return self.world_mesh["sp"]
119
+
120
+ @property
121
+ def dp_mesh(self):
122
+ return self.world_mesh["dp"]
123
+
124
+ @property
125
+ def sp_src_rank(self) -> int:
126
+ """Global rank of local rank 0 inside this rank's SP group."""
127
+ if not self.sp_enabled:
128
+ return dist.get_rank() if dist.is_initialized() else 0
129
+ return dist.get_global_rank(self.sp_group, 0)
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Legacy view used by the model code (nccl_info.sp_group / .sp_size / ...)
134
+ # ---------------------------------------------------------------------------
135
+
136
+ class COMM_INFO:
137
+ def __init__(self):
138
+ self.sp_group = None
139
+ self.sp_size = 1
140
+ self.global_rank = 0
141
+ self.rank_within_spgroup = 0
142
+ self.parallel_dims = None
143
+ self.device_mesh = None
144
+ self.use_dynamic_ring_attention = False
145
+ self.sp_stream = None
146
+ self.sp_rank_list = None
147
+
148
+
149
+ nccl_info = COMM_INFO()
150
+ _SEQUENCE_PARALLEL_STATE = False
151
+ _PARALLEL_DIMS: Optional[ParallelDims] = None
152
+
153
+
154
+ def set_sequence_parallel_state(state: bool):
155
+ global _SEQUENCE_PARALLEL_STATE
156
+ _SEQUENCE_PARALLEL_STATE = state
157
+
158
+
159
+ def get_sequence_parallel_state() -> bool:
160
+ return _SEQUENCE_PARALLEL_STATE
161
+
162
+
163
+ def get_parallel_state() -> ParallelDims:
164
+ """Active topology, or a mesh-free single-rank default if none was set up.
165
+
166
+ The fallback deliberately does NOT build a device mesh: ``init_device_mesh``
167
+ is collective, and creating one from an incidental accessor call would mean
168
+ some ranks entering a collective their peers never reach.
169
+ """
170
+ global _PARALLEL_DIMS
171
+ if _PARALLEL_DIMS is None:
172
+ _PARALLEL_DIMS = ParallelDims(sp=1, dp_replicate=1, world_size=1)
173
+ return _PARALLEL_DIMS
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Init
178
+ # ---------------------------------------------------------------------------
179
+
180
+ def init_distributed(timeout_seconds: int = 5400) -> int:
181
+ """Initialise the default process group and bind this rank's device.
182
+
183
+ ``device_id`` is passed on purpose: it makes PyTorch build the default NCCL
184
+ communicator eagerly during ``init_process_group`` instead of lazily on the
185
+ first collective. Lazy communicator creation is a classic source of hangs
186
+ when different ranks reach their first collective through different code
187
+ paths (e.g. the MoE expert alternating between steps).
188
+
189
+ The timeout here covers *startup*, where a rank can legitimately sit in a
190
+ barrier for a long time while a peer streams a 15B checkpoint off a network
191
+ filesystem. Shrink it with :func:`set_collective_timeout` once the training
192
+ loop starts, where a long wait means a genuine desync rather than slow I/O.
193
+ """
194
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
195
+ if torch.cuda.is_available():
196
+ torch.cuda.set_device(local_rank)
197
+ if not dist.is_initialized():
198
+ kwargs = {}
199
+ if torch.cuda.is_available():
200
+ kwargs["device_id"] = torch.device("cuda", local_rank)
201
+ dist.init_process_group(
202
+ backend="nccl",
203
+ timeout=timedelta(seconds=timeout_seconds),
204
+ **kwargs,
205
+ )
206
+ return local_rank
207
+
208
+
209
+ def set_collective_timeout(timeout_seconds: int, logger=None):
210
+ """Re-arm the watchdog with a tighter per-collective deadline.
211
+
212
+ Called once the steady-state training loop begins. A short deadline is what
213
+ turns a desync into an abort with a stack trace instead of a job that sits
214
+ idle until someone notices. Applied to every group we created, since the
215
+ watchdog is per-communicator.
216
+
217
+ Failures are swallowed deliberately: this touches a private torch API and
218
+ performs no collective, so a rank that cannot re-arm simply keeps the
219
+ startup timeout rather than diverging from its peers.
220
+ """
221
+ if not dist.is_initialized():
222
+ return False
223
+ try:
224
+ from torch.distributed.distributed_c10d import _set_pg_timeout
225
+ except ImportError:
226
+ if logger is not None:
227
+ logger.warning("[dist] _set_pg_timeout unavailable; keeping the startup timeout")
228
+ return False
229
+
230
+ timeout = timedelta(seconds=int(timeout_seconds))
231
+ groups = [None]
232
+ pd = _PARALLEL_DIMS
233
+ if pd is not None:
234
+ if pd.world_mesh is not None:
235
+ groups += [pd.world_mesh["sp"].get_group(), pd.world_mesh["dp"].get_group()]
236
+ if pd.fsdp_mesh is not None:
237
+ groups += [pd.fsdp_mesh["fsdp_shard"].get_group(),
238
+ pd.fsdp_mesh["dp_replicate"].get_group()]
239
+ for group in groups:
240
+ try:
241
+ _set_pg_timeout(timeout, group)
242
+ except Exception as exc: # noqa: BLE001
243
+ if logger is not None:
244
+ logger.warning(f"[dist] could not re-arm timeout on {group}: {exc}")
245
+ return False
246
+ if logger is not None:
247
+ logger.info(f"[dist] per-collective timeout tightened to {timeout_seconds}s for training")
248
+ return True
249
+
250
+
251
+ def initialize_parallel_state(
252
+ sp: int = 1,
253
+ dp_replicate: int = 1,
254
+ use_dynamic_ring_attention: bool = False,
255
+ warmup: bool = True,
256
+ ) -> ParallelDims:
257
+ """Build the device meshes and publish them to the model-facing globals."""
258
+ global _PARALLEL_DIMS
259
+
260
+ parallel_dims = ParallelDims(
261
+ sp=sp,
262
+ dp_replicate=dp_replicate,
263
+ use_dynamic_ring_attention=use_dynamic_ring_attention,
264
+ )
265
+ if dist.is_initialized():
266
+ parallel_dims.build_mesh("cuda")
267
+
268
+ nccl_info.sp_size = parallel_dims.sp
269
+ nccl_info.global_rank = dist.get_rank() if dist.is_initialized() else 0
270
+ nccl_info.rank_within_spgroup = parallel_dims.sp_rank
271
+ nccl_info.sp_group = parallel_dims.sp_group
272
+ nccl_info.parallel_dims = parallel_dims
273
+ nccl_info.device_mesh = parallel_dims.world_mesh
274
+ nccl_info.use_dynamic_ring_attention = use_dynamic_ring_attention
275
+ if use_dynamic_ring_attention and parallel_dims.sp_enabled:
276
+ nccl_info.sp_stream = torch.cuda.Stream()
277
+ nccl_info.sp_rank_list = parallel_dims.world_mesh["sp"].mesh.tolist()
278
+
279
+ set_sequence_parallel_state(parallel_dims.sp_enabled)
280
+ _PARALLEL_DIMS = parallel_dims
281
+
282
+ if warmup and dist.is_initialized():
283
+ warmup_process_groups(parallel_dims)
284
+ return parallel_dims
285
+
286
+
287
+ def warmup_process_groups(parallel_dims: Optional[ParallelDims] = None):
288
+ """Force NCCL communicator creation for every group we will ever use.
289
+
290
+ Every rank walks the same group list in the same order, so all communicators
291
+ exist before the training loop starts. Without this, the first collective on
292
+ a lazily-created group can land while other ranks are inside a different
293
+ collective, which NCCL cannot untangle and the watchdog eventually kills.
294
+ """
295
+ if not dist.is_initialized():
296
+ return
297
+ parallel_dims = parallel_dims or get_parallel_state()
298
+ device = torch.device("cuda", torch.cuda.current_device())
299
+ probe = torch.zeros(1, device=device)
300
+
301
+ groups = [None] # default process group
302
+ if parallel_dims.world_mesh is not None:
303
+ groups.append(parallel_dims.world_mesh["sp"].get_group())
304
+ groups.append(parallel_dims.world_mesh["dp"].get_group())
305
+ if parallel_dims.fsdp_mesh is not None:
306
+ groups.append(parallel_dims.fsdp_mesh["fsdp_shard"].get_group())
307
+ groups.append(parallel_dims.fsdp_mesh["dp_replicate"].get_group())
308
+
309
+ for group in groups:
310
+ dist.all_reduce(probe, group=group)
311
+ torch.cuda.synchronize()
312
+
313
+
314
+ def destroy_sequence_parallel_group():
315
+ if dist.is_initialized():
316
+ dist.destroy_process_group()
317
+
318
+
319
+ # ---------------------------------------------------------------------------
320
+ # Sequence-parallel sequence-length context
321
+ # ---------------------------------------------------------------------------
322
+
323
+ class SPSequenceContext:
324
+ """Full (pre-shard) sequence length of each SP-sharded stream.
325
+
326
+ Ulysses' head-scatter all-to-all has to undo the uneven ``torch.chunk``
327
+ split, which means it needs the *global* sequence length -- information a
328
+ rank cannot recover from its own shard. The towers publish it here once per
329
+ forward instead of paying a ``dist.all_gather_object`` inside every single
330
+ attention call (~800 object collectives per step at 40 layers x 2 towers).
331
+
332
+ Values stay valid until the next forward overwrites them, so activation
333
+ checkpointing recompute during backward reads the correct lengths.
334
+ """
335
+
336
+ __slots__ = ("visual", "audio")
337
+
338
+ def __init__(self):
339
+ self.visual = None
340
+ self.audio = None
341
+
342
+ def set(self, visual=None, audio=None):
343
+ if visual is not None:
344
+ self.visual = int(visual)
345
+ if audio is not None:
346
+ self.audio = int(audio)
347
+
348
+ def get(self, stream: str):
349
+ return getattr(self, stream, None)
350
+
351
+ def clear(self):
352
+ self.visual = None
353
+ self.audio = None
354
+
355
+
356
+ sp_seq_ctx = SPSequenceContext()
357
+
358
+
359
+ # ---------------------------------------------------------------------------
360
+ # Cross-rank data / RNG alignment (replaces hy_parallelism)
361
+ # ---------------------------------------------------------------------------
362
+
363
+ def _flatten_tensors(obj, out):
364
+ if isinstance(obj, torch.Tensor):
365
+ out.append(obj)
366
+ elif isinstance(obj, dict):
367
+ for value in obj.values():
368
+ _flatten_tensors(value, out)
369
+ elif isinstance(obj, (list, tuple)):
370
+ for value in obj:
371
+ _flatten_tensors(value, out)
372
+ return out
373
+
374
+
375
+ def _rebuild_with_tensors(obj, tensors, cursor):
376
+ if isinstance(obj, torch.Tensor):
377
+ value = tensors[cursor[0]]
378
+ cursor[0] += 1
379
+ return value
380
+ if isinstance(obj, dict):
381
+ return {k: _rebuild_with_tensors(v, tensors, cursor) for k, v in obj.items()}
382
+ if isinstance(obj, (list, tuple)):
383
+ rebuilt = [_rebuild_with_tensors(v, tensors, cursor) for v in obj]
384
+ return tuple(rebuilt) if isinstance(obj, tuple) else rebuilt
385
+ return obj
386
+
387
+
388
+ def sync_data_for_sp(obj, parallel_dims: Optional[ParallelDims] = None, force_object: bool = False):
389
+ """Make ``obj`` identical on every rank of the SP group (source = local rank 0).
390
+
391
+ Containers are walked recursively, so a whole dataloader batch can be passed
392
+ straight in. Non-tensor leaves go through ``broadcast_object_list``; tensors
393
+ are broadcast on the device.
394
+
395
+ The shapes are agreed on FIRST, in a single object broadcast, and any rank
396
+ whose local tensor disagrees reallocates before the data broadcast. That
397
+ matters because ``dist.broadcast`` demands matching shapes on every
398
+ participant: without the agreement step a rank that silently substituted a
399
+ different sample (the dataset retries on an I/O error, and an error need not
400
+ hit every rank) would corrupt the batch or hang the group -- the exact
401
+ failure this call exists to prevent.
402
+ """
403
+ parallel_dims = parallel_dims or get_parallel_state()
404
+ if not parallel_dims.sp_enabled:
405
+ return obj
406
+
407
+ src = parallel_dims.sp_src_rank
408
+ group = parallel_dims.sp_group
409
+
410
+ if force_object or not isinstance(obj, (torch.Tensor, dict, list, tuple)):
411
+ holder = [obj]
412
+ dist.broadcast_object_list(holder, src=src, group=group)
413
+ return holder[0]
414
+
415
+ tensors = _flatten_tensors(obj, [])
416
+ if not tensors:
417
+ holder = [obj]
418
+ dist.broadcast_object_list(holder, src=src, group=group)
419
+ return holder[0]
420
+
421
+ specs = [(tuple(t.shape), t.dtype) for t in tensors]
422
+ holder = [specs]
423
+ dist.broadcast_object_list(holder, src=src, group=group)
424
+ specs = holder[0]
425
+
426
+ device = torch.device("cuda", torch.cuda.current_device())
427
+ synced = []
428
+ for tensor, (shape, dtype) in zip(tensors, specs):
429
+ target_device = tensor.device
430
+ if tuple(tensor.shape) != shape or tensor.dtype != dtype:
431
+ staged = torch.empty(shape, dtype=dtype, device=device)
432
+ else:
433
+ staged = tensor if tensor.is_cuda else tensor.to(device)
434
+ dist.broadcast(staged, src=src, group=group)
435
+ synced.append(staged if target_device.type == "cuda" else staged.to(target_device))
436
+
437
+ return _rebuild_with_tensors(obj, synced, [0])
438
+
439
+
440
+ def sync_random_states(step: int, base_seed: int = 0, dp_rank: int = 0):
441
+ """Re-seed the RNGs so an SP group samples identical noise.
442
+
443
+ Derived purely from ``(base_seed, dp_rank, step)``: every SP rank computes
444
+ the same value locally, so there is no collective and no device sync, while
445
+ distinct DP groups still see distinct noise.
446
+ """
447
+ seed = (int(base_seed) * 1_000_003 + int(dp_rank) * 10_007 + int(step)) % (2 ** 31 - 1)
448
+ random.seed(seed)
449
+ np.random.seed(seed)
450
+ torch.manual_seed(seed)
451
+ if torch.cuda.is_available():
452
+ torch.cuda.manual_seed_all(seed)
453
+ return seed
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.8.0
2
+ torchvision==0.23.0
3
+ diffusers==0.33.0
4
+ transformers==4.57.1
5
+ accelerate>=1.10.0
6
+ safetensors>=0.6.2
7
+ huggingface_hub[hf_xet]
8
+ einops>=0.8.1
9
+ loguru
10
+ ftfy
11
+ imageio[ffmpeg]>=2.37.0
12
+ numpy
13
+ scipy
14
+ sentencepiece