mrfakename commited on
Commit
d13df9d
·
1 Parent(s): 7002fe1

Integrate NVIDIA H3 Sol acceleration

Browse files
Files changed (4) hide show
  1. README.md +21 -9
  2. app.py +2 -1
  3. h3_nvfp4.py +185 -13
  4. 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 conservative, request-local adaptive residual reuse. An optional Ultra
28
- Fast mode forecasts between exact anchors, while Exact evaluates every requested step. This is not prompt-to-video
29
- output caching.
 
 
 
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 itself stays on diffusers' `_native_cudnn` backend, which is faster than the default SDPA path on the
104
- ZeroGPU RTX PRO 6000 pool. The MLP uses one fused QKV-style gate/up matrix, in-place SiLU×up, and the NVFP4 down
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 retains the conservative EasyCache-style latent-change estimator at `0.10`; Exact disables all step reuse.
 
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), and the adaptive residual estimator follows ComfyUI's built-in
172
- [`EasyCache`](https://github.com/Comfy-Org/ComfyUI/blob/master/comfy_extras/nodes_easycache.py) design. The quantized checkpoint and its conversion notes are from
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']} forecasts ({acceleration}) · "
 
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 = dispatch_attention_fn(
400
- query,
401
- key,
402
- value,
403
- attn_mask=None,
404
- dropout_p=0.0,
405
- is_causal=False,
406
- backend=backend,
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
- for block in self.blocks:
 
 
 
 
 
 
 
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(normalized, rope, self.attention_backend),
 
 
 
 
 
 
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`).