Add support to train Fast H3 Omni with training adapter
This commit is contained in:
1 parent
15303bf876
commit
b1d5ef8534
9 files changed
+377
-10
No files matched your search
@@ -22,7 +22,7 @@ from .krea2 import Krea2Model
|
|||||||
from .iris3b import Iris3BModel
|
from .iris3b import Iris3BModel
|
||||||
from .boogu_image import BooguImageModel, BooguImageEditModel
|
from .boogu_image import BooguImageModel, BooguImageEditModel
|
||||||
from .mageflow import MageFlowModel, MageFlowEditModel
|
from .mageflow import MageFlowModel, MageFlowEditModel
|
||||||
from .minimax_h3 import MinimaxH3Model, MinimaxH3Ref2VAModel, MinimaxH3FastModel
|
from .minimax_h3 import MinimaxH3Model, MinimaxH3Ref2VAModel, MinimaxH3FastModel, MinimaxH3OmniModel
|
||||||
from .flux3 import Flux3Model
|
from .flux3 import Flux3Model
|
||||||
from .moss_image import MossImageModel
|
from .moss_image import MossImageModel
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ AI_TOOLKIT_MODELS = [
|
|||||||
MinimaxH3Model,
|
MinimaxH3Model,
|
||||||
MinimaxH3Ref2VAModel,
|
MinimaxH3Ref2VAModel,
|
||||||
MinimaxH3FastModel,
|
MinimaxH3FastModel,
|
||||||
|
MinimaxH3OmniModel,
|
||||||
Flux3Model,
|
Flux3Model,
|
||||||
MossImageModel,
|
MossImageModel,
|
||||||
]
|
]
|
||||||
@@ -1 +1 @@
|
|||||||
from .minimax_h3 import MinimaxH3Model, MinimaxH3Ref2VAModel, MinimaxH3FastModel
|
from .minimax_h3 import MinimaxH3Model, MinimaxH3Ref2VAModel, MinimaxH3FastModel, MinimaxH3OmniModel
|
||||||
@@ -120,10 +120,13 @@ COMFY_FILES = {
|
|||||||
"video_vae": "vae/minimax_h3_video_vae_fp16.safetensors",
|
"video_vae": "vae/minimax_h3_video_vae_fp16.safetensors",
|
||||||
"audio_vae": "vae/minimax_h3_audio_vae_fp32.safetensors",
|
"audio_vae": "vae/minimax_h3_audio_vae_fp32.safetensors",
|
||||||
"dit_fasth3": "diffusion_models/fastvideo_fasth3_8step_v2_pruned_int8_convrot.safetensors",
|
"dit_fasth3": "diffusion_models/fastvideo_fasth3_8step_v2_pruned_int8_convrot.safetensors",
|
||||||
|
"dit_fasth3_omni": "diffusion_models/fasth3_omniref_8step_pruned_int8_convrot.safetensors",
|
||||||
}
|
}
|
||||||
# FastH3 8-Step V2 (FastVideo DMD2 + VSA distill) comfy repack: DiT plus the
|
# FastH3 8-Step V2 (FastVideo DMD2 + VSA distill) comfy repack: DiT plus the
|
||||||
# shared text encoder and VAEs at the same repo-relative paths
|
# shared text encoder and VAEs at the same repo-relative paths
|
||||||
FASTH3_REPO = "FastVideo/FastVideo-FastH3-Comfy"
|
FASTH3_REPO = "FastVideo/FastVideo-FastH3-Comfy"
|
||||||
|
# FastH3 Omni (8-step ref2va distill, 32 PDD heads) comfy repack, same layout
|
||||||
|
FASTH3_OMNI_REPO = "FastVideo/FastH3-Omni-Comfy"
|
||||||
# tokenizer/processor/text-encoder config come from the original repo (tiny files)
|
# tokenizer/processor/text-encoder config come from the original repo (tiny files)
|
||||||
ORIGINAL_REPO = "MiniMaxAI/MiniMax-H3"
|
ORIGINAL_REPO = "MiniMaxAI/MiniMax-H3"
|
||||||
|
|
||||||
@@ -955,6 +958,7 @@ class MinimaxH3Model(BaseModel):
|
|||||||
text_indices=text_indices.to(device),
|
text_indices=text_indices.to(device),
|
||||||
# target-video token grid (patch 1x2x2); consumed only by VSA models
|
# target-video token grid (patch 1x2x2); consumed only by VSA models
|
||||||
vsa_video_grid=(t_lat, h_lat // 2, w_lat // 2),
|
vsa_video_grid=(t_lat, h_lat // 2, w_lat // 2),
|
||||||
|
pdd_weights=self._pdd_weights(sigma_v),
|
||||||
)
|
)
|
||||||
|
|
||||||
if num_cond_audio > 0:
|
if num_cond_audio > 0:
|
||||||
@@ -975,6 +979,19 @@ class MinimaxH3Model(BaseModel):
|
|||||||
)
|
)
|
||||||
return -noise_pred
|
return -noise_pred
|
||||||
|
|
||||||
|
def _pdd_weights(self, sigma_v, sigma_v_next=None):
|
||||||
|
"""Head weights for PDD checkpoints (None otherwise). Training passes a
|
||||||
|
single sigma: the one 1/n interval containing it."""
|
||||||
|
n = unwrap_model(self.model).params.pdd_steps
|
||||||
|
if n <= 1:
|
||||||
|
return None
|
||||||
|
return packing.build_pdd_weights(
|
||||||
|
sigma_v.flatten().tolist(),
|
||||||
|
n,
|
||||||
|
self.video_sigma_shift,
|
||||||
|
sigma_v_next=None if sigma_v_next is None else sigma_v_next.flatten().tolist(),
|
||||||
|
)
|
||||||
|
|
||||||
def get_loss_target(self, *args, **kwargs):
|
def get_loss_target(self, *args, **kwargs):
|
||||||
noise = kwargs.get("noise")
|
noise = kwargs.get("noise")
|
||||||
batch = kwargs.get("batch")
|
batch = kwargs.get("batch")
|
||||||
@@ -1618,3 +1635,59 @@ class MinimaxH3FastModel(MinimaxH3Model):
|
|||||||
def text_embedding_space_version(self):
|
def text_embedding_space_version(self):
|
||||||
# same Qwen3-VL presentation as the base model: share the embed cache
|
# same Qwen3-VL presentation as the base model: share the embed cache
|
||||||
return "minimax_h3"
|
return "minimax_h3"
|
||||||
|
|
||||||
|
|
||||||
|
class MinimaxH3OmniModel(MinimaxH3Ref2VAModel):
|
||||||
|
"""FastH3 Omni: FastVideo's 8-step ref2va distill. References work exactly
|
||||||
|
like ``minimax_h3_ref2va`` (control images/videos as reference blocks).
|
||||||
|
|
||||||
|
The DiT carries 32 PDD output heads per stream (see
|
||||||
|
MiniMaxH3TransformerParams.pdd_steps): sampling blends the heads each
|
||||||
|
step spans, training uses the head of the interval containing its sigma.
|
||||||
|
Video/audio shifts stay 12/3.
|
||||||
|
|
||||||
|
It was distilled with FastVideo's VSA (tile 128, ``p2_multi_region``
|
||||||
|
reference policy), which src/vsa.py does not implement, so attention runs
|
||||||
|
dense like native ComfyUI. ``model_kwargs.vsa`` (default false) opts into
|
||||||
|
the (4,4,4) tile VSA with references as dense prefix, at
|
||||||
|
``vsa_sparsity`` (default 0.9).
|
||||||
|
|
||||||
|
Sample with 8 steps and guidance_scale 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
arch = "minimax_h3_omni"
|
||||||
|
comfy_repo = FASTH3_OMNI_REPO
|
||||||
|
# trained ladder: [999, 874, ..., 125] -> PDD spans [0, 4, ..., 32]
|
||||||
|
t1000_sample_ladder = True
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
kw = self.model_config.model_kwargs
|
||||||
|
self.vsa_sparsity: Optional[float] = None
|
||||||
|
if bool(kw.get("vsa", False)):
|
||||||
|
self.vsa_sparsity = float(kw.get("vsa_sparsity", 0.9))
|
||||||
|
|
||||||
|
def _dit_component(self) -> str:
|
||||||
|
return "dit_fasth3_omni"
|
||||||
|
|
||||||
|
def _load_transformer(self) -> MiniMaxH3Transformer:
|
||||||
|
transformer = super()._load_transformer()
|
||||||
|
if transformer.params.pdd_steps <= 1:
|
||||||
|
raise ValueError(
|
||||||
|
"minimax_h3_omni needs the FastH3 Omni checkpoint with stacked "
|
||||||
|
"PDD output heads; this file has a single head. Use arch "
|
||||||
|
"minimax_h3_ref2va for the base ref2va checkpoints."
|
||||||
|
)
|
||||||
|
transformer.vsa_sparsity = self.vsa_sparsity
|
||||||
|
return transformer
|
||||||
|
|
||||||
|
def get_base_model_version(self):
|
||||||
|
return "minimax_h3_omni"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text_embedding_space_version(self):
|
||||||
|
# same Qwen3-VL reference presentation as ref2va: share its embed cache
|
||||||
|
n = self._image_ref_video_frames()
|
||||||
|
if n:
|
||||||
|
return f"minimax_h3_ref2va:img_as_vid{n}"
|
||||||
|
return "minimax_h3_ref2va"
|
||||||
@@ -536,6 +536,49 @@ def shift_sigma(sigma, shift: float):
|
|||||||
return shift * sigma / (1.0 + (shift - 1.0) * sigma)
|
return shift * sigma / (1.0 + (shift - 1.0) * sigma)
|
||||||
|
|
||||||
|
|
||||||
|
def pdd_span(sigma_v: float, n: int, video_shift: float, sigma_v_next=None):
|
||||||
|
"""Head interval [start, stop) on the unshifted n-interval grid. A sampling
|
||||||
|
step spans sigma -> sigma_next (ComfyUI's rounding); a single training
|
||||||
|
sigma uses the one interval containing it."""
|
||||||
|
base_t = 1.0 - float(shift_sigma(sigma_v, 1.0 / video_shift))
|
||||||
|
if sigma_v_next is None:
|
||||||
|
start = min(max(int(math.floor(base_t * n)), 0), n - 1)
|
||||||
|
return start, start + 1
|
||||||
|
base_t_next = 1.0 - float(shift_sigma(sigma_v_next, 1.0 / video_shift))
|
||||||
|
start = min(round(base_t * n), n - 1)
|
||||||
|
stop = max(round(base_t_next * n), start + 1)
|
||||||
|
return start, stop
|
||||||
|
|
||||||
|
|
||||||
|
def pdd_head_weights(start: int, stop: int, n: int, flow_shift: float):
|
||||||
|
"""(n,) weights over the stacked heads: head 0 (the full head) at 1, the
|
||||||
|
offset heads in [max(start, 1), stop) at their normalized dt share."""
|
||||||
|
grid = torch.linspace(1.0, 0.0, n + 1, dtype=torch.float64)
|
||||||
|
dt = (1.0 - shift_sigma(grid, flow_shift)).diff()[start:stop]
|
||||||
|
w = torch.zeros(n, dtype=torch.float64)
|
||||||
|
w[0] = 1.0
|
||||||
|
first = max(start, 1)
|
||||||
|
w[first:stop] = (dt / dt.sum())[first - start :]
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
def build_pdd_weights(
|
||||||
|
sigma_v, # (B,) video sigmas
|
||||||
|
n: int,
|
||||||
|
video_shift: float,
|
||||||
|
audio_shift: float = AUDIO_SIGMA_SHIFT,
|
||||||
|
sigma_v_next=None, # (B,) or None for single-sigma training
|
||||||
|
):
|
||||||
|
"""Per-sample (video, audio) head weights, each (B, n) float32."""
|
||||||
|
wv, wa = [], []
|
||||||
|
for i in range(len(sigma_v)):
|
||||||
|
nxt = None if sigma_v_next is None else float(sigma_v_next[i])
|
||||||
|
start, stop = pdd_span(float(sigma_v[i]), n, video_shift, nxt)
|
||||||
|
wv.append(pdd_head_weights(start, stop, n, video_shift))
|
||||||
|
wa.append(pdd_head_weights(start, stop, n, audio_shift))
|
||||||
|
return torch.stack(wv).float(), torch.stack(wa).float()
|
||||||
|
|
||||||
|
|
||||||
def remap_sigma(
|
def remap_sigma(
|
||||||
sigma, from_shift: float = VIDEO_SIGMA_SHIFT, to_shift: float = AUDIO_SIGMA_SHIFT
|
sigma, from_shift: float = VIDEO_SIGMA_SHIFT, to_shift: float = AUDIO_SIGMA_SHIFT
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ class MiniMaxH3Pipeline:
|
|||||||
text_indices=text_indices,
|
text_indices=text_indices,
|
||||||
# target-video token grid (patch 1x2x2); consumed only by VSA models
|
# target-video token grid (patch 1x2x2); consumed only by VSA models
|
||||||
vsa_video_grid=(t_lat, h_lat // 2, w_lat // 2),
|
vsa_video_grid=(t_lat, h_lat // 2, w_lat // 2),
|
||||||
|
pdd_weights=model._pdd_weights(sv, sv_next),
|
||||||
)
|
)
|
||||||
v_video = video_pred[:, num_cond:].float()
|
v_video = video_pred[:, num_cond:].float()
|
||||||
v_audio = audio_pred[:, layout.num_condition_audio_rows :].float()
|
v_audio = audio_pred[:, layout.num_condition_audio_rows :].float()
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ class MiniMaxH3TransformerParams:
|
|||||||
# and add a bias to the block AdaLN linears, which the original weights
|
# and add a bias to the block AdaLN linears, which the original weights
|
||||||
# lack (the final layer's AdaLN carries a bias in both variants).
|
# lack (the final layer's AdaLN carries a bias in both variants).
|
||||||
adaln_t_table_size: Optional[int] = None
|
adaln_t_table_size: Optional[int] = None
|
||||||
|
# PDD checkpoints (FastH3 Omni) stack pdd_steps output heads per stream on
|
||||||
|
# the unshifted [1, 0] grid: block 0 is a full head, later blocks are
|
||||||
|
# offsets from it; a step uses the dt-weighted blend of the heads it spans
|
||||||
|
pdd_steps: int = 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def adaln_apply_silu(self) -> bool:
|
def adaln_apply_silu(self) -> bool:
|
||||||
@@ -368,14 +372,22 @@ class MiniMaxH3FinalLayer(nn.Module):
|
|||||||
apply_silu=p.adaln_apply_silu,
|
apply_silu=p.adaln_apply_silu,
|
||||||
bias=True,
|
bias=True,
|
||||||
)
|
)
|
||||||
self.video_out = nn.Linear(p.hidden_size, video_patch_dim, bias=True)
|
self.pdd_steps = p.pdd_steps
|
||||||
self.audio_out = nn.Linear(p.hidden_size, p.audio_latents_dim, bias=True)
|
self.video_out = nn.Linear(
|
||||||
|
p.hidden_size, video_patch_dim * p.pdd_steps, bias=True
|
||||||
|
)
|
||||||
|
self.audio_out = nn.Linear(
|
||||||
|
p.hidden_size, p.audio_latents_dim * p.pdd_steps, bias=True
|
||||||
|
)
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
self,
|
self,
|
||||||
x: torch.Tensor, # (B, S, hidden)
|
x: torch.Tensor, # (B, S, hidden)
|
||||||
temb: torch.Tensor, # (M, time_embed_dim)
|
temb: torch.Tensor, # (M, time_embed_dim)
|
||||||
timestep_indices: torch.Tensor, # (B, S) long into the M rows
|
timestep_indices: torch.Tensor, # (B, S) long into the M rows
|
||||||
|
video_indices: torch.Tensor, # (Nv,) long
|
||||||
|
audio_indices: torch.Tensor, # (Na,) long
|
||||||
|
pdd_weights: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
||||||
):
|
):
|
||||||
shift, scale = self.adaln_proj(temb)
|
shift, scale = self.adaln_proj(temb)
|
||||||
dt = x.dtype
|
dt = x.dtype
|
||||||
@@ -383,7 +395,23 @@ class MiniMaxH3FinalLayer(nn.Module):
|
|||||||
timestep_indices
|
timestep_indices
|
||||||
].to(dt)
|
].to(dt)
|
||||||
h = h.to(self.video_out.weight.dtype)
|
h = h.to(self.video_out.weight.dtype)
|
||||||
return self.video_out(h), self.audio_out(h)
|
video = self.video_out(h.index_select(1, video_indices))
|
||||||
|
audio = self.audio_out(h.index_select(1, audio_indices))
|
||||||
|
if self.pdd_steps > 1:
|
||||||
|
if pdd_weights is None:
|
||||||
|
raise ValueError("PDD heads need pdd_weights for the current step")
|
||||||
|
# heads run through the Linear's own forward so LoRA hooks apply
|
||||||
|
video = _blend_pdd_heads(video, pdd_weights[0], self.pdd_steps)
|
||||||
|
audio = _blend_pdd_heads(audio, pdd_weights[1], self.pdd_steps)
|
||||||
|
return video, audio
|
||||||
|
|
||||||
|
|
||||||
|
def _blend_pdd_heads(out: torch.Tensor, weights: torch.Tensor, n: int):
|
||||||
|
"""(B, N, n * d) stacked head outputs, (B, n) weights -> (B, N, d)."""
|
||||||
|
b, rows, _ = out.shape
|
||||||
|
return torch.einsum(
|
||||||
|
"brkd,bk->brd", out.view(b, rows, n, -1), weights.to(out.device, out.dtype)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MiniMaxH3Transformer(nn.Module, OstrisModelMixin):
|
class MiniMaxH3Transformer(nn.Module, OstrisModelMixin):
|
||||||
@@ -399,6 +427,12 @@ class MiniMaxH3Transformer(nn.Module, OstrisModelMixin):
|
|||||||
params.adaln_t_table_size = table.shape[0]
|
params.adaln_t_table_size = table.shape[0]
|
||||||
params.time_embed_dim = table.shape[1]
|
params.time_embed_dim = table.shape[1]
|
||||||
params.gate_compress = "blocks.0.attn.to_gate_compress.weight" in state_dict
|
params.gate_compress = "blocks.0.attn.to_gate_compress.weight" in state_dict
|
||||||
|
video_out = state_dict.get("final_layer.video_out.weight", None)
|
||||||
|
if video_out is not None:
|
||||||
|
p = params.patch_size
|
||||||
|
params.pdd_steps = video_out.shape[0] // (
|
||||||
|
params.latents_dim * p[0] * p[1] * p[2]
|
||||||
|
)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -504,6 +538,9 @@ class MiniMaxH3Transformer(nn.Module, OstrisModelMixin):
|
|||||||
vsa_video_grid: Optional[
|
vsa_video_grid: Optional[
|
||||||
Tuple[int, int, int]
|
Tuple[int, int, int]
|
||||||
] = None, # target-video token grid (t, h, w)
|
] = None, # target-video token grid (t, h, w)
|
||||||
|
# PDD checkpoints only: per-sample (video, audio) head weights, each
|
||||||
|
# (B, pdd_steps), from packing.build_pdd_weights
|
||||||
|
pdd_weights: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
||||||
):
|
):
|
||||||
"""Returns (video_out (B, Nv, 96), audio_out (B, Na, 32)) — the
|
"""Returns (video_out (B, Nv, 96), audio_out (B, Na, 32)) — the
|
||||||
data-ward velocity ``clean - noise`` for every row, in input order.
|
data-ward velocity ``clean - noise`` for every row, in input order.
|
||||||
@@ -582,7 +619,6 @@ class MiniMaxH3Transformer(nn.Module, OstrisModelMixin):
|
|||||||
else:
|
else:
|
||||||
x = block(x, temb, adaln_indices, rotary_emb, attn_mask, vsa_ctx)
|
x = block(x, temb, adaln_indices, rotary_emb, attn_mask, vsa_ctx)
|
||||||
|
|
||||||
video_all, audio_all = self.final_layer(x, temb, inverse)
|
return self.final_layer(
|
||||||
video_out = video_all.index_select(1, video_indices)
|
x, temb, inverse, video_indices, audio_indices, pdd_weights
|
||||||
audio_out = audio_all.index_select(1, audio_indices)
|
)
|
||||||
return video_out, audio_out
|
|
||||||
@@ -1472,6 +1472,218 @@ export const AI_TOOLKIT_UI_MODELS: ModelArch[] = [
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "minimax_h3_omni",
|
||||||
|
label: "FastH3 Omni",
|
||||||
|
group: "video",
|
||||||
|
isVideoModel: true,
|
||||||
|
defaults: {
|
||||||
|
// default updates when [selected, unselected] in the UI
|
||||||
|
"config.process[0].model.name_or_path": [
|
||||||
|
"FastVideo/FastH3-Omni-Comfy",
|
||||||
|
defaultNameOrPath,
|
||||||
|
],
|
||||||
|
// pre-quantized weights: matching qtypes keep the load unchanged
|
||||||
|
"config.process[0].model.quantize": [true, false],
|
||||||
|
"config.process[0].model.qtype": ["convrot8", "qfloat8"],
|
||||||
|
"config.process[0].model.quantize_te": [true, false],
|
||||||
|
"config.process[0].model.qtype_te": ["nvfp4", "qfloat8"],
|
||||||
|
"config.process[0].model.low_vram": [true, false],
|
||||||
|
"config.process[0].sample.sampler": ["flowmatch", "flowmatch"],
|
||||||
|
"config.process[0].train.noise_scheduler": ["flowmatch", "flowmatch"],
|
||||||
|
"config.process[0].train.cache_text_embeddings": [true, false],
|
||||||
|
"config.process[0].model.assistant_lora_path": [
|
||||||
|
"ostris/minimax_h3_training_adapter/fastvideo_fasth3_omni_training_adapter_v1.safetensors",
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
"config.process[0].network.linear": [16, defaultLinearRank],
|
||||||
|
"config.process[0].network.linear_alpha": [16, defaultLinearRank],
|
||||||
|
"config.process[0].network.network_kwargs.ignore_if_contains": [
|
||||||
|
["adaln_proj"],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
"config.process[0].sample.num_frames": [107, 1],
|
||||||
|
"config.process[0].sample.fps": [24, 1],
|
||||||
|
"config.process[0].sample.width": [768, 1024],
|
||||||
|
"config.process[0].sample.height": [768, 1024],
|
||||||
|
"config.process[0].sample.guidance_scale": [1, 4],
|
||||||
|
"config.process[0].sample.sample_steps": [8, 25],
|
||||||
|
"config.process[0].train.audio_loss_multiplier": [1.0, undefined],
|
||||||
|
"config.process[0].train.timestep_type": ["shift", "sigmoid"],
|
||||||
|
"config.process[0].datasets[x].do_audio": [true, undefined],
|
||||||
|
"config.process[0].datasets[x].cache_latents_to_disk": [true, false],
|
||||||
|
"config.process[0].datasets[x].fps": [24, undefined],
|
||||||
|
"config.process[0].datasets[x].num_frames": [39, undefined],
|
||||||
|
"config.process[0].datasets[x].auto_frame_count": [true, undefined],
|
||||||
|
},
|
||||||
|
disableSections: ["network.conv"],
|
||||||
|
additionalSections: [
|
||||||
|
"sample.multi_ctrl_imgs",
|
||||||
|
"datasets.multi_control_paths",
|
||||||
|
"datasets.num_frames",
|
||||||
|
"model.layer_offloading",
|
||||||
|
"model.low_vram",
|
||||||
|
"datasets.do_audio",
|
||||||
|
"datasets.audio_normalize",
|
||||||
|
"datasets.audio_preserve_pitch",
|
||||||
|
"train.audio_loss_multiplier",
|
||||||
|
"datasets.auto_frame_count",
|
||||||
|
"model.assistant_lora_path",
|
||||||
|
],
|
||||||
|
customModelSelectOptions: [
|
||||||
|
{
|
||||||
|
label: "Distillation Handling Method",
|
||||||
|
options: [
|
||||||
|
{ value: "ta", label: "Training Adapter (default)" },
|
||||||
|
{ value: "dopsd", label: "D-OPSD" },
|
||||||
|
{ value: "none", label: "None" },
|
||||||
|
],
|
||||||
|
getValue: (config: JobConfig) => {
|
||||||
|
if (config?.config?.process?.[0]?.model?.model_kwargs?.dopsd) {
|
||||||
|
return "dopsd";
|
||||||
|
}
|
||||||
|
const assistantLoraPath =
|
||||||
|
config?.config?.process?.[0]?.model?.assistant_lora_path;
|
||||||
|
return assistantLoraPath && assistantLoraPath.trim() !== ""
|
||||||
|
? "ta"
|
||||||
|
: "none";
|
||||||
|
},
|
||||||
|
onChange: (
|
||||||
|
value: string,
|
||||||
|
config: JobConfig,
|
||||||
|
setJobConfig: (value: any, key: string) => void,
|
||||||
|
) => {
|
||||||
|
const kwargs = {
|
||||||
|
...(config?.config?.process?.[0]?.model?.model_kwargs ?? {}),
|
||||||
|
};
|
||||||
|
if (value === "dopsd") {
|
||||||
|
kwargs.dopsd = true;
|
||||||
|
kwargs.dopsd_bleed_strength = 1.0;
|
||||||
|
} else {
|
||||||
|
delete kwargs.dopsd;
|
||||||
|
delete kwargs.dopsd_bleed_strength;
|
||||||
|
}
|
||||||
|
setJobConfig(kwargs, "config.process[0].model.model_kwargs");
|
||||||
|
setJobConfig(
|
||||||
|
value === "ta"
|
||||||
|
? "ostris/minimax_h3_training_adapter/fastvideo_fasth3_omni_training_adapter_v1.safetensors"
|
||||||
|
: undefined,
|
||||||
|
"config.process[0].model.assistant_lora_path",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
doc: {
|
||||||
|
title: "FastH3 Omni Distillation Handling",
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
FastH3 Omni is a step-distilled model, so training on it directly
|
||||||
|
will make the distillation break down. The Training Adapter (the
|
||||||
|
MiniMax-H3 Ref2V adapter) absorbs that drift during training and
|
||||||
|
is removed at inference, but can still break down over a long
|
||||||
|
run. D-OPSD instead self-distills: a no-grad teacher pass sees the
|
||||||
|
training target as its own reference and its prediction becomes
|
||||||
|
the training target for a reference-free pass, baking the
|
||||||
|
reference into your trigger word (or the caption itself when no
|
||||||
|
trigger word is set). Re-caches latents with pixel tensors.
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Image Reference Presentation",
|
||||||
|
options: [
|
||||||
|
{ value: "picture", label: "Picture (default)" },
|
||||||
|
{ value: "video", label: "Static video clip" },
|
||||||
|
],
|
||||||
|
getValue: (config: JobConfig) => {
|
||||||
|
return config?.config?.process?.[0]?.model?.model_kwargs
|
||||||
|
?.image_refs_as_video
|
||||||
|
? "video"
|
||||||
|
: "picture";
|
||||||
|
},
|
||||||
|
onChange: (
|
||||||
|
value: string,
|
||||||
|
config: JobConfig,
|
||||||
|
setJobConfig: (value: any, key: string) => void,
|
||||||
|
) => {
|
||||||
|
const kwargs = {
|
||||||
|
...(config?.config?.process?.[0]?.model?.model_kwargs ?? {}),
|
||||||
|
};
|
||||||
|
if (value === "video") {
|
||||||
|
kwargs.image_refs_as_video = true;
|
||||||
|
} else {
|
||||||
|
delete kwargs.image_refs_as_video;
|
||||||
|
delete kwargs.image_ref_video_frames;
|
||||||
|
}
|
||||||
|
setJobConfig(kwargs, "config.process[0].model.model_kwargs");
|
||||||
|
},
|
||||||
|
doc: {
|
||||||
|
title: "MiniMax-H3 Image Reference Presentation",
|
||||||
|
description: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>
|
||||||
|
How still-image references (dataset control images and sample
|
||||||
|
ctrl images) are shown to the model. Video references always use
|
||||||
|
the video path.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Picture</strong>: the native ref2va recipe — a
|
||||||
|
single-frame reference block, shown to Qwen3-VL as a{" "}
|
||||||
|
<code><Picture i></code> block, scaled down only.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Static video clip</strong>: the image is held for 5
|
||||||
|
frames (2 latent frames) and routed through the exact path a
|
||||||
|
reference VIDEO takes — video sizing (matched to the target's
|
||||||
|
pixel area), multi-frame reference block,{" "}
|
||||||
|
<code><Video k></code> timestamped presentation. Use this
|
||||||
|
when training on image references but sampling with video
|
||||||
|
references, so the LoRA learns the pathway it will be used
|
||||||
|
through. Adds a handful of rows per reference. Frame count is
|
||||||
|
adjustable with <code>model_kwargs.image_ref_video_frames</code>{" "}
|
||||||
|
(17n+5). Changing this re-caches text embeddings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
modelNotes: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>
|
||||||
|
FastVideo's FastH3 Omni: MiniMax-H3 Ref2V distilled to 8 steps.
|
||||||
|
References work exactly like MiniMax-H3 Ref2V: control images and
|
||||||
|
videos condition the output as subject/style references (never as a
|
||||||
|
first frame), from the dataset control path(s) in training and the
|
||||||
|
sample ctrl images in sampling.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Weights load from the{" "}
|
||||||
|
<Link href="/settings" className="text-blue-400 hover:underline">
|
||||||
|
Models Folder Path
|
||||||
|
</Link>{" "}
|
||||||
|
set in settings. Anything missing is downloaded there from{" "}
|
||||||
|
<code>FastVideo/FastH3-Omni-Comfy</code> on first load. The text
|
||||||
|
encoder and VAEs are the same files MiniMax-H3 uses, so an existing
|
||||||
|
MiniMax-H3 setup only needs the new DiT. Files used:
|
||||||
|
</p>
|
||||||
|
<pre className="bg-gray-900 border border-gray-700 rounded-lg p-3 text-xs overflow-x-auto">
|
||||||
|
<code>{`<MODELS_PATH>/
|
||||||
|
├── diffusion_models/
|
||||||
|
│ └── fasth3_omniref_8step_pruned_int8_convrot.safetensors
|
||||||
|
├── text_encoders/
|
||||||
|
│ └── qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors
|
||||||
|
└── vae/
|
||||||
|
├── minimax_h3_video_vae_fp16.safetensors
|
||||||
|
└── minimax_h3_audio_vae_fp32.safetensors`}</code>
|
||||||
|
</pre>
|
||||||
|
<p>
|
||||||
|
Sample with 8 steps and guidance scale 1 (the trained schedule).
|
||||||
|
Attention runs dense, like native ComfyUI. Video is fixed 24 fps and
|
||||||
|
frame counts snap down to the 17n+5 grid.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "minimax_h3_vsa",
|
name: "minimax_h3_vsa",
|
||||||
label: "FastH3 8-Step V2",
|
label: "FastH3 8-Step V2",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ ARCH_LATENT_FORMAT = {
|
|||||||
"minimax_h3": "MiniMaxH3Video",
|
"minimax_h3": "MiniMaxH3Video",
|
||||||
"minimax_h3_ref2va": "MiniMaxH3Video",
|
"minimax_h3_ref2va": "MiniMaxH3Video",
|
||||||
"minimax_h3_vsa": "MiniMaxH3Video",
|
"minimax_h3_vsa": "MiniMaxH3Video",
|
||||||
|
"minimax_h3_omni": "MiniMaxH3Video",
|
||||||
"anima": "Wan21",
|
"anima": "Wan21",
|
||||||
"cogview4": "Flux",
|
"cogview4": "Flux",
|
||||||
"prx_pixel": None, # pixel space
|
"prx_pixel": None, # pixel space
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
VERSION = "0.13.34"
|
VERSION = "0.13.35"
|
||||||
Reference in new issue
Block a user