Instructions to use FastVideo/FastH3-Omni-Comfy with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusion Single File
How to use FastVideo/FastH3-Omni-Comfy with Diffusion Single File:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Download conversion/upstream/comfy_model.py from FastVideo/FastH3-Omni-Comfy: direct link, hf CLI and curl.
- Browser
- Download file 41.1 kB
-
https://huggingface.co/FastVideo/FastH3-Omni-Comfy/resolve/main/conversion/upstream/comfy_model.py
- Command line
-
hf download hf://FastVideo/FastH3-Omni-Comfy/conversion/upstream/comfy_model.py
-
curl -L -o comfy_model.py https://huggingface.co/FastVideo/FastH3-Omni-Comfy/resolve/main/conversion/upstream/comfy_model.py
41.1 kB
| """MiniMax H3 audio-video DiT. | |
| Single-stream packed-token transformer denoising video (24ch, patch 1x2x2) and | |
| stereo audio (32ch, 40 Hz) latents jointly, conditioned on Qwen3-VL layer-50 hidden states. | |
| The packed sequence is: | |
| [text | cond rows | audio | video] for t2va/fl2va | |
| [text | reference blocks | audio | video] for ref2va | |
| Timestep domain: the model receives the *video* sigma from the sampler and | |
| derives per-token timesteps t = 1 - sigma internally; the audio stream runs on | |
| its own shifted schedule (sigma_shift video 12.0 / audio 3.0), mapped from the | |
| video sigma in closed form. The sampler carries the audio latent scaled onto the | |
| video schedule (ModelSamplingAV); forward() undoes that scale and converts the | |
| velocity back, so _forward only ever sees the stream's own latent. | |
| """ | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import comfy.ldm.common_dit | |
| import comfy.model_management | |
| import comfy.model_prefetch | |
| import comfy.ops | |
| import comfy.patcher_extension | |
| import comfy.quant_ops | |
| from comfy.ldm.modules.attention import ComfyAttention, AttentionTensorContainer, optimized_attention | |
| FRAME_PER_TOKEN = (1, 4, 4, 4, 4) | |
| FRAME_RESCALE = 5.0 / 3.0 | |
| VISUAL_COND_TIMESTEP = 0.999 | |
| AUDIO_COND_TIMESTEP = 1.0 | |
| def time_shift_sigma(sigma, from_shift, to_shift): | |
| # invert sigma = s*b/(1+(s-1)*b) to the base grid, re-apply the other shift | |
| base = sigma / (from_shift + sigma * (1.0 - from_shift)) | |
| return to_shift * base / (1.0 + (to_shift - 1.0) * base) | |
| def patchify_video(latent, patch_size=(1, 2, 2)): | |
| # [B, C, T, H, W] -> [B*t*h*w, C*pt*ph*pw] | |
| b, c, t_full, h_full, w_full = latent.shape | |
| pt, ph, pw = patch_size | |
| t, h, w = t_full // pt, h_full // ph, w_full // pw | |
| x = latent.reshape(b, c, t, pt, h, ph, w, pw) | |
| x = torch.einsum("nctrhpwq->nthwcrpq", x) | |
| return x.reshape(b * t * h * w, c * pt * ph * pw) | |
| def unpatchify_video(rows, t, h, w, c=24, patch_size=(1, 2, 2)): | |
| pt, ph, pw = patch_size | |
| x = rows.reshape(-1, t, h, w, c, pt, ph, pw) | |
| x = torch.einsum("nthwcrpq->nctrhpwq", x) | |
| return x.reshape(-1, c, t * pt, h * ph, w * pw) | |
| def pack_audio(latent): | |
| # [B, C=32, ch=2, T] -> [ch*T, 32] channel-major (ch0 t0..T-1, ch1 t0..T-1) | |
| b, c, ch, t = latent.shape | |
| return latent[0].permute(1, 2, 0).reshape(ch * t, c) | |
| def unpack_audio(rows, ch=2): | |
| t = rows.shape[0] // ch | |
| return rows.reshape(ch, t, rows.shape[-1]).permute(2, 0, 1).unsqueeze(0) | |
| def _axis_from_sqrt_area(dim, patch, sqrt_area): | |
| # linspace((1 - ratio) / 2, (1 + ratio) / 2, dim // patch, endpoint=False) * 32 | |
| ratio = dim / sqrt_area | |
| n = dim // patch | |
| return (torch.arange(n, dtype=torch.float64) * (ratio / n) + (1.0 - ratio) / 2.0) * 32.0 | |
| def mask_row_values(mask, latent_t, lat_h, lat_w): | |
| # [T, H, W] denoise mask (1 = generate) -> per-2x2-patch-row float in [0, 1], | |
| # None when every row fully generates | |
| m = torch.nn.functional.pad(mask, (0, lat_w - mask.shape[-1], 0, lat_h - mask.shape[-2]), mode="replicate") | |
| m = m.reshape(latent_t, lat_h // 2, 2, lat_w // 2, 2).amax(dim=(2, 4)) | |
| values = m.reshape(-1) | |
| if bool((values >= 1.0 - 1e-3).all()): | |
| return None | |
| return values | |
| def _frame_grid(h, w): | |
| # area-normalized (h, w) coordinates of one latent frame's 2x2-patch rows | |
| area = math.sqrt(h * w) | |
| hh, ww = torch.meshgrid(_axis_from_sqrt_area(h, 2, area), _axis_from_sqrt_area(w, 2, area), indexing="ij") | |
| return torch.stack([hh.reshape(-1), ww.reshape(-1)], dim=-1), _axis_from_sqrt_area(w, 2, area) | |
| def _video_t_spans(n): | |
| return [FRAME_RESCALE * FRAME_PER_TOKEN[k % 5] for k in range(n)] | |
| def _video_t_grid(n, origin): | |
| # origin + exclusive cumsum | |
| spans = torch.tensor(_video_t_spans(n), dtype=torch.float64) | |
| return float(origin) + torch.cat([torch.zeros(1, dtype=torch.float64), spans[:-1].cumsum(0)]) | |
| def _ref_t_span(blk): | |
| # time-axis span a reference block occupies ahead of the target streams | |
| kind = blk["kind"] | |
| if kind == "image": | |
| return 1.0 | |
| if kind == "audio": | |
| return float(blk["ref_audio_t"]) | |
| if kind in ("video", "video_audio"): | |
| return max(float(blk["ref_audio_t"]), sum(_video_t_spans(blk["latent_t"]))) | |
| return 0.0 | |
| def _audio_grid(cursor, t, w_low, w_high): | |
| # channel-major stereo rows: t advances per latent frame, w pinned to the grid extremes per stereo channel, h stays 0 | |
| g = torch.zeros(t * 2, 3, dtype=torch.float64) | |
| g[:, 0] = (cursor + torch.arange(t, dtype=torch.float64)).repeat(2) | |
| g[:t, 2] = w_low | |
| g[t:, 2] = w_high | |
| return g | |
| def _video_grid(vt, frame, cursor): | |
| g = torch.empty(vt, frame.shape[0], 3, dtype=torch.float64) | |
| g[:, :, 0] = _video_t_grid(vt, cursor)[:, None] | |
| g[:, :, 1:] = frame[None] | |
| return g.reshape(-1, 3) | |
| class TimeEmbedder(nn.Module): | |
| def __init__(self, freq_dim, hidden, out, dtype=None, device=None, operations=None): | |
| super().__init__() | |
| self.freq_dim = freq_dim | |
| self.proj_in = operations.Linear(freq_dim, hidden, bias=True, dtype=dtype, device=device) | |
| self.proj_out = operations.Linear(hidden, out, bias=True, dtype=dtype, device=device) | |
| def forward(self, t): | |
| # t: [M] in [0, 1]; fp32 throughout, cos before sin | |
| half = self.freq_dim // 2 | |
| freqs = torch.exp(-math.log(10000.0) * torch.arange(half, dtype=torch.float32, device=t.device) / half) | |
| args = t.to(torch.float32)[:, None] * freqs[None] | |
| emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) | |
| return self.proj_out(nn.functional.silu(self.proj_in(emb))) | |
| def rope_rotation_table(angles, dtype): | |
| """[S, rot_dim] pair angles -> [1, S, 1, rot_dim/2, 2, 2] rotation matrices.""" | |
| half = angles.shape[-1] // 2 | |
| ang = angles[:, :half] # duplicated halves: [:, :half] == [:, half:] | |
| c, s = torch.cos(ang), torch.sin(ang) | |
| table = torch.stack([c, -s, s, c], dim=-1).reshape(1, angles.shape[0], 1, half, 2, 2) | |
| return table.to(dtype) | |
| class Attention(nn.Module): | |
| def __init__(self, hidden, heads, head_dim, eps, gate_compress=False, dtype=None, device=None, operations=None): | |
| super().__init__() | |
| self.comfy_attention = ComfyAttention() | |
| self.heads = heads | |
| self.head_dim = head_dim | |
| inner = heads * head_dim | |
| self.qkv_proj = operations.Linear(hidden, inner * 3, bias=False, dtype=dtype, device=device) | |
| self.q_norm = operations.RMSNorm(head_dim, eps=eps, dtype=dtype, device=device) | |
| self.k_norm = operations.RMSNorm(head_dim, eps=eps, dtype=dtype, device=device) | |
| self.out_proj = operations.Linear(inner, hidden, bias=False, dtype=dtype, device=device) | |
| self.to_gate_compress = None | |
| if gate_compress: | |
| # VSA gate, unused by the dense forward; consumed by sparse attention patches | |
| self.to_gate_compress = operations.Linear(hidden, inner, bias=False, dtype=dtype, device=device) | |
| def forward(self, x, rope_freqs=None, transformer_options={}): | |
| s = x.shape[0] | |
| q, k, v = self.qkv_proj(x).split(self.heads * self.head_dim, dim=-1) | |
| v = v.view(s, self.heads, self.head_dim) | |
| if rope_freqs is not None: | |
| # fused per-head RMSNorm + partial split-half rope, in place on the qkv buffer | |
| q = q.view(1, s, self.heads, self.head_dim) | |
| k = k.view(1, s, self.heads, self.head_dim) | |
| qw = comfy.model_management.cast_to(self.q_norm.weight, device=x.device) | |
| kw = comfy.model_management.cast_to(self.k_norm.weight, device=x.device) | |
| rot = rope_freqs.shape[-3] * 2 | |
| if comfy.model_management.in_training: | |
| q, k = comfy.quant_ops.ck.rms_rope_split_half( | |
| q, k, rope_freqs, qw, kw, epsilon=self.q_norm.eps, rot_dim=rot) | |
| else: | |
| comfy.quant_ops.ck.rms_rope_split_half_( | |
| q, k, rope_freqs, qw, kw, epsilon=self.q_norm.eps, rot_dim=rot) | |
| q = q[0] | |
| k = k[0] | |
| else: | |
| q = self.q_norm(q.view(s, self.heads, self.head_dim)) | |
| k = self.k_norm(k.view(s, self.heads, self.head_dim)) | |
| q = AttentionTensorContainer(q.transpose(0, 1).unsqueeze(0)) | |
| k = AttentionTensorContainer(k.transpose(0, 1).unsqueeze(0)) | |
| v = AttentionTensorContainer(v.transpose(0, 1).unsqueeze(0)) | |
| out = optimized_attention(q, k, v, self.heads, preferred_attention=self.comfy_attention, mask=None, skip_reshape=True, transformer_options=transformer_options) | |
| return self.out_proj(out.squeeze(0)) | |
| class MLP(nn.Module): | |
| def __init__(self, hidden, ffn, dtype=None, device=None, operations=None): | |
| super().__init__() | |
| self.fc1 = operations.Linear(hidden, ffn * 2, bias=False, dtype=dtype, device=device) | |
| self.fc2 = operations.Linear(ffn, hidden, bias=False, dtype=dtype, device=device) | |
| def forward(self, x): | |
| return comfy.ops.linear_input_act(self.fc2, self.fc1(x), "swiglu") | |
| class AdalnProj(nn.Module): | |
| def __init__(self, t_dim, hidden, expand, modalities, apply_silu=True, | |
| dtype=None, device=None, operations=None): | |
| super().__init__() | |
| self.expand = expand | |
| self.modalities = modalities | |
| self.hidden = hidden | |
| self.apply_silu = apply_silu | |
| self.linear = operations.Linear(t_dim, expand * hidden * modalities, bias=True, dtype=dtype, device=device) | |
| def forward(self, t_emb): | |
| # [M, t_dim] -> expand tensors of [M*modalities, hidden] | |
| x = self.linear(nn.functional.silu(t_emb) if self.apply_silu else t_emb) | |
| x = x.view(x.shape[0] * self.modalities, self.expand * self.hidden) | |
| return x.chunk(self.expand, dim=-1) | |
| def _mod_row(vecs, row, dtype): | |
| # row is a mod-row index, or a per-token LongTensor of mod-row indices | |
| return vecs[row].to(dtype) | |
| def _mod_scale_shift(h, shift, scale, segments): | |
| # segments: [(start, stop, mod_row)] covering h contiguously. | |
| for a, b, row in segments: | |
| h[a:b].mul_(1.0 + _mod_row(scale, row, h.dtype)).add_(_mod_row(shift, row, h.dtype)) | |
| return h | |
| def _mod_gate(x, gate, other, segments): | |
| # other is the fresh attn/mlp output: accumulate the gated residual into the stream in place, one fused kernel per segment | |
| for a, b, row in segments: | |
| x[a:b].addcmul_(other[a:b], _mod_row(gate, row, x.dtype)) | |
| return x | |
| class RefinerBlock(nn.Module): | |
| def __init__(self, hidden, heads, head_dim, ffn, eps, qk_eps, dtype=None, device=None, operations=None): | |
| super().__init__() | |
| self.norm1 = operations.RMSNorm(hidden, eps=eps, dtype=dtype, device=device) | |
| self.norm2 = operations.RMSNorm(hidden, eps=eps, dtype=dtype, device=device) | |
| self.attn = Attention(hidden, heads, head_dim, qk_eps, dtype=dtype, device=device, operations=operations) | |
| self.mlp = MLP(hidden, ffn, dtype=dtype, device=device, operations=operations) | |
| def forward(self, x, transformer_options={}): | |
| # attn/mlp outputs are fresh: accumulate residuals in place | |
| x = self.attn(self.norm1(x), transformer_options=transformer_options).add_(x) | |
| return self.mlp(self.norm2(x)).add_(x) | |
| class TokenRefiner(nn.Module): | |
| def __init__(self, num_layers, hidden, heads, head_dim, ffn, eps, qk_eps, final_eps, | |
| dtype=None, device=None, operations=None): | |
| super().__init__() | |
| self.blocks = nn.ModuleList([ | |
| RefinerBlock(hidden, heads, head_dim, ffn, eps, qk_eps, dtype=dtype, device=device, operations=operations) | |
| for _ in range(num_layers)]) | |
| self.final_norm = operations.RMSNorm(hidden, eps=final_eps, dtype=dtype, device=device) | |
| def forward(self, x, transformer_options={}): | |
| for block in self.blocks: | |
| x = block(x, transformer_options=transformer_options) | |
| return self.final_norm(x) | |
| class DiTBlock(nn.Module): | |
| def __init__(self, hidden, heads, head_dim, ffn, t_dim, eps, qk_eps, | |
| apply_silu=True, adaln_dtype=None, gate_compress=False, dtype=None, device=None, operations=None): | |
| super().__init__() | |
| self.norm1 = operations.RMSNorm(hidden, eps=eps, dtype=dtype, device=device) | |
| self.norm2 = operations.RMSNorm(hidden, eps=eps, dtype=dtype, device=device) | |
| self.attn = Attention(hidden, heads, head_dim, qk_eps, gate_compress=gate_compress, | |
| dtype=dtype, device=device, operations=operations) | |
| self.mlp = MLP(hidden, ffn, dtype=dtype, device=device, operations=operations) | |
| self.adaln_proj = AdalnProj(t_dim, hidden, 6, 3, apply_silu=apply_silu, | |
| dtype=adaln_dtype if adaln_dtype is not None else dtype, | |
| device=device, operations=operations) | |
| def forward(self, x, t_emb, mod_segments, rope_freqs, transformer_options={}, attention=None): | |
| attention = self.attn if attention is None else attention | |
| shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaln_proj(t_emb) | |
| h = _mod_scale_shift(self.norm1(x), shift_msa, scale_msa, mod_segments) | |
| x = _mod_gate(x, gate_msa, attention(h, rope_freqs=rope_freqs, transformer_options=transformer_options), mod_segments) | |
| h = _mod_scale_shift(self.norm2(x), shift_mlp, scale_mlp, mod_segments) | |
| return _mod_gate(x, gate_mlp, self.mlp(h), mod_segments) | |
| class FinalLayer(nn.Module): | |
| def __init__(self, hidden, t_dim, video_dim, audio_dim, eps, apply_silu=True, adaln_dtype=None, pdd_steps=1, | |
| dtype=None, device=None, operations=None): | |
| super().__init__() | |
| self.video_dim = video_dim | |
| self.norm = operations.RMSNorm(hidden, eps=eps, dtype=dtype, device=device) | |
| self.adaln_proj = AdalnProj(t_dim, hidden, 2, 1, apply_silu=apply_silu, | |
| dtype=adaln_dtype if adaln_dtype is not None else dtype, | |
| device=device, operations=operations) | |
| # output heads are the checkpoint's fp32 island; norm/adaln are stored at model dtype. | |
| # a PDD checkpoint ships pdd_steps stacked heads per stream (a PDD LoRA grows plain heads to the same layout) | |
| self.video_out = operations.Linear(hidden, video_dim * pdd_steps, bias=True, dtype=torch.float32, device=device) | |
| self.audio_out = operations.Linear(hidden, audio_dim * pdd_steps, bias=True, dtype=torch.float32, device=device) | |
| def forward(self, x, t_emb, video_seg, audio_seg, sigma, sample_sigmas, shifts): | |
| # video_seg / audio_seg: (start, stop, row) of the target streams, where row | |
| # is a mod-row index or a per-token blend (see _mod_row) | |
| shift, scale = self.adaln_proj(t_emb) | |
| def mod(seg): | |
| a, b, row = seg | |
| return (self.norm(x[a:b]) * (1.0 + _mod_row(scale, row, scale.dtype)) + _mod_row(shift, row, shift.dtype)).to(torch.float32) | |
| n = self.video_out.weight.shape[0] // self.video_dim | |
| if n == 1: | |
| return self.video_out(mod(video_seg)), self.audio_out(mod(audio_seg)) | |
| # PDD head bank: row block 0 is a full head, later blocks are offsets from it; | |
| # a step consumes the dt-weighted mean of the heads it spans. | |
| if sample_sigmas is None: | |
| raise ValueError("MiniMax H3 PDD heads need the sampler's sigma schedule") | |
| i = int((sample_sigmas - sigma).abs().argmin()) | |
| sigma_next = sample_sigmas[min(i + 1, sample_sigmas.shape[0] - 1)] | |
| start, stop = (round(float(1.0 - time_shift_sigma(s, shifts[0], 1.0)) * n) for s in (sigma, sigma_next)) | |
| start = min(start, n - 1) | |
| stop = max(stop, start + 1) | |
| return (_pdd_head(self.video_out, mod(video_seg), n, start, stop, shifts[0]), | |
| _pdd_head(self.audio_out, mod(audio_seg), n, start, stop, shifts[1])) | |
| def _pdd_head(head, h, n, start, stop, flow_shift): | |
| grid = torch.linspace(1.0, 0.0, n + 1, dtype=torch.float64) | |
| dt = (1.0 - flow_shift * grid / (1.0 + (flow_shift - 1.0) * grid)).diff()[start:stop] | |
| w = (dt / dt.sum()).to(h) | |
| with comfy.ops.CastBiasWeightContext(head, h, offloadable=True) as (weight, bias): | |
| rows = weight.reshape(n, -1, weight.shape[1]) | |
| brows = bias.reshape(n, -1) | |
| first = max(start, 1) | |
| return nn.functional.linear(h, rows[0] + torch.einsum("n,noi->oi", w[first - start:], rows[first:stop]), | |
| brows[0] + torch.einsum("n,no->o", w[first - start:], brows[first:stop])) | |
| class PackedLayout: | |
| """Static packed-sequence structure for one shape/conditioning signature.""" | |
| def __init__(self, text_len, latent_t, latent_h, latent_w, audio_t, keyframes=None, refs=None): | |
| frame, w_grid = _frame_grid(latent_h, latent_w) | |
| frame_rows = frame.shape[0] | |
| segments = [("text", text_len)] # (kind, n_rows) | |
| g = torch.zeros(text_len, 3, dtype=torch.float64) | |
| g[:, 0] = torch.arange(text_len, dtype=torch.float64) | |
| pos = [g] # per segment: [n, 3] float64 (t, h, w) | |
| img_pos, img_update = [], [] | |
| audio_pos, audio_update = [], [] | |
| row = text_len | |
| target_audio_w = (float(w_grid[0]), float(w_grid[-1])) | |
| # refs pack between text and the targets, so the target timeline starts after their spans | |
| cursor = float(text_len) | |
| for blk in refs or (): | |
| cursor += _ref_t_span(blk) | |
| if keyframes: | |
| # fl2va: keyframe cond rows right after text, sharing the target spatial grid; | |
| # anchors count from the target timeline origin, FRAME_RESCALE per pixel frame, 1.0 per audio latent frame | |
| for kf in keyframes: | |
| cond_t = cursor + FRAME_RESCALE * kf["resolved_frame_index"] | |
| video_latent = kf.get("latent") | |
| if video_latent is not None: | |
| vt = video_latent.shape[2] | |
| n = vt * frame_rows | |
| segments.append(("cond", n)) | |
| pos.append(_video_grid(vt, frame, cond_t)) | |
| img_pos.append(torch.arange(row, row + n)) | |
| img_update.append(torch.zeros(n, dtype=torch.bool)) | |
| row += n | |
| audio_latent = kf.get("audio_latent") | |
| if audio_latent is not None: | |
| rt = audio_latent.shape[-1] | |
| segments.append(("cond_audio", rt * 2)) | |
| pos.append(_audio_grid(cond_t, rt, *target_audio_w)) | |
| audio_pos.append(torch.arange(row, row + rt * 2)) | |
| audio_update.append(torch.zeros(rt * 2, dtype=torch.bool)) | |
| row += rt * 2 | |
| if refs: | |
| cursor = float(text_len) | |
| for blk in refs: | |
| kind = blk["kind"] | |
| if kind == "image": | |
| r_frame, _ = _frame_grid(blk["latent_h"], blk["latent_w"]) | |
| n = r_frame.shape[0] | |
| g = torch.empty(n, 3, dtype=torch.float64) | |
| g[:, 0] = cursor | |
| g[:, 1:] = r_frame | |
| segments.append(("ref_img", n)) | |
| pos.append(g) | |
| img_pos.append(torch.arange(row, row + n)) | |
| img_update.append(torch.zeros(n, dtype=torch.bool)) | |
| row += n | |
| cursor += 1.0 | |
| elif kind == "audio": | |
| rt = blk["ref_audio_t"] | |
| if rt > 0: | |
| segments.append(("ref_audio", rt * 2)) | |
| pos.append(_audio_grid(cursor, rt, *target_audio_w)) | |
| audio_pos.append(torch.arange(row, row + rt * 2)) | |
| audio_update.append(torch.zeros(rt * 2, dtype=torch.bool)) | |
| row += rt * 2 | |
| cursor += float(rt) | |
| elif kind in ("video", "video_audio"): | |
| # the block's audio rows pack immediately before its video | |
| # rows, both sharing the cursor origin | |
| rt = blk["ref_audio_t"] | |
| vt = blk["latent_t"] | |
| r_frame, r_w_grid = _frame_grid(blk["latent_h"], blk["latent_w"]) | |
| if rt > 0: | |
| segments.append(("ref_audio", rt * 2)) | |
| pos.append(_audio_grid(cursor, rt, float(r_w_grid[0]), float(r_w_grid[-1]))) | |
| audio_pos.append(torch.arange(row, row + rt * 2)) | |
| audio_update.append(torch.zeros(rt * 2, dtype=torch.bool)) | |
| row += rt * 2 | |
| n = vt * r_frame.shape[0] | |
| segments.append(("ref_img", n)) | |
| pos.append(_video_grid(vt, r_frame, cursor)) | |
| img_pos.append(torch.arange(row, row + n)) | |
| img_update.append(torch.zeros(n, dtype=torch.bool)) | |
| row += n | |
| cursor += max(float(rt), sum(_video_t_spans(vt))) | |
| # target audio then target video, always the last two segments | |
| segments.append(("audio", audio_t * 2)) | |
| pos.append(_audio_grid(cursor, audio_t, *target_audio_w)) | |
| audio_pos.append(torch.arange(row, row + audio_t * 2)) | |
| audio_update.append(torch.ones(audio_t * 2, dtype=torch.bool)) | |
| row += audio_t * 2 | |
| n_video = latent_t * frame_rows | |
| segments.append(("video", n_video)) | |
| pos.append(_video_grid(latent_t, frame, cursor)) | |
| img_pos.append(torch.arange(row, row + n_video)) | |
| img_update.append(torch.ones(n_video, dtype=torch.bool)) | |
| row += n_video | |
| self.seq_len = row | |
| self.position_ids = torch.cat(pos) # [S, 3] float64 | |
| self.img_pos = torch.cat(img_pos) | |
| self.img_update = torch.cat(img_update) | |
| self.audio_pos = torch.cat(audio_pos) | |
| self.audio_update = torch.cat(audio_update) | |
| self.signature = (text_len, latent_t, latent_h, latent_w, audio_t) | |
| # contiguous segment table (start, stop, kind) | |
| # kinds: text / cond / cond_audio / ref_img / ref_audio / audio / video | |
| # the packed sequence is uniform per segment in (modality tag, timestep class), | |
| # except the text span (tag runs resolved at forward time from the presentation tags) | |
| seg_abs = [] | |
| off = 0 | |
| for kind, n in segments: | |
| seg_abs.append((off, off + n, kind)) | |
| off += n | |
| self.segments = seg_abs | |
| class MiniMaxH3Model(nn.Module): | |
| def __init__(self, hidden_size=5376, num_layers=50, token_refiner_num_layers=2, | |
| num_attention_heads=56, attention_head_dim=128, ffn_hidden_size=14336, | |
| latents_dim=24, audio_latents_dim=32, patch_size=(1, 2, 2), text_dim=5120, | |
| timestep_input_dim=256, time_embed_hidden_size=5376, time_embed_dim=2688, | |
| rope_inv_freq_len=16, norm_eps=1e-5, qk_norm_eps=1e-5, final_norm_eps=1e-5, | |
| sigma_shift_video=12.0, sigma_shift_audio=3.0, | |
| adaln_curve_grid=None, gate_compress=False, pdd_steps=1, | |
| image_model=None, dtype=None, device=None, operations=None, **kwargs): | |
| super().__init__() | |
| self.dtype = dtype | |
| self.hidden_size = hidden_size | |
| self.patch_size = tuple(patch_size) | |
| self.latents_dim = latents_dim | |
| self.audio_latents_dim = audio_latents_dim | |
| self.sigma_shift_video = sigma_shift_video | |
| self.sigma_shift_audio = sigma_shift_audio | |
| self.use_adaln_curves = adaln_curve_grid is not None | |
| # curve-form checkpoints replace the time embedder and full-width adaln weights with a small shared basis of the time-embedding curve | |
| curve = {"apply_silu": not self.use_adaln_curves, | |
| "adaln_dtype": torch.float32 if self.use_adaln_curves else dtype} | |
| video_patch_dim = latents_dim * self.patch_size[0] * self.patch_size[1] * self.patch_size[2] | |
| self.video_patch_proj = operations.Linear(video_patch_dim, hidden_size, bias=True, dtype=torch.float32, device=device) | |
| self.audio_patch_proj = operations.Linear(audio_latents_dim, hidden_size, bias=True, dtype=torch.float32, device=device) | |
| self.condition_proj = operations.Linear(text_dim, hidden_size, bias=True, dtype=dtype, device=device) | |
| if self.use_adaln_curves: | |
| self.register_buffer("adaln_t_table", torch.empty(adaln_curve_grid, time_embed_dim, dtype=torch.float32)) | |
| else: | |
| self.time_embedder = TimeEmbedder(timestep_input_dim, time_embed_hidden_size, time_embed_dim, | |
| dtype=torch.float32, device=device, operations=operations) | |
| self.rope = nn.Module() | |
| self.rope.register_buffer("inv_freq", torch.empty(rope_inv_freq_len, dtype=torch.float32)) | |
| self.token_refiner = TokenRefiner(token_refiner_num_layers, hidden_size, num_attention_heads, | |
| attention_head_dim, ffn_hidden_size, norm_eps, qk_norm_eps, | |
| final_norm_eps, dtype=dtype, device=device, operations=operations) | |
| self.blocks = nn.ModuleList([ | |
| DiTBlock(hidden_size, num_attention_heads, attention_head_dim, ffn_hidden_size, | |
| time_embed_dim, norm_eps, qk_norm_eps, **curve, gate_compress=gate_compress, | |
| dtype=dtype, device=device, operations=operations) | |
| for _ in range(num_layers)]) | |
| self.final_layer = FinalLayer(hidden_size, time_embed_dim, video_patch_dim, audio_latents_dim, | |
| final_norm_eps, **curve, pdd_steps=pdd_steps, dtype=dtype, device=device, operations=operations) | |
| def preprocess_text_embeds(self, text_states): | |
| """[B, L, text_dim] Qwen states -> [B, L, hidden] refined text embeds.""" | |
| if text_states.shape[-1] == self.hidden_size: | |
| return text_states | |
| return self.token_refiner(self.condition_proj(text_states[0])).unsqueeze(0) | |
| def rope_freqs(self, position_ids, device): | |
| # [S, 3] float64 -> [S, 96] fp32 | |
| pos = position_ids.to(torch.float32).to(device) | |
| inv = comfy.model_management.cast_to(self.rope.inv_freq, device=device) | |
| per_axis = pos.unsqueeze(-1) * inv.view(1, 1, -1) # [S, 3, 16] | |
| t_f, h_f, w_f = per_axis.unbind(dim=1) | |
| half = torch.cat((t_f, h_f, w_f), dim=-1) # [S, 48] | |
| return torch.cat((half, half), dim=-1) # [S, 96] | |
| def _cond_video_rows(self, payload, device): | |
| """Concatenated visual condition rows (normalized latents -> patchified), with condition noise augmentation.""" | |
| rows = [] | |
| aug = payload.get("visual_cond_noise_aug", VISUAL_COND_TIMESTEP) | |
| seed = int(payload.get("seed", 0)) | |
| # every condition intentionally restarts the same RNG stream | |
| for z in payload.get("cond_video_latents", []): | |
| r = patchify_video(z.to(torch.float32), self.patch_size) | |
| if aug < 1.0: | |
| gen = torch.Generator("cpu").manual_seed(seed) | |
| noise = torch.randn(r.shape, generator=gen, dtype=torch.float32) | |
| r = aug * r + (1.0 - aug) * noise.to(r.device) | |
| rows.append(r.to(device)) | |
| return torch.cat(rows, dim=0) if rows else None | |
| def _cond_audio_rows(self, payload, device): | |
| rows = [] | |
| aug = payload.get("audio_cond_noise_aug", AUDIO_COND_TIMESTEP) | |
| seed = int(payload.get("seed", 0)) + 1 | |
| for z in payload.get("cond_audio_latents", []): | |
| r = pack_audio(z.to(torch.float32)) | |
| if aug < 1.0: | |
| gen = torch.Generator("cpu").manual_seed(seed) | |
| noise = torch.randn(r.shape, generator=gen, dtype=torch.float32) | |
| r = aug * r + (1.0 - aug) * noise.to(r.device) | |
| rows.append(r.to(device)) | |
| return torch.cat(rows, dim=0) if rows else None | |
| def _embed_and_pack(self, video_x, audio_x, context, layout, payload, transformer_options): | |
| device = video_x.device | |
| dtype = context.dtype | |
| img_update = layout.img_update.to(device) | |
| audio_update = layout.audio_update.to(device) | |
| video_rows = patchify_video(video_x.to(torch.float32), self.patch_size) | |
| audio_rows = pack_audio(audio_x.to(torch.float32)) | |
| cond_video_rows = self._cond_video_rows(payload, device) | |
| cond_audio_rows = self._cond_audio_rows(payload, device) | |
| all_video_rows = video_rows | |
| if cond_video_rows is not None: | |
| all_video_rows = torch.empty(img_update.shape[0], video_rows.shape[1], dtype=torch.float32, device=device) | |
| all_video_rows[~img_update] = cond_video_rows | |
| all_video_rows[img_update] = video_rows | |
| all_audio_rows = audio_rows | |
| if cond_audio_rows is not None: | |
| all_audio_rows = torch.empty(audio_update.shape[0], audio_rows.shape[1], dtype=torch.float32, device=device) | |
| all_audio_rows[~audio_update] = cond_audio_rows | |
| all_audio_rows[audio_update] = audio_rows | |
| video_embed = self.video_patch_proj(all_video_rows).to(dtype) | |
| audio_embed = self.audio_patch_proj(all_audio_rows).to(dtype) | |
| text_states = context[0] | |
| if text_states.shape[-1] != self.hidden_size: | |
| text_states = self.token_refiner(self.condition_proj(text_states), | |
| transformer_options=transformer_options) | |
| # segments are contiguous: assemble by slices, embed rows follow segment order | |
| h = torch.empty(layout.seq_len, self.hidden_size, dtype=dtype, device=device) | |
| voff = aoff = 0 | |
| for a, b, kind in layout.segments: | |
| n = b - a | |
| if kind == "text": | |
| h[a:b] = text_states | |
| elif kind in ("cond", "ref_img", "video"): | |
| h[a:b] = video_embed[voff:voff + n] | |
| voff += n | |
| else: # ref_audio / audio | |
| h[a:b] = audio_embed[aoff:aoff + n] | |
| aoff += n | |
| return h | |
| def forward(self, x, timestep, context, transformer_options={}, minimax_payload=None, denoise_mask=None, audio_denoise_mask=None, **kwargs): | |
| # the sampler carries the audio as (sigma_v / sigma_a) * x_audio; undo it outside | |
| # the wrappers so they and the network see the stream's own latent and velocity | |
| scale = float((minimax_payload or {}).get("audio_scale", 1.0)) | |
| audio_src = x[1] | |
| if scale != 1.0: | |
| shift_v = float(transformer_options.get("minimax_h3_sigma_shift_video", self.sigma_shift_video)) | |
| shift_a = float(transformer_options.get("minimax_h3_sigma_shift_audio", self.sigma_shift_audio)) | |
| sigma_v = (timestep.flatten()[0] / 1000.0).float().clamp(min=1e-6) | |
| sigma_a = time_shift_sigma(sigma_v, shift_v, shift_a) | |
| carry = (sigma_a / sigma_v).to(audio_src.dtype) | |
| x = [x[0], audio_src * carry] | |
| compile_allocations = comfy.model_prefetch.malloc_graph_enabled(x[0].device) | |
| if compile_allocations: | |
| out = [torch.empty_like(x[0]), torch.empty_like(x[1])] | |
| comfy.model_prefetch.malloc_graph_begin(x[0].device) | |
| graph_out = comfy.patcher_extension.WrapperExecutor.new_class_executor( | |
| self._forward, | |
| self, | |
| comfy.patcher_extension.get_all_wrappers(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, transformer_options) | |
| ).execute(x, timestep, context, transformer_options, minimax_payload=minimax_payload, | |
| denoise_mask=denoise_mask, audio_denoise_mask=audio_denoise_mask, **kwargs) | |
| if compile_allocations: | |
| out[0].copy_(graph_out[0]) | |
| out[1].copy_(graph_out[1]) | |
| del graph_out | |
| comfy.model_prefetch.malloc_graph_end() | |
| else: | |
| out = graph_out | |
| # Masked rows predict at mask * sigma; scale their velocity to match the outer x0 conversion. | |
| if denoise_mask is not None: | |
| out[0] = out[0] * denoise_mask | |
| if audio_denoise_mask is not None: | |
| out[1] = out[1] * audio_denoise_mask | |
| if scale != 1.0: | |
| # d/d(sigma_v) of the carried variable | |
| out[1] = ((1.0 - scale) * (audio_src * carry) | |
| + (1.0 + (scale - 1.0) * sigma_a).to(out[1].dtype) * out[1]) | |
| return out | |
| def _forward(self, x, timestep, context, transformer_options={}, minimax_payload=None, denoise_mask=None, audio_denoise_mask=None, **kwargs): | |
| video_x, audio_x = x[0], x[1] | |
| orig_t, orig_h, orig_w = video_x.shape[2], video_x.shape[3], video_x.shape[4] | |
| video_x = comfy.ldm.common_dit.pad_to_patch_size(video_x, self.patch_size) | |
| if video_x.shape[0] != 1: | |
| raise ValueError("MiniMax H3 supports batch size 1") | |
| payload = minimax_payload or {} | |
| device = video_x.device | |
| dtype = context.dtype # compute dtype | |
| latent_t, lat_h, lat_w = video_x.shape[2], video_x.shape[3], video_x.shape[4] | |
| audio_t = audio_x.shape[-1] | |
| text_len = context.shape[1] | |
| # extra_conds prebuilds the layout once per sampling run | |
| layout = payload.get("layout") | |
| if layout is None or layout.signature != (text_len, latent_t, lat_h, lat_w, audio_t): | |
| layout = PackedLayout(text_len, latent_t, lat_h, lat_w, audio_t, | |
| keyframes=payload.get("keyframes"), | |
| refs=payload.get("refs")) | |
| transformer_options["minimax_h3_layout"] = layout # segment spans for attention patches | |
| # model_base passes model_sampling.timestep(sigma) = sigma * 1000 | |
| shift_v = float(transformer_options.get("minimax_h3_sigma_shift_video", self.sigma_shift_video)) | |
| shift_a = float(transformer_options.get("minimax_h3_sigma_shift_audio", self.sigma_shift_audio)) | |
| sigma_v = (timestep.flatten()[0] / 1000.0).float().clamp(min=1e-6) | |
| t_v = float(1.0 - sigma_v) | |
| t_a = float(1.0 - time_shift_sigma(sigma_v, shift_v, shift_a)) | |
| # distinct timesteps are known analytically: text/pad follow video, cond rows pin near 1 | |
| vis_aug = float(payload.get("visual_cond_noise_aug", VISUAL_COND_TIMESTEP)) | |
| aud_aug = float(payload.get("audio_cond_noise_aug", AUDIO_COND_TIMESTEP)) | |
| seg_t = {"text": t_v, "video": t_v, "audio": t_a, | |
| "cond": max(t_v, vis_aug), "ref_img": max(t_v, vis_aug), | |
| "cond_audio": max(t_a, aud_aug), "ref_audio": max(t_a, aud_aug)} | |
| # masked rows run at their own strength: mask value m puts a row at sigma = m * sigma_stream, | |
| # so its label is 1 - m * sigma, clamped at the cond timestep for fully preserved rows | |
| t_pin_v = max(t_v, VISUAL_COND_TIMESTEP) | |
| t_pin_a = max(t_a, AUDIO_COND_TIMESTEP) | |
| video_rows_t = None | |
| audio_rows_t = None | |
| if denoise_mask is not None: | |
| m = mask_row_values(denoise_mask[0, 0].to(torch.float32), latent_t, lat_h, lat_w) | |
| if m is not None: | |
| rows_t = (1.0 - m * sigma_v.to(m.device)).clamp(max=t_pin_v) | |
| if rows_t.unique().numel() == 1: | |
| seg_t["video"] = float(rows_t[0]) | |
| else: | |
| video_rows_t = rows_t | |
| if audio_denoise_mask is not None: | |
| m = audio_denoise_mask[0, 0].to(torch.float32).reshape(-1) | |
| if not bool((m >= 1.0 - 1e-3).all()): | |
| sigma_a = 1.0 - t_a | |
| rows_t = (1.0 - m * sigma_a).clamp(max=t_pin_a) | |
| if rows_t.unique().numel() == 1: | |
| seg_t["audio"] = float(rows_t[0]) | |
| else: | |
| audio_rows_t = rows_t | |
| unique_t = sorted({t_v, t_a} | {seg_t[k] for _, _, k in layout.segments} | |
| | (set(video_rows_t.unique().tolist()) if video_rows_t is not None else set()) | |
| | (set(audio_rows_t.unique().tolist()) if audio_rows_t is not None else set())) | |
| t_row = {t: i for i, t in enumerate(unique_t)} | |
| seg_tag = {"text": 1, "video": 0, "audio": 2, "cond": 0, "ref_img": 0, "cond_audio": 2, "ref_audio": 2} | |
| def rows_to_mod_index(rows_t, tag): | |
| # per-row timestep values -> per-row mod-row indices into the t_emb table | |
| levels = rows_t.unique() | |
| base = torch.tensor([t_row[v] * 3 + tag for v in levels.tolist()], | |
| dtype=torch.long, device=rows_t.device) | |
| return base[torch.searchsorted(levels, rows_t)] | |
| text_tags = payload.get("text_token_tags") | |
| mod_segments = [] | |
| for a, b, kind in layout.segments: | |
| row_base = t_row[seg_t[kind]] * 3 | |
| if kind == "text" and text_tags is not None: | |
| # the presentation text span mixes tags (vision pads carry the video modality) split into tag runs | |
| tags = text_tags.view(-1).tolist() | |
| run_start = 0 | |
| for i in range(1, b - a + 1): | |
| if i == b - a or tags[i] != tags[run_start]: | |
| mod_segments.append((a + run_start, a + i, row_base + int(tags[run_start]))) | |
| run_start = i | |
| elif kind == "video" and video_rows_t is not None: | |
| mod_segments.append((a, b, rows_to_mod_index(video_rows_t, seg_tag[kind]))) | |
| elif kind == "audio" and audio_rows_t is not None: | |
| mod_segments.append((a, b, rows_to_mod_index(audio_rows_t, seg_tag[kind]))) | |
| else: | |
| mod_segments.append((a, b, row_base + seg_tag[kind])) | |
| h = self._embed_and_pack(video_x, audio_x, context, layout, payload, transformer_options) | |
| t_vals = torch.tensor(unique_t, dtype=torch.float32, device=device) | |
| if self.use_adaln_curves: | |
| # adaln projections consume interpolated coordinates of the time-embedding curve | |
| table = comfy.model_management.cast_to(self.adaln_t_table, device=device) | |
| pos = t_vals.clamp(0.0, 1.0) * (table.shape[0] - 1) # t in [0,1] -> fractional grid index, out-of-range t clamps to the curve ends | |
| i0 = pos.floor().long().clamp(max=table.shape[0] - 2) # lower grid row, max-clamp keeps t=1.0 on the last interval instead of reading past the table | |
| t_emb = torch.lerp(table[i0], table[i0 + 1], (pos - i0).unsqueeze(1)) # blend the two rows by the fractional part | |
| else: | |
| t_emb = self.time_embedder(t_vals).to(dtype) | |
| # rotation table computed once per forward, consumed by the kitchen split-half rope | |
| rope_freqs = rope_rotation_table(self.rope_freqs(layout.position_ids, device), dtype) | |
| # blocks | |
| patches_replace = transformer_options.get("patches_replace", {}) | |
| blocks_replace = patches_replace.get("dit", {}) | |
| prefetch_queue = comfy.model_prefetch.make_prefetch_queue(list(self.blocks), device, transformer_options) | |
| for i, block in enumerate(self.blocks): | |
| comfy.model_prefetch.prefetch_queue_pop(prefetch_queue, device, block, malloc_scope="block") | |
| transformer_options["block_index"] = i | |
| if ("double_block", i) in blocks_replace: | |
| def block_wrap(args): | |
| return {"img": block(args["img"], args["t_emb"], args["mod_segments"], args["rope_freqs"], | |
| transformer_options=args["transformer_options"], attention=args.get("attention"))} | |
| h = blocks_replace[("double_block", i)]( | |
| {"img": h, "t_emb": t_emb, "mod_segments": mod_segments, "rope_freqs": rope_freqs, | |
| "layout": layout, "transformer_options": transformer_options}, | |
| {"original_block": block_wrap})["img"] | |
| else: | |
| h = block(h, t_emb, mod_segments, rope_freqs, transformer_options=transformer_options) | |
| comfy.model_prefetch.prefetch_queue_pop(prefetch_queue, device, None, malloc_scope="block") | |
| # target streams are single contiguous segments (audio then video, last two) | |
| va, vb, _ = next(s for s in layout.segments if s[2] == "video") | |
| aa, ab, _ = next(s for s in layout.segments if s[2] == "audio") | |
| if video_rows_t is not None: | |
| video_seg = (va, vb, rows_to_mod_index(video_rows_t, 0) // 3) | |
| else: | |
| video_seg = (va, vb, t_row[seg_t["video"]]) | |
| if audio_rows_t is not None: | |
| audio_seg = (aa, ab, rows_to_mod_index(audio_rows_t, 0) // 3) | |
| else: | |
| audio_seg = (aa, ab, t_row[seg_t["audio"]]) | |
| v, a = self.final_layer(h, t_emb, video_seg, audio_seg, sigma_v, transformer_options.get("sample_sigmas"), (shift_v, shift_a)) | |
| video_out = unpatchify_video(v, latent_t, lat_h // 2, lat_w // 2, self.latents_dim, self.patch_size) | |
| video_out = video_out[:, :, :orig_t, :orig_h, :orig_w] | |
| audio_out = unpack_audio(a) | |
| return [-video_out.to(video_x.dtype), -audio_out.to(audio_x.dtype)] | |