Spaces:
Running on Zero
Running on Zero
Commit ·
d13df9d
1
Parent(s): 7002fe1
Integrate NVIDIA H3 Sol acceleration
Browse files- README.md +21 -9
- app.py +2 -1
- h3_nvfp4.py +185 -13
- requirements.txt +5 -0
README.md
CHANGED
|
@@ -24,9 +24,12 @@ Blackwell-native ComfyUI optimization path:
|
|
| 24 |
- Native CUDA 13 NVFP4 tensor-core GEMMs through `comfy-kitchen`.
|
| 25 |
- A local 15.7 GB Qwen3-VL NVFP4-AWQ conditioner replaces the normal cross-Space API call. It contains exactly the
|
| 26 |
first 50 language layers H3 reads, with the unused 14-layer tail and vocabulary head removed.
|
| 27 |
-
- The default 28-step Balanced schedule uses
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
| 30 |
- Segment-wise in-place AdaLN modulation and gated residual accumulation.
|
| 31 |
- Each block converts its complete AdaLN table in one launch instead of six, and the final RMSNorm runs only on
|
| 32 |
generated video/audio rows whose outputs are retained.
|
|
@@ -100,14 +103,15 @@ does not install or launch the ComfyUI application. The small adapter uses only
|
|
| 100 |
1. Dynamic NVFP4 activation quantization and native FP4 matrix multiplication.
|
| 101 |
2. Fused in-place Q/K RMSNorm and three-axis split-half rotary embedding.
|
| 102 |
|
| 103 |
-
Attention
|
| 104 |
-
|
| 105 |
-
projection.
|
| 106 |
|
| 107 |
Above that kernel path, Ultra Fast preserves the scheduler's 16-step trajectory but evaluates the full DiT only at
|
| 108 |
7 anchor steps. Between anchors it extrapolates the joint video/audio residual from the last two exact evaluations.
|
| 109 |
It keeps the first three and last two evaluations exact and never forecasts more than three consecutive steps.
|
| 110 |
-
Balanced
|
|
|
|
| 111 |
The result panel and Space log report actual DiT evaluations and forecasts for every request. Different prompts and
|
| 112 |
seeds never share this state.
|
| 113 |
|
|
@@ -146,8 +150,16 @@ exact original denoiser, set `H3_ENGINE=bf16`; this restores the 61.7 GiB unquan
|
|
| 146 |
| `H3_EASYCACHE_START` | `0.15` | Fraction of the schedule before which every step is evaluated. |
|
| 147 |
| `H3_EASYCACHE_END` | `0.95` | Fraction of the schedule after which every step is evaluated. |
|
| 148 |
| `H3_EASYCACHE_SUBSAMPLE` | `8` | Generated-video row stride used by the inexpensive change estimator. |
|
|
|
|
|
|
|
|
|
|
| 149 |
| `H3_FORECAST_BLEND` | `0.65` | Ultra Fast linear-trend strength; lower values stay closer to last-residual reuse. |
|
| 150 |
| `H3_FUSED_ADALN` | `0` | Opt in to fused AdaLN kernels; compilation is expensive on fresh ZeroGPU workers. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
| `H3_GPU_SIZE` | `xlarge` | 95 GiB Blackwell ZeroGPU allocation. |
|
| 152 |
| `H3_AOTI` | `0` | BF16 engine only: load the optional repeated-block AoTI package. |
|
| 153 |
|
|
@@ -168,7 +180,7 @@ This is an optimized derivative of the original
|
|
| 168 |
|
| 169 |
The fused/pruned model structure follows
|
| 170 |
[`comfy/ldm/minimax/model.py`](https://github.com/Comfy-Org/ComfyUI/blob/master/comfy/ldm/minimax/model.py) from
|
| 171 |
-
ComfyUI (Apache-2.0)
|
| 172 |
-
[`
|
| 173 |
[`lilcheaty/MiniMax-H3-NVFP4`](https://huggingface.co/lilcheaty/MiniMax-H3-NVFP4). MiniMax-H3 weights remain governed
|
| 174 |
by the MiniMax-H3 Community License Agreement.
|
|
|
|
| 24 |
- Native CUDA 13 NVFP4 tensor-core GEMMs through `comfy-kitchen`.
|
| 25 |
- A local 15.7 GB Qwen3-VL NVFP4-AWQ conditioner replaces the normal cross-Space API call. It contains exactly the
|
| 26 |
first 50 language layers H3 reads, with the unused 14-layer tail and vocabulary head removed.
|
| 27 |
+
- The default 28-step Balanced schedule uses NVIDIA Sol-Engine's H3 FirstBlockCache: block 1 is evaluated as a
|
| 28 |
+
high-signal change probe before deciding whether blocks 2–50 can reuse their previous joint residual. An optional
|
| 29 |
+
Ultra Fast mode forecasts between exact anchors, while Exact evaluates every requested step. This is not
|
| 30 |
+
prompt-to-video output caching.
|
| 31 |
+
- NVIDIA Sol-Attn's native SM120 kernel sparsifies target-video attention after a dense warmup while retaining text,
|
| 32 |
+
conditioning-video and generated-audio rows as an exact KV sink with dense prefix queries.
|
| 33 |
- Segment-wise in-place AdaLN modulation and gated residual accumulation.
|
| 34 |
- Each block converts its complete AdaLN table in one launch instead of six, and the final RMSNorm runs only on
|
| 35 |
generated video/audio rows whose outputs are retained.
|
|
|
|
| 103 |
1. Dynamic NVFP4 activation quantization and native FP4 matrix multiplication.
|
| 104 |
2. Fused in-place Q/K RMSNorm and three-axis split-half rotary embedding.
|
| 105 |
|
| 106 |
+
Attention uses NVIDIA Sol-Attn's SM120 CuTe kernel on eligible target-video calls and keeps diffusers'
|
| 107 |
+
`_native_cudnn` backend for the first ten denoising steps, the first two blocks, Exact mode, short sequences and any
|
| 108 |
+
safe fallback. The MLP uses one fused QKV-style gate/up matrix, in-place SiLU×up, and the NVFP4 down projection.
|
| 109 |
|
| 110 |
Above that kernel path, Ultra Fast preserves the scheduler's 16-step trajectory but evaluates the full DiT only at
|
| 111 |
7 anchor steps. Between anchors it extrapolates the joint video/audio residual from the last two exact evaluations.
|
| 112 |
It keeps the first three and last two evaluations exact and never forecasts more than three consecutive steps.
|
| 113 |
+
Balanced uses the official H3 FirstBlockCache signal at `0.08`, with three dense warmup and two dense tail steps;
|
| 114 |
+
Exact disables all block reuse and sparse attention.
|
| 115 |
The result panel and Space log report actual DiT evaluations and forecasts for every request. Different prompts and
|
| 116 |
seeds never share this state.
|
| 117 |
|
|
|
|
| 150 |
| `H3_EASYCACHE_START` | `0.15` | Fraction of the schedule before which every step is evaluated. |
|
| 151 |
| `H3_EASYCACHE_END` | `0.95` | Fraction of the schedule after which every step is evaluated. |
|
| 152 |
| `H3_EASYCACHE_SUBSAMPLE` | `8` | Generated-video row stride used by the inexpensive change estimator. |
|
| 153 |
+
| `H3_FIRST_BLOCK_THRESHOLD` | `0.08` | Balanced-mode normalized first-block residual threshold, matching NVIDIA's H3 preset. |
|
| 154 |
+
| `H3_FIRST_BLOCK_DENSE_START` | `3` | Dense warmup steps retained before FirstBlockCache may reuse blocks 2–50. |
|
| 155 |
+
| `H3_FIRST_BLOCK_DENSE_END` | `2` | Dense tail steps retained after FirstBlockCache reuse is disabled. |
|
| 156 |
| `H3_FORECAST_BLEND` | `0.65` | Ultra Fast linear-trend strength; lower values stay closer to last-residual reuse. |
|
| 157 |
| `H3_FUSED_ADALN` | `0` | Opt in to fused AdaLN kernels; compilation is expensive on fresh ZeroGPU workers. |
|
| 158 |
+
| `H3_SOL_ATTN` | `1` | Use NVIDIA Sol-Attn outside Exact mode, with automatic dense fallback. |
|
| 159 |
+
| `H3_SOL_ATTN_TAU` | `1.0` | Official H3 sparse-routing threshold. |
|
| 160 |
+
| `H3_SOL_ATTN_DENSE_STEPS` | `10` | Initial denoising steps kept on exact dense attention. |
|
| 161 |
+
| `H3_SOL_ATTN_DENSE_LAYERS` | `2` | Initial transformer blocks kept on exact dense attention. |
|
| 162 |
+
| `H3_SOL_ATTN_MIN_TOKENS` | `8192` | Skip sparse-kernel overhead on shorter packed sequences. |
|
| 163 |
| `H3_GPU_SIZE` | `xlarge` | 95 GiB Blackwell ZeroGPU allocation. |
|
| 164 |
| `H3_AOTI` | `0` | BF16 engine only: load the optional repeated-block AoTI package. |
|
| 165 |
|
|
|
|
| 180 |
|
| 181 |
The fused/pruned model structure follows
|
| 182 |
[`comfy/ldm/minimax/model.py`](https://github.com/Comfy-Org/ComfyUI/blob/master/comfy/ldm/minimax/model.py) from
|
| 183 |
+
ComfyUI (Apache-2.0). FirstBlockCache and Sol-Attn are adapted from NVIDIA's Apache-2.0
|
| 184 |
+
[`MiniMax-H3 Sol-Engine`](https://github.com/NVlabs/Sana/tree/sol-engine/models/minimax_h3) release. The quantized checkpoint and its conversion notes are from
|
| 185 |
[`lilcheaty/MiniMax-H3-NVFP4`](https://huggingface.co/lilcheaty/MiniMax-H3-NVFP4). MiniMax-H3 weights remain governed
|
| 186 |
by the MiniMax-H3 Community License Agreement.
|
app.py
CHANGED
|
@@ -429,7 +429,8 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
|
|
| 429 |
|
| 430 |
report = (
|
| 431 |
f"`{width}x{height}`, {num_frames} frames ({num_frames / FPS:.3f} s), {int(steps)} scheduler steps · "
|
| 432 |
-
f"{cache_stats['computed']} DiT evaluations + {cache_stats['forecasted']}
|
|
|
|
| 433 |
f"conditioner {condition_seconds:.0f}s ({num_text_tokens} tokens"
|
| 434 |
f"{', upsampled' if refined else ''}) · "
|
| 435 |
f"denoise + decode {denoise_seconds:.0f}s ({denoise_seconds / int(steps):.1f} s/step) · seed {int(seed)}"
|
|
|
|
| 429 |
|
| 430 |
report = (
|
| 431 |
f"`{width}x{height}`, {num_frames} frames ({num_frames / FPS:.3f} s), {int(steps)} scheduler steps · "
|
| 432 |
+
f"{cache_stats['computed']} full DiT evaluations + {cache_stats['forecasted']} cached block-stack reuses "
|
| 433 |
+
f"({acceleration}) · Sol-Attn {cache_stats.get('sol_sparse_calls', 0)} sparse calls · "
|
| 434 |
f"conditioner {condition_seconds:.0f}s ({num_text_tokens} tokens"
|
| 435 |
f"{', upsampled' if refined else ''}) · "
|
| 436 |
f"denoise + decode {denoise_seconds:.0f}s ({denoise_seconds / int(steps):.1f} s/step) · seed {int(seed)}"
|
h3_nvfp4.py
CHANGED
|
@@ -54,8 +54,16 @@ EASYCACHE_THRESHOLD = max(0.0, float(os.environ.get("H3_EASYCACHE_THRESHOLD", "0
|
|
| 54 |
EASYCACHE_START = min(1.0, max(0.0, float(os.environ.get("H3_EASYCACHE_START", "0.15"))))
|
| 55 |
EASYCACHE_END = min(1.0, max(EASYCACHE_START, float(os.environ.get("H3_EASYCACHE_END", "0.95"))))
|
| 56 |
EASYCACHE_SUBSAMPLE = max(1, int(os.environ.get("H3_EASYCACHE_SUBSAMPLE", "8")))
|
|
|
|
|
|
|
|
|
|
| 57 |
FORECAST_BLEND = min(1.0, max(0.0, float(os.environ.get("H3_FORECAST_BLEND", "0.65"))))
|
| 58 |
FUSED_ADALN = os.environ.get("H3_FUSED_ADALN", "0") == "1" and triton is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
|
| 61 |
if triton is not None:
|
|
@@ -117,6 +125,9 @@ class H3StepCache:
|
|
| 117 |
self.pending_input = None
|
| 118 |
self.pending_input_change = None
|
| 119 |
self.pending_track = False
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
def begin(self, total_steps: int | None, profile: str = "balanced") -> None:
|
| 122 |
self.__init__()
|
|
@@ -146,6 +157,11 @@ class H3StepCache:
|
|
| 146 |
if not self.enabled:
|
| 147 |
return None
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
# Ultra Fast is deliberately bounded: no more than three forecasts can separate exact transformer calls, and
|
| 150 |
# the high-noise warmup plus low-noise tail remain exact. At the default 16 steps this executes 7 full DiT
|
| 151 |
# evaluations instead of 16 while still sampling the original 16-step scheduler trajectory.
|
|
@@ -202,7 +218,58 @@ class H3StepCache:
|
|
| 202 |
return video_input + self.video_residual, audio_input + self.audio_residual
|
| 203 |
return None
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
def update(self, video_input, audio_input, video_output, audio_output, condition_rows: int) -> None:
|
|
|
|
|
|
|
|
|
|
| 206 |
if self.pending_track:
|
| 207 |
sampled_output = video_output[0, condition_rows::EASYCACHE_SUBSAMPLE].detach().float()
|
| 208 |
if self.previous_output is not None and self.pending_input_change is not None:
|
|
@@ -249,6 +316,83 @@ class H3StepCache:
|
|
| 249 |
return stats
|
| 250 |
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
def _quant_config(handle, prefix: str) -> dict | None:
|
| 253 |
key = f"{prefix}.comfy_quant"
|
| 254 |
if key not in handle.keys():
|
|
@@ -378,7 +522,7 @@ class H3Attention(nn.Module):
|
|
| 378 |
self.k_norm.load(handle, f"{prefix}.k_norm")
|
| 379 |
self.out_proj.load(handle, f"{prefix}.out_proj")
|
| 380 |
|
| 381 |
-
def forward(self, hidden_states, rope_table, backend: str):
|
| 382 |
sequence = hidden_states.shape[0]
|
| 383 |
qkv = self.qkv_proj(hidden_states)
|
| 384 |
query, key, value = qkv.split(HEADS * HEAD_DIM, dim=-1)
|
|
@@ -396,15 +540,17 @@ class H3Attention(nn.Module):
|
|
| 396 |
epsilon=self.q_norm.eps,
|
| 397 |
rot_dim=rope_table.shape[-3] * 2,
|
| 398 |
)
|
| 399 |
-
attended =
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
|
|
|
|
|
|
| 408 |
return self.out_proj(attended.reshape(sequence, HEADS * HEAD_DIM))
|
| 409 |
|
| 410 |
|
|
@@ -525,6 +671,7 @@ class H3NVFP4Transformer(nn.Module):
|
|
| 525 |
self._output_indices = None
|
| 526 |
self._generated_rows = None
|
| 527 |
self._step_cache = H3StepCache()
|
|
|
|
| 528 |
|
| 529 |
@property
|
| 530 |
def dtype(self) -> torch.dtype:
|
|
@@ -566,9 +713,13 @@ class H3NVFP4Transformer(nn.Module):
|
|
| 566 |
self._output_indices = None
|
| 567 |
self._generated_rows = None
|
| 568 |
self._step_cache.begin(total_steps, profile)
|
|
|
|
| 569 |
|
| 570 |
def end_request(self) -> dict:
|
| 571 |
stats = self._step_cache.finish()
|
|
|
|
|
|
|
|
|
|
| 572 |
self._text_cache = None
|
| 573 |
self._rope_cache = None
|
| 574 |
self._segment_cache = None
|
|
@@ -719,15 +870,28 @@ class H3NVFP4Transformer(nn.Module):
|
|
| 719 |
adaln_indices = timestep_indices * 3 + token_tags.clamp(min=0)
|
| 720 |
segments = self._segments(adaln_indices)
|
| 721 |
rope = self._rope(position_ids, packed.dtype)
|
| 722 |
-
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
# One conversion per small modulation table, rather than one conversion per sequence segment.
|
| 725 |
modulations = block.adaln_proj(time_embedding, packed.dtype)
|
| 726 |
shift_attn, scale_attn, gate_attn, shift_mlp, scale_mlp, gate_mlp = modulations
|
| 727 |
normalized = self._modulate(block.norm1(packed), shift_attn, scale_attn, adaln_indices, segments)
|
| 728 |
packed = self._gate(
|
| 729 |
packed,
|
| 730 |
-
block.attn(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
gate_attn,
|
| 732 |
adaln_indices,
|
| 733 |
segments,
|
|
@@ -735,6 +899,14 @@ class H3NVFP4Transformer(nn.Module):
|
|
| 735 |
normalized = self._modulate(block.norm2(packed), shift_mlp, scale_mlp, adaln_indices, segments)
|
| 736 |
packed = self._gate(packed, block.mlp(normalized), gate_mlp, adaln_indices, segments)
|
| 737 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
shift, scale = self.final_layer.adaln_proj(time_embedding)
|
| 739 |
|
| 740 |
# Keyframe output rows are discarded by the scheduler. Avoid their FP32 output projection and put zeros in
|
|
|
|
| 54 |
EASYCACHE_START = min(1.0, max(0.0, float(os.environ.get("H3_EASYCACHE_START", "0.15"))))
|
| 55 |
EASYCACHE_END = min(1.0, max(EASYCACHE_START, float(os.environ.get("H3_EASYCACHE_END", "0.95"))))
|
| 56 |
EASYCACHE_SUBSAMPLE = max(1, int(os.environ.get("H3_EASYCACHE_SUBSAMPLE", "8")))
|
| 57 |
+
FIRST_BLOCK_THRESHOLD = max(0.0, float(os.environ.get("H3_FIRST_BLOCK_THRESHOLD", "0.08")))
|
| 58 |
+
FIRST_BLOCK_DENSE_START = max(1, int(os.environ.get("H3_FIRST_BLOCK_DENSE_START", "3")))
|
| 59 |
+
FIRST_BLOCK_DENSE_END = max(1, int(os.environ.get("H3_FIRST_BLOCK_DENSE_END", "2")))
|
| 60 |
FORECAST_BLEND = min(1.0, max(0.0, float(os.environ.get("H3_FORECAST_BLEND", "0.65"))))
|
| 61 |
FUSED_ADALN = os.environ.get("H3_FUSED_ADALN", "0") == "1" and triton is not None
|
| 62 |
+
SOL_ATTN = os.environ.get("H3_SOL_ATTN", "1") == "1"
|
| 63 |
+
SOL_ATTN_TAU = float(os.environ.get("H3_SOL_ATTN_TAU", "1.0"))
|
| 64 |
+
SOL_ATTN_DENSE_STEPS = max(0, int(os.environ.get("H3_SOL_ATTN_DENSE_STEPS", "10")))
|
| 65 |
+
SOL_ATTN_DENSE_LAYERS = max(0, int(os.environ.get("H3_SOL_ATTN_DENSE_LAYERS", "2")))
|
| 66 |
+
SOL_ATTN_MIN_TOKENS = max(0, int(os.environ.get("H3_SOL_ATTN_MIN_TOKENS", "8192")))
|
| 67 |
|
| 68 |
|
| 69 |
if triton is not None:
|
|
|
|
| 125 |
self.pending_input = None
|
| 126 |
self.pending_input_change = None
|
| 127 |
self.pending_track = False
|
| 128 |
+
self.head_residual = None
|
| 129 |
+
self.tail_residual = None
|
| 130 |
+
self.first_block_output = None
|
| 131 |
|
| 132 |
def begin(self, total_steps: int | None, profile: str = "balanced") -> None:
|
| 133 |
self.__init__()
|
|
|
|
| 157 |
if not self.enabled:
|
| 158 |
return None
|
| 159 |
|
| 160 |
+
# Balanced uses NVIDIA's H3 FirstBlockCache below, after block 0 has produced a high-signal residual.
|
| 161 |
+
# Only the deliberately aggressive Ultra profile forecasts a whole transformer call before block 0.
|
| 162 |
+
if not self.profile.startswith("ultra"):
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
# Ultra Fast is deliberately bounded: no more than three forecasts can separate exact transformer calls, and
|
| 166 |
# the high-noise warmup plus low-noise tail remain exact. At the default 16 steps this executes 7 full DiT
|
| 167 |
# evaluations instead of 16 while still sampling the original 16-step scheduler trajectory.
|
|
|
|
| 218 |
return video_input + self.video_residual, audio_input + self.audio_residual
|
| 219 |
return None
|
| 220 |
|
| 221 |
+
def first_block_decision(self, block_input: torch.Tensor, block_output: torch.Tensor) -> bool:
|
| 222 |
+
"""Return True when blocks 1..49 can reuse their previous joint residual.
|
| 223 |
+
|
| 224 |
+
This is the single-GPU equivalent of NVIDIA Sol-Engine's H3 FirstBlockCache at threshold 0.08. The first
|
| 225 |
+
block is always evaluated. Its normalized residual change is a much stronger predictor than raw latent
|
| 226 |
+
motion, while the cached tail residual still covers the complete text/video/audio packed sequence.
|
| 227 |
+
"""
|
| 228 |
+
if not self.enabled or self.profile.startswith("ultra"):
|
| 229 |
+
return False
|
| 230 |
+
if FIRST_BLOCK_THRESHOLD <= 0.0:
|
| 231 |
+
self.first_block_output = block_output.detach().clone()
|
| 232 |
+
return False
|
| 233 |
+
|
| 234 |
+
keep_dense = self.step < FIRST_BLOCK_DENSE_START or self.step >= self.total_steps - FIRST_BLOCK_DENSE_END
|
| 235 |
+
residual = block_output - block_input
|
| 236 |
+
reusable = (
|
| 237 |
+
not keep_dense
|
| 238 |
+
and self.head_residual is not None
|
| 239 |
+
and self.tail_residual is not None
|
| 240 |
+
and self.tail_residual.shape == block_output.shape
|
| 241 |
+
)
|
| 242 |
+
should_reuse = False
|
| 243 |
+
if reusable:
|
| 244 |
+
difference = (residual - self.head_residual).abs().mean()
|
| 245 |
+
reference = self.head_residual.abs().mean().clamp_min(1e-8)
|
| 246 |
+
should_reuse = bool(((difference / reference) <= FIRST_BLOCK_THRESHOLD).item())
|
| 247 |
+
|
| 248 |
+
if should_reuse:
|
| 249 |
+
self.skipped += 1
|
| 250 |
+
self.consecutive_skips += 1
|
| 251 |
+
self.step += 1
|
| 252 |
+
return True
|
| 253 |
+
|
| 254 |
+
# This engine's residual/gate operations update `packed` in place. Preserve the head output before later
|
| 255 |
+
# blocks mutate the same storage; diffusers' reference blocks are out-of-place and do not need this clone.
|
| 256 |
+
self.first_block_output = block_output.detach().clone()
|
| 257 |
+
self.head_residual = residual.detach()
|
| 258 |
+
return False
|
| 259 |
+
|
| 260 |
+
def update_first_block_tail(self, final_block_output: torch.Tensor) -> None:
|
| 261 |
+
if self.first_block_output is None:
|
| 262 |
+
return
|
| 263 |
+
self.tail_residual = (final_block_output - self.first_block_output).detach()
|
| 264 |
+
self.last_actual_step = self.step
|
| 265 |
+
self.consecutive_skips = 0
|
| 266 |
+
self.step += 1
|
| 267 |
+
self.first_block_output = None
|
| 268 |
+
|
| 269 |
def update(self, video_input, audio_input, video_output, audio_output, condition_rows: int) -> None:
|
| 270 |
+
# Balanced's clock and state are updated at the block-stack boundary by FirstBlockCache.
|
| 271 |
+
if self.enabled and not self.profile.startswith("ultra"):
|
| 272 |
+
return
|
| 273 |
if self.pending_track:
|
| 274 |
sampled_output = video_output[0, condition_rows::EASYCACHE_SUBSAMPLE].detach().float()
|
| 275 |
if self.previous_output is not None and self.pending_input_change is not None:
|
|
|
|
| 316 |
return stats
|
| 317 |
|
| 318 |
|
| 319 |
+
class H3SolAttention:
|
| 320 |
+
"""NVIDIA Sol-Attn policy adapted to H3's single-GPU packed attention.
|
| 321 |
+
|
| 322 |
+
The packed prefix (text, conditioning video and generated audio) remains an exact KV sink and its query rows are
|
| 323 |
+
recomputed densely. Only target-video query/key interactions become sparse, after ten dense denoising steps and
|
| 324 |
+
outside the first two transformer blocks. Any unavailable/JIT-failing backend falls back to cuDNN for the request.
|
| 325 |
+
"""
|
| 326 |
+
|
| 327 |
+
def __init__(self):
|
| 328 |
+
self.enabled = SOL_ATTN
|
| 329 |
+
self.step = 0
|
| 330 |
+
self.video_start = 0
|
| 331 |
+
self.sparse_calls = 0
|
| 332 |
+
self.dense_calls = 0
|
| 333 |
+
self.failure = None
|
| 334 |
+
|
| 335 |
+
def begin(self):
|
| 336 |
+
self.step = 0
|
| 337 |
+
self.video_start = 0
|
| 338 |
+
self.sparse_calls = 0
|
| 339 |
+
self.dense_calls = 0
|
| 340 |
+
self.failure = None
|
| 341 |
+
|
| 342 |
+
def observe(self, video_indices: torch.Tensor, sequence: int, step: int) -> None:
|
| 343 |
+
self.step = int(step)
|
| 344 |
+
if not self.video_start:
|
| 345 |
+
deltas = video_indices[1:] - video_indices[:-1]
|
| 346 |
+
breaks = (deltas != 1).nonzero().flatten()
|
| 347 |
+
start = int(breaks[-1]) + 1 if len(breaks) else 0
|
| 348 |
+
self.video_start = int(video_indices[start]) if video_indices.numel() else sequence
|
| 349 |
+
|
| 350 |
+
def __call__(self, query, key, value, layer: int):
|
| 351 |
+
tokens = int(query.shape[1])
|
| 352 |
+
if (
|
| 353 |
+
not self.enabled
|
| 354 |
+
or self.failure is not None
|
| 355 |
+
or self.step < SOL_ATTN_DENSE_STEPS
|
| 356 |
+
or layer < SOL_ATTN_DENSE_LAYERS
|
| 357 |
+
or tokens < SOL_ATTN_MIN_TOKENS
|
| 358 |
+
or not 0 < self.video_start < tokens
|
| 359 |
+
):
|
| 360 |
+
self.dense_calls += 1
|
| 361 |
+
return None
|
| 362 |
+
try:
|
| 363 |
+
from sol_attn import sol_attn
|
| 364 |
+
|
| 365 |
+
q, k, v = (tensor.contiguous() for tensor in (query, key, value))
|
| 366 |
+
attended = sol_attn(
|
| 367 |
+
q,
|
| 368 |
+
k,
|
| 369 |
+
v,
|
| 370 |
+
tau=SOL_ATTN_TAU,
|
| 371 |
+
thresh_type="diag",
|
| 372 |
+
kv_splits=1,
|
| 373 |
+
sink_start=0,
|
| 374 |
+
sink_tokens=self.video_start,
|
| 375 |
+
)
|
| 376 |
+
# An exact KV sink does not make the prefix's own queries dense. H3 jointly generates audio in that
|
| 377 |
+
# prefix, so reproduce those rows with exact attention as NVIDIA's H3 integration does.
|
| 378 |
+
prefix = self.video_start
|
| 379 |
+
dense_prefix = F.scaled_dot_product_attention(
|
| 380 |
+
q[:, :prefix].transpose(1, 2),
|
| 381 |
+
k.transpose(1, 2),
|
| 382 |
+
v.transpose(1, 2),
|
| 383 |
+
dropout_p=0.0,
|
| 384 |
+
is_causal=False,
|
| 385 |
+
).transpose(1, 2)
|
| 386 |
+
attended[:, :prefix] = dense_prefix
|
| 387 |
+
self.sparse_calls += 1
|
| 388 |
+
return attended
|
| 389 |
+
except Exception as error:
|
| 390 |
+
self.failure = f"{type(error).__name__}: {error}"
|
| 391 |
+
print(f"[h3-sol-attn] falling back to dense attention: {self.failure}", flush=True)
|
| 392 |
+
self.dense_calls += 1
|
| 393 |
+
return None
|
| 394 |
+
|
| 395 |
+
|
| 396 |
def _quant_config(handle, prefix: str) -> dict | None:
|
| 397 |
key = f"{prefix}.comfy_quant"
|
| 398 |
if key not in handle.keys():
|
|
|
|
| 522 |
self.k_norm.load(handle, f"{prefix}.k_norm")
|
| 523 |
self.out_proj.load(handle, f"{prefix}.out_proj")
|
| 524 |
|
| 525 |
+
def forward(self, hidden_states, rope_table, backend: str, sparse=None, layer: int = -1):
|
| 526 |
sequence = hidden_states.shape[0]
|
| 527 |
qkv = self.qkv_proj(hidden_states)
|
| 528 |
query, key, value = qkv.split(HEADS * HEAD_DIM, dim=-1)
|
|
|
|
| 540 |
epsilon=self.q_norm.eps,
|
| 541 |
rot_dim=rope_table.shape[-3] * 2,
|
| 542 |
)
|
| 543 |
+
attended = sparse(query, key, value, layer) if sparse is not None else None
|
| 544 |
+
if attended is None:
|
| 545 |
+
attended = dispatch_attention_fn(
|
| 546 |
+
query,
|
| 547 |
+
key,
|
| 548 |
+
value,
|
| 549 |
+
attn_mask=None,
|
| 550 |
+
dropout_p=0.0,
|
| 551 |
+
is_causal=False,
|
| 552 |
+
backend=backend,
|
| 553 |
+
)
|
| 554 |
return self.out_proj(attended.reshape(sequence, HEADS * HEAD_DIM))
|
| 555 |
|
| 556 |
|
|
|
|
| 671 |
self._output_indices = None
|
| 672 |
self._generated_rows = None
|
| 673 |
self._step_cache = H3StepCache()
|
| 674 |
+
self._sol_attention = H3SolAttention()
|
| 675 |
|
| 676 |
@property
|
| 677 |
def dtype(self) -> torch.dtype:
|
|
|
|
| 713 |
self._output_indices = None
|
| 714 |
self._generated_rows = None
|
| 715 |
self._step_cache.begin(total_steps, profile)
|
| 716 |
+
self._sol_attention.begin()
|
| 717 |
|
| 718 |
def end_request(self) -> dict:
|
| 719 |
stats = self._step_cache.finish()
|
| 720 |
+
stats["sol_sparse_calls"] = self._sol_attention.sparse_calls
|
| 721 |
+
stats["sol_dense_calls"] = self._sol_attention.dense_calls
|
| 722 |
+
stats["sol_failure"] = self._sol_attention.failure
|
| 723 |
self._text_cache = None
|
| 724 |
self._rope_cache = None
|
| 725 |
self._segment_cache = None
|
|
|
|
| 870 |
adaln_indices = timestep_indices * 3 + token_tags.clamp(min=0)
|
| 871 |
segments = self._segments(adaln_indices)
|
| 872 |
rope = self._rope(position_ids, packed.dtype)
|
| 873 |
+
use_sol_attention = self._step_cache.profile != "exact" and self._sol_attention.enabled
|
| 874 |
+
self._sol_attention.observe(video_indices, packed.shape[0], self._step_cache.step)
|
| 875 |
+
|
| 876 |
+
reused_tail = False
|
| 877 |
+
for layer, block in enumerate(self.blocks):
|
| 878 |
+
if layer == 0:
|
| 879 |
+
# Block 0 writes its residual updates in place, so retain the pre-block value for the official FBC
|
| 880 |
+
# signal `(head_output - head_input)`.
|
| 881 |
+
block_input = packed.detach().clone()
|
| 882 |
# One conversion per small modulation table, rather than one conversion per sequence segment.
|
| 883 |
modulations = block.adaln_proj(time_embedding, packed.dtype)
|
| 884 |
shift_attn, scale_attn, gate_attn, shift_mlp, scale_mlp, gate_mlp = modulations
|
| 885 |
normalized = self._modulate(block.norm1(packed), shift_attn, scale_attn, adaln_indices, segments)
|
| 886 |
packed = self._gate(
|
| 887 |
packed,
|
| 888 |
+
block.attn(
|
| 889 |
+
normalized,
|
| 890 |
+
rope,
|
| 891 |
+
self.attention_backend,
|
| 892 |
+
self._sol_attention if use_sol_attention else None,
|
| 893 |
+
layer,
|
| 894 |
+
),
|
| 895 |
gate_attn,
|
| 896 |
adaln_indices,
|
| 897 |
segments,
|
|
|
|
| 899 |
normalized = self._modulate(block.norm2(packed), shift_mlp, scale_mlp, adaln_indices, segments)
|
| 900 |
packed = self._gate(packed, block.mlp(normalized), gate_mlp, adaln_indices, segments)
|
| 901 |
|
| 902 |
+
if layer == 0:
|
| 903 |
+
if self._step_cache.first_block_decision(block_input, packed):
|
| 904 |
+
packed = packed + self._step_cache.tail_residual
|
| 905 |
+
reused_tail = True
|
| 906 |
+
break
|
| 907 |
+
if layer == len(self.blocks) - 1 and not reused_tail:
|
| 908 |
+
self._step_cache.update_first_block_tail(packed)
|
| 909 |
+
|
| 910 |
shift, scale = self.final_layer.adaln_proj(time_embedding)
|
| 911 |
|
| 912 |
# Keyframe output rows are discarded by the scheduler. Avoid their FP32 output projection and put zeros in
|
requirements.txt
CHANGED
|
@@ -19,6 +19,11 @@ spaces==0.51.1
|
|
| 19 |
# Blackwell-native NVFP4 GEMMs and the fused Q/K RMSNorm + split-half RoPE kernel used by h3_nvfp4.py.
|
| 20 |
# CUDA 13 is mandatory: older builds emulate this path and are slower than BF16.
|
| 21 |
comfy-kitchen==0.2.26
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# No `kernels` pin on purpose: the Hub attention backends want `kernels>=0.12.3`, and that version breaks
|
| 23 |
# transformers 5.8.0 at import.
|
| 24 |
# PyAV muxes the generated soundtrack onto the frames (`encode_video`).
|
|
|
|
| 19 |
# Blackwell-native NVFP4 GEMMs and the fused Q/K RMSNorm + split-half RoPE kernel used by h3_nvfp4.py.
|
| 20 |
# CUDA 13 is mandatory: older builds emulate this path and are slower than BF16.
|
| 21 |
comfy-kitchen==0.2.26
|
| 22 |
+
# NVIDIA Sol-Engine's released sparse-attention package, pinned to the H3 release. The native SM120 path uses
|
| 23 |
+
# CuTe DSL; the package retains its portable Triton fallback if that optional backend cannot initialize.
|
| 24 |
+
sol-attn @ git+https://github.com/NVlabs/Sana.git@46031940ba8af5d18054217e571149579424c0b1#subdirectory=techniques/sparse_backends
|
| 25 |
+
nvidia-cutlass-dsl>=4.5.0
|
| 26 |
+
cuda-python>=13.0
|
| 27 |
# No `kernels` pin on purpose: the Hub attention backends want `kernels>=0.12.3`, and that version breaks
|
| 28 |
# transformers 5.8.0 at import.
|
| 29 |
# PyAV muxes the generated soundtrack onto the frames (`encode_video`).
|