[diffusion] feat: support hybrid conditioning for minimax h3 (#36080)
This commit is contained in:
+11
-9
@@ -105,40 +105,42 @@ def _keyframe_canvas_size(shape: Any) -> tuple[int, int]:
|
||||
geometry = str(shape["geometry"])
|
||||
if geometry != "resolved_v2":
|
||||
raise ValueError(
|
||||
"fl2va keyframe preparation requires pre-queue resolved_v2 "
|
||||
"keyframe preparation requires pre-queue resolved_v2 "
|
||||
f"geometry, got {geometry!r}"
|
||||
)
|
||||
return int(shape["width"]), int(shape["height"])
|
||||
|
||||
|
||||
def _validate_keyframe_materials(plan: Any, keyframes: list[Any]) -> tuple[int, ...]:
|
||||
if str(plan.task) != "fl2va":
|
||||
raise ValueError("keyframe target-canvas materials require plan.task='fl2va'")
|
||||
if str(plan.task) not in {"fl2va", "ref2va"}:
|
||||
raise ValueError(
|
||||
"keyframe target-canvas materials require plan.task='fl2va' or 'ref2va'"
|
||||
)
|
||||
semantic_indices = tuple(material.frame_index for material in keyframes)
|
||||
if semantic_indices not in MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES:
|
||||
raise ValueError(
|
||||
"fl2va keyframes must use one of the ordered frame_index signatures "
|
||||
"MiniMax H3 keyframes must use one of the ordered frame_index signatures "
|
||||
f"{MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES!r}, got {semantic_indices!r}"
|
||||
)
|
||||
frame_count = plan.shape.get("frame_count")
|
||||
if isinstance(frame_count, bool) or not isinstance(frame_count, int):
|
||||
raise ValueError("fl2va keyframe preparation requires an integer frame_count")
|
||||
raise ValueError("keyframe preparation requires an integer frame_count")
|
||||
if frame_count <= 1:
|
||||
raise ValueError("fl2va keyframe preparation requires frame_count > 1")
|
||||
raise ValueError("keyframe preparation requires frame_count > 1")
|
||||
expected_pixels = tuple(
|
||||
frame_count - 1 if index == -1 else index for index in semantic_indices
|
||||
)
|
||||
resolved_pixels = tuple(material.resolved_frame_index for material in keyframes)
|
||||
if resolved_pixels != expected_pixels:
|
||||
raise ValueError(
|
||||
"fl2va keyframe resolved_frame_index values disagree with semantic "
|
||||
"keyframe resolved_frame_index values disagree with semantic "
|
||||
f"anchors: expected {expected_pixels!r}, got {resolved_pixels!r}"
|
||||
)
|
||||
return semantic_indices
|
||||
|
||||
|
||||
def minimax_h3_prepared_keyframes(batch: Any, plan: Any) -> dict[str, Any]:
|
||||
"""Resolve + prepare one or two fl2va keyframes once per request.
|
||||
"""Resolve + prepare one or two first/last keyframes once per request.
|
||||
|
||||
The target canvas is shared across keyframes and must already be frozen by
|
||||
the pre-queue probe/resolve hook.
|
||||
@@ -154,7 +156,7 @@ def minimax_h3_prepared_keyframes(batch: Any, plan: Any) -> dict[str, Any]:
|
||||
cached_images = cached.get("images") or ()
|
||||
if cached_indices != semantic_indices or len(cached_images) != len(keyframes):
|
||||
raise ValueError(
|
||||
"cached fl2va keyframe preparation disagrees with the resolved plan"
|
||||
"cached keyframe preparation disagrees with the resolved plan"
|
||||
)
|
||||
return cached
|
||||
|
||||
|
||||
+40
-8
@@ -279,6 +279,8 @@ def minimax_h3_packed_sequence_ref2va_blocks(
|
||||
latent_w: int,
|
||||
audio_t: int,
|
||||
ref_blocks: Sequence[Mapping[str, object]],
|
||||
keyframe_frame_indices: list[int] | tuple[int, ...] | None = None,
|
||||
frame_count: int | None = None,
|
||||
audio_channel: int = 2,
|
||||
seq_len: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -290,10 +292,12 @@ def minimax_h3_packed_sequence_ref2va_blocks(
|
||||
- ``{"kind": "video"|"video_audio", "ref_audio_t": T,
|
||||
"latent_t": RT, "latent_h": RH, "latent_w": RW}``
|
||||
|
||||
Video-bearing blocks pack their audio rows immediately before their video
|
||||
rows; both share the same temporal origin and advance by the longer of the
|
||||
audio and video spans. Standalone audio advances the target origin by its
|
||||
own T, and image blocks advance it by one integer slot.
|
||||
Optional first/last keyframes are packed immediately after text, matching
|
||||
Comfy's hybrid Ref2VA + guide layout. Video-bearing blocks pack their audio
|
||||
rows immediately before their video rows; both share the same temporal
|
||||
origin and advance by the longer of the audio and video spans. Standalone
|
||||
audio advances the target origin by its own T, and image blocks advance it
|
||||
by one integer slot.
|
||||
"""
|
||||
if not isinstance(ref_blocks, Sequence) or isinstance(ref_blocks, (str, bytes)):
|
||||
raise ValueError("ref_blocks must be a sequence")
|
||||
@@ -345,10 +349,19 @@ def minimax_h3_packed_sequence_ref2va_blocks(
|
||||
|
||||
ph, pw = latent_h // _PATCH_H, latent_w // _PATCH_W
|
||||
frame_rows = ph * pw
|
||||
keyframe_indices = _keyframe_cond_frame_indices(
|
||||
include_keyframe_cond=keyframe_frame_indices is not None,
|
||||
keyframe_frame_indices=keyframe_frame_indices,
|
||||
)
|
||||
resolved_keyframe_indices = _resolve_keyframe_frame_indices(
|
||||
keyframe_indices,
|
||||
frame_count=frame_count,
|
||||
)
|
||||
keyframe_rows = len(keyframe_indices) * frame_rows
|
||||
video_rows = latent_t * frame_rows
|
||||
audio_rows = audio_t * audio_channel
|
||||
ref_rows = ref_visual_rows + ref_audio_rows
|
||||
used = text_len + ref_rows + audio_rows + video_rows
|
||||
used = text_len + keyframe_rows + ref_rows + audio_rows + video_rows
|
||||
if seq_len is None:
|
||||
seq_len = (
|
||||
(used + MINIMAX_H3_PACKED_SEQUENCE_ALIGNMENT - 1)
|
||||
@@ -359,7 +372,8 @@ def minimax_h3_packed_sequence_ref2va_blocks(
|
||||
raise ValueError(f"seq_len {seq_len} < used rows {used}")
|
||||
|
||||
text_sl = slice(0, text_len)
|
||||
cursor = text_len
|
||||
keyframe_sl = slice(text_len, text_len + keyframe_rows)
|
||||
cursor = keyframe_sl.stop
|
||||
block_slices: list[dict[str, object]] = []
|
||||
for item in parsed:
|
||||
kind = str(item["kind"])
|
||||
@@ -466,13 +480,31 @@ def minimax_h3_packed_sequence_ref2va_blocks(
|
||||
video_g[:, :, 0] = _video_t_grid(latent_t, t_cursor)[:, None]
|
||||
video_g[:, :, 1:] = target_frame[None]
|
||||
|
||||
for block_index, pixel_index in enumerate(resolved_keyframe_indices):
|
||||
sl = slice(
|
||||
keyframe_sl.start + block_index * frame_rows,
|
||||
keyframe_sl.start + (block_index + 1) * frame_rows,
|
||||
)
|
||||
if pixel_index == 0:
|
||||
cond_t = t_cursor
|
||||
elif frame_count is not None and pixel_index == frame_count - 1:
|
||||
cond_t = t_cursor + _temporal_position_span(latent_t) - _FRAME_RESCALE
|
||||
else:
|
||||
raise ValueError(
|
||||
"hybrid ref2va layout only supports first/last keyframe anchors, "
|
||||
f"got resolved frame index {pixel_index}"
|
||||
)
|
||||
g[sl, 0] = cond_t
|
||||
g[sl, 1:] = target_frame
|
||||
|
||||
keyframe_img_pos = _range_for_slice(keyframe_sl)
|
||||
target_img_pos = _range_for_slice(video_sl)
|
||||
target_audio_pos = _range_for_slice(audio_sl)
|
||||
img_pos = _cat_ranges(ref_img_pos_parts + [target_img_pos])
|
||||
img_pos = _cat_ranges([keyframe_img_pos] + ref_img_pos_parts + [target_img_pos])
|
||||
audio_pos = _cat_ranges(ref_audio_pos_parts + [target_audio_pos])
|
||||
|
||||
update_mask = torch.zeros(img_pos.shape[0], dtype=torch.bool)
|
||||
update_mask[ref_visual_rows:] = True
|
||||
update_mask[keyframe_rows + ref_visual_rows :] = True
|
||||
audio_update_mask = torch.zeros(audio_pos.shape[0], dtype=torch.bool)
|
||||
audio_update_mask[ref_audio_rows:] = True
|
||||
text_pos = torch.arange(0, text_len)
|
||||
|
||||
+29
-9
@@ -248,16 +248,31 @@ def _validate_conditions(
|
||||
return normalized
|
||||
|
||||
|
||||
def _validate_fl2va_conditions(conditions: Sequence[Mapping[str, Any]]) -> None:
|
||||
"""Enforce the public FL contract after per-entry schema validation."""
|
||||
def _validate_keyframe_conditions(
|
||||
conditions: Sequence[Mapping[str, Any]], *, task: str
|
||||
) -> None:
|
||||
"""Enforce the shared first/last-frame contract after schema validation."""
|
||||
|
||||
frame_indices = tuple(condition.get("frame_index") for condition in conditions)
|
||||
keyframes = [
|
||||
condition
|
||||
for condition in conditions
|
||||
if condition["role"] == MINIMAX_H3_CONDITION_ROLE_KEYFRAME
|
||||
]
|
||||
frame_indices = tuple(condition.get("frame_index") for condition in keyframes)
|
||||
if frame_indices not in MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES:
|
||||
raise ValueError(
|
||||
"conditions for task 'fl2va' must be one or two ordered "
|
||||
f"conditions for task {task!r} must include one or two ordered "
|
||||
"image/keyframe entries with frame_index [0], [-1], or [0, -1], "
|
||||
f"got {list(frame_indices)!r}"
|
||||
)
|
||||
if task == MINIMAX_H3_TASK_REF2VA and not any(
|
||||
condition["role"] == MINIMAX_H3_CONDITION_ROLE_REFERENCE
|
||||
for condition in conditions
|
||||
):
|
||||
raise ValueError(
|
||||
"ref2va keyframes require at least one reference condition; "
|
||||
"use task 'fl2va' for keyframe-only generation"
|
||||
)
|
||||
|
||||
|
||||
def minimax_h3_validate_canonical_request(
|
||||
@@ -300,11 +315,16 @@ def minimax_h3_validate_canonical_request(
|
||||
frame_count=requested_frame_count,
|
||||
)
|
||||
if profile.task == MINIMAX_H3_TASK_FL2VA:
|
||||
_validate_fl2va_conditions(normalized_conditions)
|
||||
# ref2va accepts ordered material streams containing any mix of
|
||||
# image/audio/video/video_audio references. Type admission is handled by
|
||||
# the task profile; temporal ambiguity is validated later when target
|
||||
# duration is omitted.
|
||||
_validate_keyframe_conditions(normalized_conditions, task=profile.task)
|
||||
elif profile.task == MINIMAX_H3_TASK_REF2VA and any(
|
||||
condition["role"] == MINIMAX_H3_CONDITION_ROLE_KEYFRAME
|
||||
for condition in normalized_conditions
|
||||
):
|
||||
_validate_keyframe_conditions(normalized_conditions, task=profile.task)
|
||||
# ref2va accepts ordered reference streams and, for hybrid checkpoints,
|
||||
# one first/last keyframe signature. Type admission is handled by the task
|
||||
# profile; temporal ambiguity is validated later when target duration is
|
||||
# omitted.
|
||||
if not profile.video_reference_supported:
|
||||
for index, cond in enumerate(normalized_conditions):
|
||||
if cond["type"] in ("video", "video_audio"):
|
||||
|
||||
+21
-7
@@ -276,8 +276,17 @@ def minimax_h3_resolve_plan(canonical: Mapping[str, Any]) -> MiniMaxH3ResolvedPl
|
||||
if key not in canonical:
|
||||
raise ValueError(f"canonical request missing {key!r}")
|
||||
profile = minimax_h3_task_profile(str(canonical["task"]))
|
||||
if profile.task == "fl2va":
|
||||
conditions = canonical["conditions"]
|
||||
conditions = canonical["conditions"]
|
||||
keyframe_conditions = (
|
||||
[
|
||||
condition
|
||||
for condition in conditions
|
||||
if isinstance(condition, Mapping) and condition.get("role") == "keyframe"
|
||||
]
|
||||
if isinstance(conditions, (list, tuple))
|
||||
else []
|
||||
)
|
||||
if profile.task == "fl2va" or keyframe_conditions:
|
||||
signatures = (
|
||||
[
|
||||
(
|
||||
@@ -285,10 +294,9 @@ def minimax_h3_resolve_plan(canonical: Mapping[str, Any]) -> MiniMaxH3ResolvedPl
|
||||
condition.get("role"),
|
||||
condition.get("frame_index"),
|
||||
)
|
||||
for condition in conditions
|
||||
for condition in keyframe_conditions
|
||||
]
|
||||
if isinstance(conditions, (list, tuple))
|
||||
and all(isinstance(condition, Mapping) for condition in conditions)
|
||||
if all(isinstance(condition, Mapping) for condition in keyframe_conditions)
|
||||
else []
|
||||
)
|
||||
frame_signature = tuple(signature[2] for signature in signatures)
|
||||
@@ -298,7 +306,8 @@ def minimax_h3_resolve_plan(canonical: Mapping[str, Any]) -> MiniMaxH3ResolvedPl
|
||||
or frame_signature not in MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES
|
||||
):
|
||||
raise ValueError(
|
||||
"fl2va ResolvedPlan requires one or two ordered image/keyframe "
|
||||
f"{profile.task} ResolvedPlan requires one or two ordered "
|
||||
"image/keyframe "
|
||||
"conditions with frame_index [0], [-1], or [0, -1], got "
|
||||
f"{signatures!r}"
|
||||
)
|
||||
@@ -363,10 +372,15 @@ def minimax_h3_resolve_plan(canonical: Mapping[str, Any]) -> MiniMaxH3ResolvedPl
|
||||
if rule.audio_tokenizer_encode:
|
||||
audio_encode.append(index)
|
||||
|
||||
qwen_condition_indices = [
|
||||
index
|
||||
for index, condition in enumerate(canonical["conditions"])
|
||||
if profile.task != "ref2va" or condition["role"] == "reference"
|
||||
]
|
||||
encoders = {
|
||||
"qwen": {
|
||||
"prompt": canonical["prompt"],
|
||||
"ordered_condition_indices": list(range(len(canonical["conditions"]))),
|
||||
"ordered_condition_indices": qwen_condition_indices,
|
||||
},
|
||||
"visual": visual_encode,
|
||||
"audio": audio_encode,
|
||||
|
||||
+49
-36
@@ -76,38 +76,42 @@ def minimax_h3_condition_noise_aug(sampling: Any) -> tuple[float, float]:
|
||||
return float(imgvid_noise_aug), float(audio_noise_aug)
|
||||
|
||||
|
||||
def _validate_fl2va_keyframe_payload(plan: Any, keyframe: Any) -> None:
|
||||
def _validate_keyframe_payload(plan: Any, keyframe: Any) -> None:
|
||||
"""Reject stale/middle/reordered keyframe payloads at the DiT sink."""
|
||||
|
||||
task = None if plan is None else str(plan.task)
|
||||
if task != "fl2va":
|
||||
if task not in {"fl2va", "ref2va"}:
|
||||
if keyframe is not None:
|
||||
raise ValueError(
|
||||
"keyframe condition rows are only valid for plan.task='fl2va'"
|
||||
"keyframe condition rows require plan.task='fl2va' or 'ref2va'"
|
||||
)
|
||||
return
|
||||
if keyframe is None:
|
||||
if task == "fl2va":
|
||||
raise ValueError("fl2va denoising requires encoded keyframe condition rows")
|
||||
return
|
||||
if not isinstance(keyframe, Mapping):
|
||||
raise ValueError("fl2va denoising requires encoded keyframe condition rows")
|
||||
raise ValueError("encoded keyframe condition rows must be a mapping")
|
||||
|
||||
semantic_indices = tuple(keyframe.get("semantic_frame_indices") or ())
|
||||
if semantic_indices not in MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES:
|
||||
raise ValueError(
|
||||
"fl2va denoising requires semantic_frame_indices in "
|
||||
"keyframe denoising requires semantic_frame_indices in "
|
||||
f"{MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES!r}, "
|
||||
f"got {semantic_indices!r}"
|
||||
)
|
||||
frame_count = keyframe.get("frame_count")
|
||||
if isinstance(frame_count, bool) or not isinstance(frame_count, int):
|
||||
raise ValueError("fl2va keyframe payload requires an integer frame_count")
|
||||
raise ValueError("keyframe payload requires an integer frame_count")
|
||||
if frame_count <= 1:
|
||||
raise ValueError("fl2va keyframe payload frame_count must be greater than one")
|
||||
raise ValueError("keyframe payload frame_count must be greater than one")
|
||||
pixel_indices = keyframe.get("pixel_frame_indices")
|
||||
expected_pixel_indices = [
|
||||
frame_count - 1 if index == -1 else index for index in semantic_indices
|
||||
]
|
||||
if pixel_indices != expected_pixel_indices:
|
||||
raise ValueError(
|
||||
"fl2va denoising requires pixel_frame_indices resolved from the "
|
||||
"keyframe denoising requires pixel_frame_indices resolved from the "
|
||||
"semantic anchors, "
|
||||
f"got {pixel_indices!r} for frame_count={frame_count}"
|
||||
)
|
||||
@@ -119,15 +123,15 @@ def _validate_fl2va_keyframe_payload(plan: Any, keyframe: Any) -> None:
|
||||
or any(not isinstance(entry, Mapping) for entry in entries)
|
||||
):
|
||||
raise ValueError(
|
||||
"fl2va denoising requires one encoded keyframe per semantic anchor"
|
||||
"keyframe denoising requires one encoded keyframe per semantic anchor"
|
||||
)
|
||||
if [entry.get("frame_index") for entry in entries] != list(semantic_indices):
|
||||
raise ValueError("fl2va encoded keyframes must remain in semantic anchor order")
|
||||
raise ValueError("encoded keyframes must remain in semantic anchor order")
|
||||
if [
|
||||
entry.get("resolved_frame_index") for entry in entries
|
||||
] != expected_pixel_indices:
|
||||
raise ValueError(
|
||||
"fl2va encoded keyframes must carry matching resolved_frame_index values"
|
||||
"encoded keyframes must carry matching resolved_frame_index values"
|
||||
)
|
||||
|
||||
latent_h = int(keyframe.get("latent_h") or 0)
|
||||
@@ -142,7 +146,7 @@ def _validate_fl2va_keyframe_payload(plan: Any, keyframe: Any) -> None:
|
||||
):
|
||||
actual_rows = None if not isinstance(rows, torch.Tensor) else int(rows.shape[0])
|
||||
raise ValueError(
|
||||
"fl2va encoded keyframe rows do not match target-canvas blocks: "
|
||||
"encoded keyframe rows do not match target-canvas blocks: "
|
||||
f"expected={expected_rows}, actual={actual_rows}"
|
||||
)
|
||||
|
||||
@@ -155,8 +159,26 @@ def _imgvid_condition_shapes(
|
||||
) -> list[tuple[int, int, int]]:
|
||||
"""Return visual-condition ``(T,H,W)`` in packed anchor-row order."""
|
||||
|
||||
shapes = []
|
||||
if isinstance(keyframe, Mapping):
|
||||
entries = keyframe.get("keyframes")
|
||||
if isinstance(entries, list) and entries:
|
||||
shapes.extend(
|
||||
(1, int(entry["latent_h"]), int(entry["latent_w"])) for entry in entries
|
||||
)
|
||||
else:
|
||||
latent_h = int(keyframe["latent_h"])
|
||||
latent_w = int(keyframe["latent_w"])
|
||||
frame_rows = (latent_h // 2) * (latent_w // 2)
|
||||
rows = keyframe["rows"]
|
||||
if frame_rows <= 0 or int(rows.shape[0]) % frame_rows:
|
||||
raise ValueError(
|
||||
"legacy keyframe rows cannot be split into visual-condition frames"
|
||||
)
|
||||
shapes.extend(
|
||||
[(1, latent_h, latent_w)] * (int(rows.shape[0]) // frame_rows)
|
||||
)
|
||||
if ref2va_blocks is not None:
|
||||
shapes = []
|
||||
for block in ref2va_blocks:
|
||||
kind = str(block["kind"])
|
||||
if kind == "image":
|
||||
@@ -176,23 +198,7 @@ def _imgvid_condition_shapes(
|
||||
# supplied above; reaching here indicates an upstream bug.
|
||||
raise ValueError("ref2va visual-condition shapes require ordered blocks")
|
||||
|
||||
if not isinstance(keyframe, Mapping):
|
||||
return []
|
||||
entries = keyframe.get("keyframes")
|
||||
if isinstance(entries, list) and entries:
|
||||
return [
|
||||
(1, int(entry["latent_h"]), int(entry["latent_w"])) for entry in entries
|
||||
]
|
||||
|
||||
latent_h = int(keyframe["latent_h"])
|
||||
latent_w = int(keyframe["latent_w"])
|
||||
frame_rows = (latent_h // 2) * (latent_w // 2)
|
||||
rows = keyframe["rows"]
|
||||
if frame_rows <= 0 or int(rows.shape[0]) % frame_rows:
|
||||
raise ValueError(
|
||||
"legacy keyframe rows cannot be split into visual-condition frames"
|
||||
)
|
||||
return [(1, latent_h, latent_w)] * (int(rows.shape[0]) // frame_rows)
|
||||
return shapes
|
||||
|
||||
|
||||
def _ref2va_payload_entry(
|
||||
@@ -246,6 +252,8 @@ def _ref2va_ordered_blocks_and_rows(
|
||||
for material in plan.materials:
|
||||
chain = str(material.material_chain)
|
||||
condition_index = int(material.condition_index)
|
||||
if chain == "image.target_canvas":
|
||||
continue
|
||||
if chain == "image.reference_preserve":
|
||||
entry = _ref2va_payload_entry(
|
||||
ref_image,
|
||||
@@ -789,8 +797,7 @@ class _FullLoopContext:
|
||||
def _resolve_full_loop_context(batch: Req) -> _FullLoopContext:
|
||||
"""Read/validate extras and the denoise state into a loop context.
|
||||
|
||||
Enforces the task-payload exclusivity rules (keyframe vs reference
|
||||
exclusivity) and cross-checks the resolved latent dims.
|
||||
Validates task payloads and cross-checks the resolved latent dims.
|
||||
"""
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.constants import (
|
||||
MINIMAX_H3_DENOISE_STATE_EXTRA_KEY,
|
||||
@@ -819,9 +826,7 @@ def _resolve_full_loop_context(batch: Req) -> _FullLoopContext:
|
||||
or ctx.ref_audio is not None
|
||||
or ctx.ref_video is not None
|
||||
)
|
||||
if ctx.is_ref2va and ctx.keyframe is not None:
|
||||
raise ValueError("keyframe and reference extras are mutually exclusive")
|
||||
_validate_fl2va_keyframe_payload(ctx.plan, ctx.keyframe)
|
||||
_validate_keyframe_payload(ctx.plan, ctx.keyframe)
|
||||
|
||||
ctx.latent_t = int(ctx.state["latent_t"])
|
||||
ctx.latent_h = int(ctx.state["latent_h"])
|
||||
@@ -844,7 +849,7 @@ def _assemble_condition_rows(ctx: _FullLoopContext) -> None:
|
||||
"ref2va reference extras require a resolved plan; "
|
||||
"plan-less ref2va requests are unsupported"
|
||||
)
|
||||
ctx.ref2va_positive_blocks, ctx.cond_rows, ctx.audio_ref_rows = (
|
||||
ctx.ref2va_positive_blocks, ref_rows, ctx.audio_ref_rows = (
|
||||
_ref2va_ordered_blocks_and_rows(
|
||||
plan=ctx.plan,
|
||||
ref_image=ctx.ref_image,
|
||||
@@ -852,6 +857,12 @@ def _assemble_condition_rows(ctx: _FullLoopContext) -> None:
|
||||
ref_video=ctx.ref_video,
|
||||
)
|
||||
)
|
||||
condition_parts = []
|
||||
if ctx.keyframe is not None:
|
||||
condition_parts.append(ctx.keyframe["rows"])
|
||||
if ref_rows is not None:
|
||||
condition_parts.append(ref_rows)
|
||||
ctx.cond_rows = _cat_optional(condition_parts)
|
||||
ctx.include_cond = ctx.cond_rows is not None
|
||||
else:
|
||||
ctx.cond_rows = ctx.keyframe["rows"] if ctx.include_cond else None
|
||||
@@ -885,6 +896,8 @@ def _build_packed_layout(
|
||||
latent_w=ctx.latent_w,
|
||||
audio_t=ctx.audio_t,
|
||||
ref_blocks=ctx.ref2va_positive_blocks,
|
||||
keyframe_frame_indices=ctx.keyframe_frame_indices,
|
||||
frame_count=ctx.keyframe_frame_count,
|
||||
)
|
||||
else:
|
||||
packed = minimax_h3_packed_sequence(
|
||||
|
||||
+6
-3
@@ -245,11 +245,11 @@ class MiniMaxH3TextEncodingStage(TextEncodingStage):
|
||||
keyframes = [
|
||||
m for m in plan.materials if m.material_chain == "image.target_canvas"
|
||||
]
|
||||
if plan.task == "fl2va":
|
||||
if plan.task in {"fl2va", "ref2va"} and keyframes:
|
||||
frame_indices = tuple(material.frame_index for material in keyframes)
|
||||
if frame_indices not in MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES:
|
||||
raise ValueError(
|
||||
"fl2va text encoding requires an ordered keyframe signature "
|
||||
"MiniMax H3 text encoding requires an ordered keyframe signature "
|
||||
f"in {MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES!r}, got "
|
||||
f"{frame_indices!r}"
|
||||
)
|
||||
@@ -360,7 +360,8 @@ class MiniMaxH3TextEncodingStage(TextEncodingStage):
|
||||
|
||||
Per condition in order — image i: '<Picture i>: ' label +
|
||||
vision block (prepared reference image); audio j: '<Audio j>: ' label
|
||||
only — then the verbatim prompt.
|
||||
only — then the verbatim prompt. Hybrid keyframes are deliberately
|
||||
omitted: they are guide latents appended after reference presentation.
|
||||
"""
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.presentation import (
|
||||
minimax_h3_ref2va_presentation,
|
||||
@@ -407,6 +408,8 @@ class MiniMaxH3TextEncodingStage(TextEncodingStage):
|
||||
has_image = False
|
||||
has_video = False
|
||||
for material in plan.materials:
|
||||
if material.material_chain == "image.target_canvas":
|
||||
continue
|
||||
if material.material_chain == "image.reference_preserve":
|
||||
counters["image"] += 1
|
||||
condition_labels.append(("image", counters["image"]))
|
||||
|
||||
+49
-52
@@ -166,7 +166,6 @@ class MiniMaxH3VisualEncodingStage(ConditionEncodingStage):
|
||||
"""Direct keyframe encode: seeded sampled encode_images ->
|
||||
normalized [n,96] cond rows in batch.extra."""
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.keyframe_encoding import (
|
||||
minimax_h3_encode_keyframe_cond_rows,
|
||||
minimax_h3_scoped_encode_fp32,
|
||||
)
|
||||
|
||||
@@ -177,13 +176,13 @@ class MiniMaxH3VisualEncodingStage(ConditionEncodingStage):
|
||||
for material in materials
|
||||
if material.material_chain == "image.target_canvas"
|
||||
]
|
||||
if str(plan.task) == "fl2va":
|
||||
if keyframe_materials and str(plan.task) in {"fl2va", "ref2va"}:
|
||||
frame_indices = tuple(
|
||||
material.frame_index for material in keyframe_materials
|
||||
)
|
||||
if frame_indices not in MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES:
|
||||
raise ValueError(
|
||||
"fl2va visual encoding requires an ordered keyframe signature "
|
||||
"MiniMax H3 visual encoding requires an ordered keyframe signature "
|
||||
f"in {MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES!r}, got "
|
||||
f"{frame_indices!r}"
|
||||
)
|
||||
@@ -191,37 +190,39 @@ class MiniMaxH3VisualEncodingStage(ConditionEncodingStage):
|
||||
raise ValueError(
|
||||
f"task {plan.task!r} cannot carry image.target_canvas materials"
|
||||
)
|
||||
if MINIMAX_H3_KEYFRAME_COND_ROWS_EXTRA_KEY in batch.extra:
|
||||
return
|
||||
if chains == {"image.reference_preserve"}:
|
||||
with minimax_h3_scoped_encode_fp32(self.video_vae):
|
||||
self._encode_reference_image(batch, plan)
|
||||
return
|
||||
video_chains = {
|
||||
"video.reference_preserve",
|
||||
"video_audio.reference_preserve",
|
||||
}
|
||||
if chains and chains <= {"image.reference_preserve", *video_chains}:
|
||||
# One VAE dtype toggle for both encodes below, not one each.
|
||||
with minimax_h3_scoped_encode_fp32(self.video_vae):
|
||||
if "image.reference_preserve" in chains:
|
||||
self._encode_reference_image(batch, plan)
|
||||
if chains & video_chains:
|
||||
self._encode_reference_video(batch, plan)
|
||||
return
|
||||
unsupported = [
|
||||
m.material_chain
|
||||
for m in materials
|
||||
if m.material_chain != "image.target_canvas"
|
||||
]
|
||||
supported_chains = {
|
||||
"image.target_canvas",
|
||||
"image.reference_preserve",
|
||||
*video_chains,
|
||||
}
|
||||
unsupported = sorted(chains - supported_chains)
|
||||
if unsupported:
|
||||
raise NotImplementedError(
|
||||
"MiniMaxH3VisualEncodingStage direct encode only supports "
|
||||
f"image.target_canvas / image.reference_preserve, got {unsupported}"
|
||||
"MiniMaxH3VisualEncodingStage cannot encode material chains "
|
||||
f"{unsupported}"
|
||||
)
|
||||
# One VAE dtype toggle for every visual condition in the request.
|
||||
with minimax_h3_scoped_encode_fp32(self.video_vae):
|
||||
if keyframe_materials:
|
||||
self._encode_target_keyframes(batch, plan)
|
||||
if "image.reference_preserve" in chains:
|
||||
self._encode_reference_image(batch, plan)
|
||||
if chains & video_chains:
|
||||
self._encode_reference_video(batch, plan)
|
||||
|
||||
def _encode_target_keyframes(self, batch: Req, plan) -> None:
|
||||
if MINIMAX_H3_KEYFRAME_COND_ROWS_EXTRA_KEY in batch.extra:
|
||||
return
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.canvas import (
|
||||
minimax_h3_prepared_keyframes,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.keyframe_encoding import (
|
||||
minimax_h3_encode_keyframe_cond_rows,
|
||||
)
|
||||
|
||||
# Parallel tiling gives each replicated rank complete tiles, then gathers
|
||||
# them before the seeded posterior sample.
|
||||
@@ -231,38 +232,34 @@ class MiniMaxH3VisualEncodingStage(ConditionEncodingStage):
|
||||
prepared.get("images") or ()
|
||||
) != len(prepared_indices):
|
||||
raise ValueError(
|
||||
"fl2va visual preparation requires one or two ordered images "
|
||||
"keyframe visual preparation requires one or two ordered images "
|
||||
"with a supported semantic_frame_indices signature"
|
||||
)
|
||||
encoded = []
|
||||
rows_list = []
|
||||
# One VAE dtype toggle for the whole signature (up to two keyframes),
|
||||
# not one per keyframe.
|
||||
with minimax_h3_scoped_encode_fp32(self.video_vae):
|
||||
for item in prepared["images"]:
|
||||
image = item["image"]
|
||||
width, height = item["canvas_width"], item["canvas_height"]
|
||||
# The encode sampling seed is pinned at 42 (the VAE sample
|
||||
# seed is part of the contract), independent of the
|
||||
# request seed.
|
||||
rows = minimax_h3_encode_keyframe_cond_rows(
|
||||
self.video_vae,
|
||||
image,
|
||||
self.vae_arch_config,
|
||||
)
|
||||
encoded.append(
|
||||
{
|
||||
"rows": rows,
|
||||
"latent_h": height // 16,
|
||||
"latent_w": width // 16,
|
||||
"canvas_height": height,
|
||||
"canvas_width": width,
|
||||
"frame_index": item.get("frame_index"),
|
||||
"resolved_frame_index": item.get("resolved_frame_index"),
|
||||
"condition_index": item.get("condition_index"),
|
||||
}
|
||||
)
|
||||
rows_list.append(rows)
|
||||
for item in prepared["images"]:
|
||||
image = item["image"]
|
||||
width, height = item["canvas_width"], item["canvas_height"]
|
||||
# The encode sampling seed is pinned at 42 (the VAE sample
|
||||
# seed is part of the contract), independent of the request seed.
|
||||
rows = minimax_h3_encode_keyframe_cond_rows(
|
||||
self.video_vae,
|
||||
image,
|
||||
self.vae_arch_config,
|
||||
)
|
||||
encoded.append(
|
||||
{
|
||||
"rows": rows,
|
||||
"latent_h": height // 16,
|
||||
"latent_w": width // 16,
|
||||
"canvas_height": height,
|
||||
"canvas_width": width,
|
||||
"frame_index": item.get("frame_index"),
|
||||
"resolved_frame_index": item.get("resolved_frame_index"),
|
||||
"condition_index": item.get("condition_index"),
|
||||
}
|
||||
)
|
||||
rows_list.append(rows)
|
||||
rows = rows_list[0] if len(rows_list) == 1 else torch.cat(rows_list, dim=0)
|
||||
first = encoded[0]
|
||||
batch.extra[MINIMAX_H3_KEYFRAME_COND_ROWS_EXTRA_KEY] = {
|
||||
|
||||
+15
-5
@@ -6,6 +6,9 @@ tasks (t2va / fl2va / ref2va). One row per task; stages and the request
|
||||
projector consume rows instead of branching on task names.
|
||||
|
||||
Design summary: keyframes bind target geometry; references remain independent.
|
||||
Ref2VA also admits keyframes for hybrid checkpoints, matching the released
|
||||
Comfy workflow where reference presentation is encoded before guide latents
|
||||
are appended.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -184,6 +187,13 @@ MINIMAX_H3_TASK_PROFILES: dict[str, MiniMaxH3TaskProfile] = {
|
||||
task=MINIMAX_H3_TASK_REF2VA,
|
||||
conditions_required=True,
|
||||
condition_rules=(
|
||||
MiniMaxH3ConditionRule(
|
||||
role=MINIMAX_H3_CONDITION_ROLE_KEYFRAME,
|
||||
condition_type="image",
|
||||
material_chain="image.target_canvas",
|
||||
requires_frame_index=True,
|
||||
visual_tokenizer_encode=True,
|
||||
),
|
||||
MiniMaxH3ConditionRule(
|
||||
role=MINIMAX_H3_CONDITION_ROLE_REFERENCE,
|
||||
condition_type="image",
|
||||
@@ -246,11 +256,11 @@ def _validate_profiles() -> None:
|
||||
for task, profile in MINIMAX_H3_TASK_PROFILES.items():
|
||||
if profile.task != task:
|
||||
raise ValueError(f"profile key/task mismatch: {task} vs {profile.task}")
|
||||
roles = {rule.role for rule in profile.condition_rules}
|
||||
if len(roles) > 1:
|
||||
raise ValueError(
|
||||
f"task {task}: condition roles must not mix, got {sorted(roles)}"
|
||||
)
|
||||
rule_keys = [
|
||||
(rule.role, rule.condition_type) for rule in profile.condition_rules
|
||||
]
|
||||
if len(rule_keys) != len(set(rule_keys)):
|
||||
raise ValueError(f"task {task}: condition rules must be unique")
|
||||
if profile.min_condition_count is not None and profile.min_condition_count <= 0:
|
||||
raise ValueError(f"task {task}: min_condition_count must be positive")
|
||||
if profile.max_condition_count is not None and profile.max_condition_count <= 0:
|
||||
|
||||
@@ -112,6 +112,26 @@ TARGET = {
|
||||
"video_audio.reference_preserve",
|
||||
],
|
||||
),
|
||||
(
|
||||
"ref2va",
|
||||
[
|
||||
{
|
||||
"type": "image",
|
||||
"uri": "file:///first.png",
|
||||
"role": "keyframe",
|
||||
"frame_index": 0,
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"uri": "file:///subject.png",
|
||||
"role": "reference",
|
||||
},
|
||||
],
|
||||
"ref2va",
|
||||
[0, 1],
|
||||
[],
|
||||
["image.target_canvas", "image.reference_preserve"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_public_tasks_resolve_to_exact_partition_and_encoder_plan(
|
||||
@@ -132,11 +152,35 @@ def test_public_tasks_resolve_to_exact_partition_and_encoder_plan(
|
||||
assert plan.encoders["audio"] == audio
|
||||
assert [material.material_chain for material in plan.materials] == chains
|
||||
if task == "ref2va":
|
||||
assert plan.materials[1].start_time_seconds == 12.5
|
||||
assert plan.encoders["qwen"]["ordered_condition_indices"] == [
|
||||
index
|
||||
for index, condition in enumerate(conditions)
|
||||
if condition["role"] == "reference"
|
||||
]
|
||||
for index, condition in enumerate(conditions):
|
||||
if condition.get("start_time_seconds") is not None:
|
||||
assert plan.materials[index].start_time_seconds == 12.5
|
||||
assert plan.shape["frame_count"] == 124
|
||||
assert plan.shape["video_latent_t"] == 37
|
||||
|
||||
|
||||
def test_ref2va_rejects_keyframes_without_a_reference():
|
||||
with pytest.raises(ValueError, match="at least one reference"):
|
||||
minimax_h3_validate_canonical_request(
|
||||
task="ref2va",
|
||||
prompt="contract",
|
||||
conditions=[
|
||||
{
|
||||
"type": "image",
|
||||
"uri": "file:///first.png",
|
||||
"role": "keyframe",
|
||||
"frame_index": 0,
|
||||
}
|
||||
],
|
||||
target=TARGET,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("partition", "tasks"),
|
||||
[("fl2va", ["t2va", "fl2va"]), ("ref2va", ["ref2va"])],
|
||||
|
||||
@@ -154,3 +154,31 @@ class TestMiniMaxH3PackedSequence(unittest.TestCase):
|
||||
target_video_t0 = built["img_position_ids"][built["img_pos"][12], 0]
|
||||
target_audio_t0 = built["img_position_ids"][built["audio_pos"][8], 0]
|
||||
self.assertEqual(float(target_audio_t0), float(target_video_t0))
|
||||
|
||||
def test_ref2va_hybrid_packs_keyframes_before_references(self):
|
||||
built = minimax_h3_packed_sequence_ref2va_blocks(
|
||||
text_len=5,
|
||||
latent_t=2,
|
||||
latent_h=4,
|
||||
latent_w=4,
|
||||
audio_t=5,
|
||||
ref_blocks=[{"kind": "image", "latent_h": 4, "latent_w": 4}],
|
||||
keyframe_frame_indices=[0, -1],
|
||||
frame_count=5,
|
||||
)
|
||||
|
||||
frame_rows = 4
|
||||
frozen_rows = 2 * frame_rows + frame_rows
|
||||
self.assertEqual(int((~built["update_mask"]).sum()), frozen_rows)
|
||||
keyframe_positions = built["img_pos"][: 2 * frame_rows].reshape(2, -1)
|
||||
keyframe_times = [
|
||||
float(built["img_position_ids"][positions, 0].unique().item())
|
||||
for positions in keyframe_positions
|
||||
]
|
||||
target_origin = 6.0 # text origin 5 + one reference-image time slot
|
||||
self.assertEqual(keyframe_times[0], target_origin)
|
||||
self.assertAlmostEqual(
|
||||
keyframe_times[1], target_origin + 4 * (5.0 / 3.0), places=12
|
||||
)
|
||||
target_t0 = built["img_position_ids"][built["img_pos"][frozen_rows], 0]
|
||||
self.assertEqual(float(target_t0), target_origin)
|
||||
|
||||
Reference in New Issue
Block a user