-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[TRTLLM-15917][feat] Integrate Sol-Attn sparse attention into VisualGen #18329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
karljang
wants to merge
9
commits into
NVIDIA:main
Choose a base branch
from
karljang:feat/sol-attn-visualgen-reduced
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
60ef12a
[TRTLLM-15917][feat] Integrate Sol-Attn sparse attention into VisualGen
karljang 8bf2f15
[TRTLLM-15917][fix] Keep the Sol-Attn CuTe DSL launch opaque to Dynamo
karljang 01b82f0
[TRTLLM-15917][doc] Declare sol_attn_backend.py's upstream derivation
karljang 4af47b6
[TRTLLM-15917][feat] Drop sm120 from the Sol-Attn port
karljang f8d6156
[TRTLLM-15917][fix] Keep Sol-Attn's non-sparse attention on the confi…
karljang 117c2a9
[TRTLLM-15917][fix] Restrict the in-family fallback to true cross-att…
karljang 7efdc28
[TRTLLM-15917][chore] Rename SolAttnAttention to SolAttention
karljang b25ca70
[TRTLLM-15917][refactor] Decide Sol-Attn eligibility per call, not pe…
karljang dd788c7
[TRTLLM-15917][fix] Address review findings on the Sol-Attn integration
karljang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
304 changes: 304 additions & 0 deletions
304
tensorrt_llm/_torch/visual_gen/attention_backend/cute_dsl/sol_attn.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,304 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| """ | ||
| Sol-Attn backend for visual generation models. | ||
|
|
||
| Sol-Attn (https://arxiv.org/abs/2607.24027) is dynamic block routing + | ||
| sparse computation + approximation correction folded into one online-softmax | ||
| pass. The kernel is vendored from its reference implementation | ||
| (https://github.com/NVlabs/Sana, branch | ||
| https://github.com/NVlabs/Sana/tree/sol-engine, pinned at commit | ||
| https://github.com/NVlabs/Sana/commit/5fe5feb -- see | ||
| ``cute_dsl_kernels/blackwell/sol_attn/THIRD_PARTY_NOTICES.md`` for the pin | ||
| and its currency-check note) under ``..cute_dsl_kernels.blackwell.sol_attn`` | ||
| / ``sol_attn_backend.py``. Only the sm100 (B200/GB200) | ||
| Blackwell) kernels are carried; the upstream sm89/sm90 kernels and the Triton | ||
| reference path are not, and the FlashAttention CuTe helpers they needed come | ||
| from the ``flash-attn-4`` dependency rather than a vendored copy. | ||
|
|
||
| This file is only the TRT-LLM AttentionBackend adapter around that kernel's | ||
| public BTHD entry point, plus the dense_layers layer-skip guard. | ||
|
|
||
| ``disabled_until_timestep`` is the dense-prefix control, and mirrors | ||
| skip_softmax's field of the same name: sparse attention stays disabled (that | ||
| is, the layer runs the backend's dense kernel) while the normalized timestep is at | ||
| or above the cutoff, and switches to the sparse kernel once it drops below. | ||
|
|
||
| The timestep arrives as a forward kwarg -- ``modules/attention.py`` already | ||
| threads it to every backend, and all VisualGen pipelines normalize it to | ||
| ``[0, 1]`` by ``num_train_timesteps`` per the ``BaseDiffusionModel.forward`` | ||
| contract. Nothing has to be wired per pipeline, and there is no process-wide | ||
| state to keep in sync. | ||
|
|
||
| """ | ||
|
|
||
| from typing import Any, Optional | ||
|
|
||
| import torch | ||
|
|
||
| from tensorrt_llm.logger import logger | ||
|
|
||
| from ..interface import AttentionBackend, AttentionTensorLayout | ||
|
|
||
| _sol_attn_import_error = None | ||
| try: | ||
| from tensorrt_llm._torch.visual_gen.cute_dsl_kernels.blackwell.sol_attn_backend import ( | ||
| _run_sol_attn_bthd as _sol_attn_run, | ||
| ) | ||
| except (ImportError, OSError) as e: | ||
| _sol_attn_run = None | ||
| _sol_attn_import_error = e | ||
|
|
||
|
|
||
| def _as_float(timestep: Any) -> Optional[float]: | ||
| """Coerce a scalar/0-d/1-element timestep to float, else None.""" | ||
| if timestep is None: | ||
| return None | ||
| if isinstance(timestep, torch.Tensor): | ||
| if timestep.numel() == 0: | ||
| return None | ||
| return float(timestep.reshape(-1)[0].item()) | ||
| try: | ||
| return float(timestep) | ||
| except (TypeError, ValueError): | ||
| return None | ||
|
|
||
|
|
||
| def sol_attn_graph_phase( | ||
| timestep: Any, *, disabled_until_timestep: Optional[float] | ||
| ) -> Optional[int]: | ||
| """Return 1 once descending timesteps cross the cutoff, 0 before, else None. | ||
|
|
||
| Same contract and sense as | ||
| ``SkipSoftmaxScheduler.get_graph_phase_for_timestep``: phase 0 is the dense | ||
| prefix, phase 1 the sparse phase, and ``None`` means there is no phase to | ||
| distinguish so the CUDA-graph runner omits the key part. | ||
| """ | ||
| if disabled_until_timestep is None: | ||
| return None | ||
| value = _as_float(timestep) | ||
| if value is None: | ||
| return None | ||
| return int(value < disabled_until_timestep) | ||
|
|
||
|
|
||
| def _cute_dense_available() -> bool: | ||
| """Whether `cute_dsl_fmha_fwd` can run on the current device. | ||
|
|
||
| Checked once at construction. Sol-Attn is sm100-only and the dense CuTe DSL | ||
| kernel covers sm_100a/sm_103a, so in practice this is always true wherever | ||
| Sol-Attn runs; the negative branch exists so an unsupported device degrades | ||
| to SDPA instead of raising. | ||
| """ | ||
| try: | ||
| from .fmha import _check_cute_runtime_available, _get_gpu_arch | ||
|
|
||
| _check_cute_runtime_available() | ||
| _get_gpu_arch() | ||
| except Exception: | ||
| return False | ||
| return True | ||
|
|
||
|
|
||
| def _parse_dense_layers(spec: Optional[str]) -> frozenset[int]: | ||
| layers: set[int] = set() | ||
| for item in str(spec or "").split(","): | ||
| item = item.strip() | ||
| if not item: | ||
| continue | ||
| if "-" in item: | ||
| start, end = item.split("-", 1) | ||
| layers.update(range(int(start), int(end) + 1)) | ||
| else: | ||
| layers.add(int(item)) | ||
| return frozenset(layers) | ||
|
|
||
|
|
||
| class SolAttention(AttentionBackend): | ||
| """Sol-Attn dynamic block-routing sparse attention (CuTeDSL, sm100). | ||
|
|
||
| The kernel wrapper already falls back to dense attention on any unsupported | ||
| shape/dtype/arch (see ``_run_sol_attn_bthd``); this class only adds the | ||
| ``dense_layers`` layer-skip guard (evaluated at construction time, no | ||
| external plumbing needed) and forwards the routing knobs from config. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| layer_idx: int = 0, | ||
| num_heads: int = 8, | ||
| head_dim: int = 128, | ||
| num_kv_heads: Optional[int] = None, | ||
| dtype: Optional[torch.dtype] = None, | ||
| sparse_attention_config=None, | ||
| **kwargs, | ||
| ): | ||
| if _sol_attn_run is None: | ||
| raise ImportError( | ||
| "SolAttention requires the vendored sol_attn kernel " | ||
| f"package; import failed: {_sol_attn_import_error}" | ||
| ) | ||
| self.layer_idx = layer_idx | ||
| self.num_heads = num_heads | ||
| self.head_dim = head_dim | ||
| self.num_kv_heads = num_kv_heads or num_heads | ||
| if self.num_kv_heads != self.num_heads: | ||
| # Not an assert: `python -O` strips those, and the kernel wrapper | ||
| # would then see unequal Q/K shapes and quietly take its dense | ||
| # fallback instead of rejecting an unsupported configuration. | ||
| raise ValueError( | ||
| f"Sol-Attn is MHA-only (num_kv_heads == num_heads), got " | ||
| f"num_kv_heads={self.num_kv_heads}, num_heads={self.num_heads}. " | ||
| f"GQA/MQA is not supported." | ||
| ) | ||
| self.dtype = dtype | ||
| cfg = sparse_attention_config | ||
| self.tau = getattr(cfg, "tau", 1.0) | ||
| self.thresh_type = getattr(cfg, "thresh_type", "diag") | ||
| self.kv_splits = getattr(cfg, "kv_splits", "auto") | ||
| self.disabled_until_timestep = getattr(cfg, "disabled_until_timestep", None) | ||
| self.dense_layers = _parse_dense_layers(getattr(cfg, "dense_layers", None)) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| # Sol-Attn's dense steps must run the backend the user selected. Without | ||
| # this they ran torch SDPA while a `backend: CUTEDSL` baseline ran | ||
| # cute_dsl_fmha_fwd, so candidate and reference differed on the dense | ||
| # steps too -- measured at LPIPS 0.214 on Wan2.2-T2V-A14B with sparsity | ||
| # switched off entirely, against a 0.25 gate. | ||
| from .fmha import CuTeDSLAttention | ||
|
|
||
| self._inner = CuTeDSLAttention( | ||
| layer_idx=layer_idx, | ||
| num_heads=num_heads, | ||
| head_dim=head_dim, | ||
| num_kv_heads=self.num_kv_heads, | ||
| dtype=dtype, | ||
| ) | ||
| # Whether the CuTe DSL dense kernel can serve this device, decided once | ||
| # here. Doing it at construction (rather than lazily on the first call) | ||
| # keeps `_dense` free of attribute mutation, so it stays traceable and | ||
| # the dense step sits in the same place in the graph as the dense | ||
| # CUTEDSL baseline's does. Deciding it lazily and marking `_dense` | ||
| # `@torch.compiler.disable` instead moved the whole dense step out of | ||
| # the graph and reintroduced the very mismatch this is meant to remove: | ||
| # measured LPIPS 0.4044 compiled, against 0.2112 eager. | ||
| self._cute_dense_ok = _cute_dense_available() | ||
| if not self._cute_dense_ok: | ||
| logger.warning_once( | ||
| "[sol-attn] the CuTe DSL FMHA kernel cannot serve this device; dense " | ||
| "steps will use torch SDPA. Numerics will differ from a `backend: " | ||
| "CUTEDSL` dense baseline.", | ||
| key="sol_attn_dense_backend_unavailable", | ||
| ) | ||
|
|
||
| # The `.item()` in here would graph-break the enclosing block once per | ||
| # attention layer, so keep it in eager (as cute_dsl/fmha.py and VSA's | ||
| # `_get_vsa_inputs` do). Returns a host-side bool, so the dense and sparse | ||
| # phases still compile as separate graphs -- they run different kernels. | ||
| @torch.compiler.disable | ||
| def _dense_by_step(self, timestep: Any) -> bool: | ||
| phase = sol_attn_graph_phase( | ||
| timestep, | ||
| disabled_until_timestep=self.disabled_until_timestep, | ||
| ) | ||
| if phase is None: | ||
| # Fail open, matching the CuTeDSL skip-softmax path: without a | ||
| # timestep we cannot tell which phase we are in, so run the | ||
| # sparse kernel rather than silently forcing dense forever. | ||
| # This degrades quality rather than raising, so say so once. | ||
| logger.warning_once( | ||
| "SolAttentionConfig.disabled_until_timestep=" | ||
| f"{self.disabled_until_timestep} is set, but no `timestep` reached " | ||
| "the Sol-Attn forward call. The dense prefix it requests will not " | ||
| "be applied. Ensure the pipeline passes a normalized timestep, or " | ||
| "unset disabled_until_timestep.", | ||
| key="sol_attn_missing_timestep", | ||
| ) | ||
| return False | ||
| return phase == 0 | ||
|
|
||
| @staticmethod | ||
| def _sdpa(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor: | ||
| """Dense attention via torch SDPA, for devices CuTe DSL cannot serve.""" | ||
| return torch.nn.functional.scaled_dot_product_attention( | ||
| q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2) | ||
| ).transpose(1, 2) | ||
|
|
||
| def _delegate( | ||
| self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, **kwargs: Any | ||
| ) -> torch.Tensor: | ||
| """Hand the call to the dense backend of the same family. | ||
|
|
||
| ``_cute_dense_ok`` answers "can this *device* run the kernel", decided at | ||
| construction; ``q.is_cuda`` answers "is this *tensor* on it". Both are | ||
| needed: the construction-time probe inspects the current CUDA device, so | ||
| it says yes on a GPU host even when a caller passes CPU tensors. | ||
| """ | ||
| if self._cute_dense_ok and q.is_cuda: | ||
| return self._inner.forward(q, k, v, **kwargs) | ||
| return self._sdpa(q, k, v) | ||
|
|
||
| def _can_serve(self, q: torch.Tensor, k: torch.Tensor, **kwargs: Any) -> bool: | ||
| """Whether the sparse kernel applies to this particular call. | ||
|
|
||
| Everything false here is delegated to ``_inner``. Deciding it from the | ||
| tensors, per call, is deliberate: ``qkv_mode`` describes how Q/K/V are | ||
| *projected*, not whether K/V come from another sequence, so a | ||
| construction-time rule keyed on ``SEPARATE_QKV`` mistakes self-attention | ||
| for cross-attention wherever that mode is chosen for other reasons -- | ||
| Qwen-Image always, and WAN's ``attn1`` under async Ulysses. | ||
| """ | ||
| # Cross-attention: K/V come from another sequence. Sol-Attn's routing | ||
| # assumes one self-attending sequence. | ||
| if k.shape[1] != q.shape[1]: | ||
| return False | ||
| if self.layer_idx in self.dense_layers: | ||
| return False | ||
| if self.disabled_until_timestep is not None and self._dense_by_step(kwargs.get("timestep")): | ||
| return False | ||
| return True | ||
|
|
||
| def forward( | ||
| self, | ||
| q: torch.Tensor, | ||
| k: torch.Tensor, | ||
| v: torch.Tensor, | ||
| **kwargs, | ||
| ) -> torch.Tensor: | ||
| """q, k, v: [B, S, H, D] (NHD), same original token order in and out.""" | ||
| if not self._can_serve(q, k, **kwargs): | ||
| return self._delegate(q, k, v, **kwargs) | ||
| return _sol_attn_run( | ||
| q, | ||
| k, | ||
| v, | ||
| tau=self.tau, | ||
| thresh_type=self.thresh_type, | ||
| kv_splits=self.kv_splits, | ||
| # Shape/dtype/arch ineligibility is only detectable inside the | ||
| # wrapper, so that last delegation happens through this hook. | ||
| dense_fn=lambda a, b, c: self._delegate(a, b, c, **kwargs), | ||
| ) | ||
|
|
||
| @classmethod | ||
| def support_lse(cls) -> bool: | ||
| return False | ||
|
|
||
| @property | ||
| def preferred_layout(self) -> AttentionTensorLayout: | ||
| return AttentionTensorLayout.NHD | ||
|
|
||
| @classmethod | ||
| def support_fused_qkv(cls) -> bool: | ||
| return False | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.