-
Notifications
You must be signed in to change notification settings - Fork 173
Add inference speed optimization #371
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import math | ||
| import os | ||
| from math import sqrt | ||
|
|
||
| import torch | ||
|
|
@@ -336,17 +337,34 @@ def do_attention(Q_L, C_L, P_LL): | |
| else: | ||
| # Original full P_LL path | ||
| b = self.to_b(P_LL) | ||
| attn_out = sparse_pairbias_attention( | ||
| Q=q, | ||
| K=k, | ||
| V=v, | ||
| B=b, | ||
| G=g, | ||
| gather_bias=True, | ||
| indices=indices, | ||
| H=self.n_head, | ||
| full=full, | ||
| ) # [D, L, c] | ||
| if use_dense_sdpa_pairbias( | ||
| Q=q, indices=indices, full=full, H=self.n_head | ||
| ): | ||
| # Mathematically equivalent to the sparse path below, but avoids | ||
| # materializing the (D, L, k, c) gathered K/V tensors. See the | ||
| # function docstring for when this is (and isn't) the faster | ||
| # choice. | ||
| attn_out = dense_sdpa_pairbias_attention( | ||
| Q=q, | ||
| K=k, | ||
| V=v, | ||
| B=b, | ||
| G=g, | ||
| indices=indices, | ||
| H=self.n_head, | ||
| ) # [D, L, c] | ||
| else: | ||
| attn_out = sparse_pairbias_attention( | ||
| Q=q, | ||
| K=k, | ||
| V=v, | ||
| B=b, | ||
| G=g, | ||
| gather_bias=True, | ||
| indices=indices, | ||
| H=self.n_head, | ||
| full=full, | ||
| ) # [D, L, c] | ||
|
|
||
| # Output projection (from adaLN-Zero) | ||
| Q_L = self.to_o(attn_out) | ||
|
|
@@ -373,6 +391,121 @@ def do_attention(Q_L, C_L, P_LL): | |
| ###################################################################################### | ||
|
|
||
|
|
||
| _DENSE_PATH_REPORTED = False | ||
|
|
||
|
|
||
| def _report_attention_path(chosen: str, reason: str, shape: str) -> None: | ||
| """Log the attention path once per process so it is visible in run logs.""" | ||
| global _DENSE_PATH_REPORTED | ||
| if _DENSE_PATH_REPORTED: | ||
| return | ||
| _DENSE_PATH_REPORTED = True | ||
| ranked_logger.info( | ||
| f"Atom attention path: {chosen} ({reason}) [{shape}]. " | ||
| "Set RFD3_DENSE_SDPA_ATTENTION=0 to force the original sparse path." | ||
| ) | ||
|
|
||
|
|
||
| def use_dense_sdpa_pairbias(Q, indices, full, H) -> bool: | ||
| """Decide whether to run the dense-SDPA path instead of the sparse gather path. | ||
|
|
||
| Only used at inference (the sparse path is kept for training, where its activation | ||
| memory scales as O(L * k) rather than O(L^2) and matters for the backward pass). | ||
|
|
||
| Set `RFD3_DENSE_SDPA_ATTENTION=0` to force the original sparse path. | ||
| """ | ||
| D, L, _ = Q.shape | ||
| k = indices.shape[-1] | ||
| shape = f"D={D} L={L} k={k} H={H}" | ||
|
|
||
| if full: | ||
| _report_attention_path("SPARSE", "full=True", shape) | ||
| return False | ||
| if torch.is_grad_enabled(): | ||
| _report_attention_path("SPARSE", "grad enabled (training)", shape) | ||
| return False | ||
| if os.environ.get("RFD3_DENSE_SDPA_ATTENTION", "1") != "1": | ||
| _report_attention_path("SPARSE", "disabled via env var", shape) | ||
| return False | ||
| if os.environ.get("RFD3_LOW_MEMORY_MODE", "0") == "1": | ||
| _report_attention_path("SPARSE", "low memory mode", shape) | ||
| return False | ||
| if not Q.is_cuda: | ||
| _report_attention_path("SPARSE", "not on CUDA", shape) | ||
| return False | ||
| # The dense path is only a win while the L x L attention/bias tensors stay small | ||
| # relative to the O(L * k) gathers it replaces, and while they fit comfortably in | ||
| # VRAM. Two (D, H, L, L) bf16 tensors are live at peak (bias + attention scores). | ||
| if L <= k: | ||
| _report_attention_path("SPARSE", f"L={L} <= k={k}", shape) | ||
| return False | ||
| free_bytes, _ = torch.cuda.mem_get_info(Q.device) | ||
| # Peak extra allocation is the (D, H, L, L) bf16 bias plus one transient of the same | ||
| # size from the masked_fill; SDPA itself streams the scores rather than storing them. | ||
| dense_bytes = 2 * D * H * L * L * 2 | ||
| if dense_bytes >= 0.35 * free_bytes: | ||
| _report_attention_path( | ||
| "SPARSE", | ||
| f"needs {dense_bytes / 2**30:.1f} GiB of {free_bytes / 2**30:.1f} GiB free", | ||
| shape, | ||
| ) | ||
| return False | ||
| _report_attention_path("DENSE-SDPA", f"{dense_bytes / 2**30:.1f} GiB bias", shape) | ||
| return True | ||
|
|
||
|
|
||
| def dense_sdpa_pairbias_attention(Q, K, V, B, indices, H, G=None): | ||
| """Pair-bias attention over the same key set as `sparse_pairbias_attention`. | ||
|
|
||
| Rather than gathering K/V into (D, L, k, c), this builds the (D, H, L, L) additive | ||
| bias, sets non-selected keys to -inf, and defers to `scaled_dot_product_attention`. | ||
| The softmax gives zero weight to the masked keys, so the result matches the sparse | ||
| path up to floating-point summation order. | ||
|
|
||
| Q, K, V: (D, L, c) | B: (L, L, H) or (D, L, L, H) | G: (D, L, c) | indices: (D, L, k) | ||
| Returns (D, L, c). | ||
| """ | ||
| D, L, c = Q.shape | ||
| d = c // H | ||
|
|
||
| # q/k are promoted to fp32 by the (auto-cast-excluded) kq RMSNorms; SDPA needs a | ||
| # single dtype for q/k/v, so settle on the value/bias dtype (bf16 under AMP). | ||
| dtype = V.dtype | ||
| q = Q.to(dtype).reshape(D, L, H, d).transpose(1, 2) # (D, H, L, d) | ||
| k = K.to(dtype).reshape(D, L, H, d).transpose(1, 2) | ||
| v = V.reshape(D, L, H, d).transpose(1, 2) | ||
|
|
||
| # Additive pair bias -> (D, H, L, L) | ||
| if B.ndim == 3: # (L, L, H), shared across the diffusion batch | ||
| bias = B.permute(2, 0, 1).unsqueeze(0).expand(D, -1, -1, -1) | ||
| elif B.ndim == 4: # (D, L, L, H) | ||
| bias = B.permute(0, 3, 1, 2) | ||
| else: | ||
| raise ValueError(f"Unexpected pair-bias shape {tuple(B.shape)}") | ||
|
|
||
| # Mask out every key that the sparse path would not have gathered. The sparse path | ||
| # broadcasts indices over the batch via advanced indexing; scatter_ does not, so a | ||
| # shared (1, L, k) index set has to be expanded explicitly or batches 1.. would end | ||
| # up fully masked (-inf everywhere -> NaN out of the softmax). | ||
| indices = indices.to(torch.int64) | ||
| if indices.shape[0] != D: | ||
| if indices.shape[0] != 1: | ||
| raise ValueError( | ||
| f"indices batch dim {indices.shape[0]} is neither 1 nor {D}" | ||
| ) | ||
| indices = indices.expand(D, -1, -1) | ||
| valid = torch.zeros((D, L, L), dtype=torch.bool, device=Q.device) | ||
| valid.scatter_(2, indices, True) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For CUDA inference with Useful? React with 👍 / 👎. |
||
| bias = bias.to(dtype).masked_fill(~valid.unsqueeze(1), float("-inf")) | ||
|
|
||
| attn_out = F.scaled_dot_product_attention(q, k, v, attn_mask=bias) # (D, H, L, d) | ||
|
|
||
| if G is not None: | ||
| attn_out = attn_out * G.reshape(D, L, H, d).transpose(1, 2) | ||
|
|
||
| return attn_out.transpose(1, 2).reshape(D, L, c).contiguous() | ||
|
|
||
|
|
||
| def sparse_pairbias_attention( | ||
| Q, K, V, B, indices, H, gather_bias=True, G=None, full=False | ||
| ): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an engine with
compile_model=Trueis reused after leaving its context manager,BaseInferenceEngine.__exit__()discards the trainer/model and resetsinitialized_, butcompiled_remains true. The nextinitialize()constructs a fresh, uncompiled model and then skips_compile_diffusion_submodules(), silently disabling the requested optimization for every subsequent run on that engine; the compilation flag needs to be reset whenever the underlying model is torn down.Useful? React with 👍 / 👎.